1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.pump.util; 18 19 import android.Manifest; 20 import android.net.TrafficStats; 21 22 import androidx.annotation.NonNull; 23 import androidx.annotation.Nullable; 24 import androidx.annotation.RequiresPermission; 25 import androidx.annotation.WorkerThread; 26 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.net.HttpURLConnection; 31 import java.net.URL; 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.Comparator; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.TreeMap; 38 import java.util.concurrent.TimeUnit; 39 40 @WorkerThread 41 public final class Http { 42 private static final String TAG = Clog.tag(Http.class); 43 44 private static final int TRAFFIC_STATS_TAG = 4711; // TODO Assign a better value 45 private static final byte[] EMPTY_DATA = new byte[0]; 46 Http()47 private Http() { } 48 49 @RequiresPermission(Manifest.permission.INTERNET) post(@onNull String uri)50 public static @NonNull byte[] post(@NonNull String uri) throws IOException { 51 return post(uri, Headers.NONE, EMPTY_DATA); 52 } 53 54 @RequiresPermission(Manifest.permission.INTERNET) post(@onNull String uri, @NonNull Headers headers)55 public static @NonNull byte[] post(@NonNull String uri, @NonNull Headers headers) 56 throws IOException { 57 return post(uri, headers, EMPTY_DATA); 58 } 59 60 @RequiresPermission(Manifest.permission.INTERNET) post(@onNull String uri, @NonNull byte[] data)61 public static @NonNull byte[] post(@NonNull String uri, @NonNull byte[] data) 62 throws IOException { 63 return post(uri, Headers.NONE, data); 64 } 65 66 @RequiresPermission(Manifest.permission.INTERNET) post(@onNull String uri, @NonNull Headers headers, @NonNull byte[] data)67 public @NonNull static byte[] post(@NonNull String uri, @NonNull Headers headers, 68 @NonNull byte[] data) throws IOException { 69 return getOrPost(uri, headers, data); 70 } 71 72 @RequiresPermission(Manifest.permission.INTERNET) get(@onNull String uri)73 public static @NonNull byte[] get(@NonNull String uri) throws IOException { 74 return get(uri, Headers.NONE); 75 } 76 77 @RequiresPermission(Manifest.permission.INTERNET) get(@onNull String uri, @NonNull Headers headers)78 public static @NonNull byte[] get(@NonNull String uri, @NonNull Headers headers) 79 throws IOException { 80 return getOrPost(uri, headers, null); 81 } 82 getOrPost(String uri, Headers headers, byte[] data)83 private static byte[] getOrPost(String uri, Headers headers, byte[] data) throws IOException { 84 final URL url = new URL(uri); 85 int numRetries = 3; 86 for (;;) { 87 long retryDelaySec = 5; 88 try { 89 return getOrPost(url, headers, data); 90 } catch (Http.HttpError e) { 91 int responseCode = e.getResponseCode(); 92 if (responseCode == HttpURLConnection.HTTP_UNAVAILABLE) { 93 String retryAfter = e.getHeaders().getField("Retry-After"); 94 if (retryAfter != null) { 95 retryDelaySec = Math.max(0, Long.valueOf(retryAfter)); 96 } 97 } else if (responseCode != HttpURLConnection.HTTP_GATEWAY_TIMEOUT) { 98 throw e; 99 } 100 if (numRetries-- <= 0) { 101 throw e; 102 } 103 } catch (IOException e) { 104 if (numRetries-- <= 0) { 105 throw e; 106 } 107 } 108 109 if (retryDelaySec > 0) { 110 try { 111 Thread.sleep(TimeUnit.SECONDS.toMillis(retryDelaySec)); 112 } catch (InterruptedException e) { 113 Clog.w(TAG, "Interrupted waiting for retry", e); 114 throw new IOException(e); 115 } 116 } 117 } 118 } 119 getOrPost(URL url, Headers headers, byte[] data)120 private static byte[] getOrPost(URL url, Headers headers, byte[] data) throws IOException { 121 HttpURLConnection connection = null; 122 OutputStream outputStream = null; 123 InputStream inputStream = null; 124 final int oldTag = TrafficStats.getThreadStatsTag(); 125 try { 126 TrafficStats.setThreadStatsTag(TRAFFIC_STATS_TAG); 127 connection = (HttpURLConnection) url.openConnection(); 128 headers.apply(connection); 129 130 if (data != null) { 131 connection.setDoOutput(true); 132 connection.setFixedLengthStreamingMode(data.length); 133 134 outputStream = connection.getOutputStream(); 135 IoUtils.writeToStream(outputStream, data); 136 checkResponseCode(connection); 137 } 138 139 checkResponseCode(connection); 140 inputStream = connection.getInputStream(); 141 return IoUtils.readFromStream(inputStream); 142 } finally { 143 IoUtils.close(inputStream); 144 IoUtils.close(outputStream); 145 disconnect(connection); 146 TrafficStats.setThreadStatsTag(oldTag); 147 } 148 } 149 checkResponseCode(HttpURLConnection connection)150 private static void checkResponseCode(HttpURLConnection connection) throws IOException { 151 int responseCode = connection.getResponseCode(); 152 if (responseCode == HttpURLConnection.HTTP_OK) return; 153 String responseMessage = connection.getResponseMessage(); 154 Headers responseHeaders = new Headers(connection.getHeaderFields()); 155 156 InputStream errorStream = null; 157 try { 158 errorStream = connection.getErrorStream(); 159 if (errorStream != null) { 160 byte[] responseBody = IoUtils.readFromStream(errorStream); 161 throw new HttpError(responseCode, responseMessage, responseHeaders, responseBody); 162 } 163 throw new HttpError(responseCode, responseMessage, responseHeaders); 164 } finally { 165 IoUtils.close(errorStream); 166 } 167 } 168 disconnect(HttpURLConnection connection)169 private static void disconnect(HttpURLConnection connection) { 170 if (connection == null) return; 171 connection.disconnect(); 172 } 173 174 public static final class ContentType { ContentType()175 private ContentType() { } 176 } 177 178 public static final class Headers { 179 private final Map<String, List<String>> mFields; 180 181 public static final Headers NONE = new Headers.Builder().build(); 182 create(String contentType)183 private static Headers create(String contentType) { 184 return new Headers.Builder().set("Content-Type", contentType).build(); 185 } 186 Headers(Map<String, List<String>> fields)187 private Headers(Map<String, List<String>> fields) { 188 mFields = fields; 189 } 190 apply(@onNull HttpURLConnection connection)191 public void apply(@NonNull HttpURLConnection connection) { 192 for (Map.Entry<String, List<String>> entry : mFields.entrySet()) { 193 boolean first = true; 194 String key = entry.getKey(); 195 for (String value: entry.getValue()) { 196 if (first) { 197 first = false; 198 connection.setRequestProperty(key, value); 199 } else { 200 connection.addRequestProperty(key, value); 201 } 202 } 203 } 204 } 205 getField(@onNull String key)206 public @Nullable String getField(@NonNull String key) { 207 List<String> values = getFieldValues(key); 208 return values == null ? null : values.get(0); 209 } 210 getFieldValues(@onNull String key)211 public @Nullable List<String> getFieldValues(@NonNull String key) { 212 return getFields().get(key); 213 } 214 getFields()215 public @NonNull Map<String, List<String>> getFields() { 216 return mFields; 217 } 218 219 public static final class Builder { 220 private static final Comparator<String> FIELD_NAME_COMPARATOR = (a, b) -> { 221 //noinspection StringEquality 222 if (a == b) { 223 return 0; 224 } else if (a == null) { 225 return -1; 226 } else if (b == null) { 227 return 1; 228 } else { 229 return String.CASE_INSENSITIVE_ORDER.compare(a, b); 230 } 231 }; 232 private final List<String> mNamesAndValues = new ArrayList<>(); 233 Builder()234 public Builder() { } 235 Builder(@onNull Headers headers)236 public Builder(@NonNull Headers headers) { 237 for (Map.Entry<String, List<String>> entry : headers.mFields.entrySet()) { 238 for (String value: entry.getValue()) { 239 mNamesAndValues.add(entry.getKey()); 240 mNamesAndValues.add(value); 241 } 242 } 243 } 244 add(@onNull String fieldName, @NonNull String value)245 public @NonNull Builder add(@NonNull String fieldName, @NonNull String value) { 246 mNamesAndValues.add(fieldName); 247 mNamesAndValues.add(value); 248 return this; 249 } 250 set(@onNull String fieldName, @NonNull String value)251 public @NonNull Builder set(@NonNull String fieldName, @NonNull String value) { 252 return removeAll(fieldName).add(fieldName, value); 253 } 254 removeAll(String fieldName)255 private Builder removeAll(String fieldName) { 256 for (int i = 0; i < mNamesAndValues.size(); i += 2) { 257 if (fieldName.equalsIgnoreCase(mNamesAndValues.get(i))) { 258 mNamesAndValues.remove(i); 259 mNamesAndValues.remove(i); 260 } 261 } 262 return this; 263 } 264 build()265 public @NonNull Headers build() { 266 Map<String, List<String>> headers = new TreeMap<>(FIELD_NAME_COMPARATOR); 267 268 for (int i = 0; i < mNamesAndValues.size(); i += 2) { 269 String fieldName = mNamesAndValues.get(i); 270 String value = mNamesAndValues.get(i + 1); 271 272 List<String> values = new ArrayList<>(); 273 List<String> others = headers.get(fieldName); 274 if (others != null) { 275 values.addAll(others); 276 } 277 values.add(value); 278 headers.put(fieldName, Collections.unmodifiableList(values)); 279 } 280 281 return new Headers(Collections.unmodifiableMap(headers)); 282 } 283 } 284 } 285 286 public static final class HttpError extends IOException { 287 private static final long serialVersionUID = 1L; 288 289 private final int mCode; 290 private final String mMessage; 291 private final Headers mHeaders; 292 private final byte[] mBody; 293 HttpError(int code, String message, Headers headers)294 private HttpError(int code, String message, Headers headers) { 295 this(code, message, headers, null); 296 } 297 HttpError(int code, String message, Headers headers, byte[] body)298 private HttpError(int code, String message, Headers headers, byte[] body) { 299 super(code + " " + message); 300 mCode = code; 301 mMessage = message; 302 mHeaders = headers; 303 mBody = body; 304 } 305 getResponseCode()306 public int getResponseCode() { 307 return mCode; 308 } 309 getResponseMessage()310 public @NonNull String getResponseMessage() { 311 return mMessage; 312 } 313 getHeaders()314 public @NonNull Headers getHeaders() { 315 return mHeaders; 316 } 317 getResponseBody()318 public @Nullable byte[] getResponseBody() { 319 return mBody; 320 } 321 } 322 } 323