| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.pump.util; |
| |
| import android.Manifest; |
| import android.net.TrafficStats; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RequiresPermission; |
| import androidx.annotation.WorkerThread; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.HttpURLConnection; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.TreeMap; |
| import java.util.concurrent.TimeUnit; |
| |
| @WorkerThread |
| public final class Http { |
| private static final String TAG = Clog.tag(Http.class); |
| |
| private static final int TRAFFIC_STATS_TAG = 4711; // TODO Assign a better value |
| private static final byte[] EMPTY_DATA = new byte[0]; |
| |
| private Http() { } |
| |
| @RequiresPermission(Manifest.permission.INTERNET) |
| public static @NonNull byte[] post(@NonNull String uri) throws IOException { |
| return post(uri, Headers.NONE, EMPTY_DATA); |
| } |
| |
| @RequiresPermission(Manifest.permission.INTERNET) |
| public static @NonNull byte[] post(@NonNull String uri, @NonNull Headers headers) |
| throws IOException { |
| return post(uri, headers, EMPTY_DATA); |
| } |
| |
| @RequiresPermission(Manifest.permission.INTERNET) |
| public static @NonNull byte[] post(@NonNull String uri, @NonNull byte[] data) |
| throws IOException { |
| return post(uri, Headers.NONE, data); |
| } |
| |
| @RequiresPermission(Manifest.permission.INTERNET) |
| public @NonNull static byte[] post(@NonNull String uri, @NonNull Headers headers, |
| @NonNull byte[] data) throws IOException { |
| return getOrPost(uri, headers, data); |
| } |
| |
| @RequiresPermission(Manifest.permission.INTERNET) |
| public static @NonNull byte[] get(@NonNull String uri) throws IOException { |
| return get(uri, Headers.NONE); |
| } |
| |
| @RequiresPermission(Manifest.permission.INTERNET) |
| public static @NonNull byte[] get(@NonNull String uri, @NonNull Headers headers) |
| throws IOException { |
| return getOrPost(uri, headers, null); |
| } |
| |
| private static byte[] getOrPost(String uri, Headers headers, byte[] data) throws IOException { |
| final URL url = new URL(uri); |
| int numRetries = 3; |
| for (;;) { |
| long retryDelaySec = 5; |
| try { |
| return getOrPost(url, headers, data); |
| } catch (Http.HttpError e) { |
| int responseCode = e.getResponseCode(); |
| if (responseCode == HttpURLConnection.HTTP_UNAVAILABLE) { |
| String retryAfter = e.getHeaders().getField("Retry-After"); |
| if (retryAfter != null) { |
| retryDelaySec = Math.max(0, Long.valueOf(retryAfter)); |
| } |
| } else if (responseCode != HttpURLConnection.HTTP_GATEWAY_TIMEOUT) { |
| throw e; |
| } |
| if (numRetries-- <= 0) { |
| throw e; |
| } |
| } catch (IOException e) { |
| if (numRetries-- <= 0) { |
| throw e; |
| } |
| } |
| |
| if (retryDelaySec > 0) { |
| try { |
| Thread.sleep(TimeUnit.SECONDS.toMillis(retryDelaySec)); |
| } catch (InterruptedException e) { |
| Clog.w(TAG, "Interrupted waiting for retry", e); |
| throw new IOException(e); |
| } |
| } |
| } |
| } |
| |
| private static byte[] getOrPost(URL url, Headers headers, byte[] data) throws IOException { |
| HttpURLConnection connection = null; |
| OutputStream outputStream = null; |
| InputStream inputStream = null; |
| final int oldTag = TrafficStats.getThreadStatsTag(); |
| try { |
| TrafficStats.setThreadStatsTag(TRAFFIC_STATS_TAG); |
| connection = (HttpURLConnection) url.openConnection(); |
| headers.apply(connection); |
| |
| if (data != null) { |
| connection.setDoOutput(true); |
| connection.setFixedLengthStreamingMode(data.length); |
| |
| outputStream = connection.getOutputStream(); |
| IoUtils.writeToStream(outputStream, data); |
| checkResponseCode(connection); |
| } |
| |
| checkResponseCode(connection); |
| inputStream = connection.getInputStream(); |
| return IoUtils.readFromStream(inputStream); |
| } finally { |
| IoUtils.close(inputStream); |
| IoUtils.close(outputStream); |
| disconnect(connection); |
| TrafficStats.setThreadStatsTag(oldTag); |
| } |
| } |
| |
| private static void checkResponseCode(HttpURLConnection connection) throws IOException { |
| int responseCode = connection.getResponseCode(); |
| if (responseCode == HttpURLConnection.HTTP_OK) return; |
| String responseMessage = connection.getResponseMessage(); |
| Headers responseHeaders = new Headers(connection.getHeaderFields()); |
| |
| InputStream errorStream = null; |
| try { |
| errorStream = connection.getErrorStream(); |
| if (errorStream != null) { |
| byte[] responseBody = IoUtils.readFromStream(errorStream); |
| throw new HttpError(responseCode, responseMessage, responseHeaders, responseBody); |
| } |
| throw new HttpError(responseCode, responseMessage, responseHeaders); |
| } finally { |
| IoUtils.close(errorStream); |
| } |
| } |
| |
| private static void disconnect(HttpURLConnection connection) { |
| if (connection == null) return; |
| connection.disconnect(); |
| } |
| |
| public static final class ContentType { |
| private ContentType() { } |
| } |
| |
| public static final class Headers { |
| private final Map<String, List<String>> mFields; |
| |
| public static final Headers NONE = new Headers.Builder().build(); |
| |
| private static Headers create(String contentType) { |
| return new Headers.Builder().set("Content-Type", contentType).build(); |
| } |
| |
| private Headers(Map<String, List<String>> fields) { |
| mFields = fields; |
| } |
| |
| public void apply(@NonNull HttpURLConnection connection) { |
| for (Map.Entry<String, List<String>> entry : mFields.entrySet()) { |
| boolean first = true; |
| String key = entry.getKey(); |
| for (String value: entry.getValue()) { |
| if (first) { |
| first = false; |
| connection.setRequestProperty(key, value); |
| } else { |
| connection.addRequestProperty(key, value); |
| } |
| } |
| } |
| } |
| |
| public @Nullable String getField(@NonNull String key) { |
| List<String> values = getFieldValues(key); |
| return values == null ? null : values.get(0); |
| } |
| |
| public @Nullable List<String> getFieldValues(@NonNull String key) { |
| return getFields().get(key); |
| } |
| |
| public @NonNull Map<String, List<String>> getFields() { |
| return mFields; |
| } |
| |
| public static final class Builder { |
| private static final Comparator<String> FIELD_NAME_COMPARATOR = (a, b) -> { |
| //noinspection StringEquality |
| if (a == b) { |
| return 0; |
| } else if (a == null) { |
| return -1; |
| } else if (b == null) { |
| return 1; |
| } else { |
| return String.CASE_INSENSITIVE_ORDER.compare(a, b); |
| } |
| }; |
| private final List<String> mNamesAndValues = new ArrayList<>(); |
| |
| public Builder() { } |
| |
| public Builder(@NonNull Headers headers) { |
| for (Map.Entry<String, List<String>> entry : headers.mFields.entrySet()) { |
| for (String value: entry.getValue()) { |
| mNamesAndValues.add(entry.getKey()); |
| mNamesAndValues.add(value); |
| } |
| } |
| } |
| |
| public @NonNull Builder add(@NonNull String fieldName, @NonNull String value) { |
| mNamesAndValues.add(fieldName); |
| mNamesAndValues.add(value); |
| return this; |
| } |
| |
| public @NonNull Builder set(@NonNull String fieldName, @NonNull String value) { |
| return removeAll(fieldName).add(fieldName, value); |
| } |
| |
| private Builder removeAll(String fieldName) { |
| for (int i = 0; i < mNamesAndValues.size(); i += 2) { |
| if (fieldName.equalsIgnoreCase(mNamesAndValues.get(i))) { |
| mNamesAndValues.remove(i); |
| mNamesAndValues.remove(i); |
| } |
| } |
| return this; |
| } |
| |
| public @NonNull Headers build() { |
| Map<String, List<String>> headers = new TreeMap<>(FIELD_NAME_COMPARATOR); |
| |
| for (int i = 0; i < mNamesAndValues.size(); i += 2) { |
| String fieldName = mNamesAndValues.get(i); |
| String value = mNamesAndValues.get(i + 1); |
| |
| List<String> values = new ArrayList<>(); |
| List<String> others = headers.get(fieldName); |
| if (others != null) { |
| values.addAll(others); |
| } |
| values.add(value); |
| headers.put(fieldName, Collections.unmodifiableList(values)); |
| } |
| |
| return new Headers(Collections.unmodifiableMap(headers)); |
| } |
| } |
| } |
| |
| public static final class HttpError extends IOException { |
| private static final long serialVersionUID = 1L; |
| |
| private final int mCode; |
| private final String mMessage; |
| private final Headers mHeaders; |
| private final byte[] mBody; |
| |
| private HttpError(int code, String message, Headers headers) { |
| this(code, message, headers, null); |
| } |
| |
| private HttpError(int code, String message, Headers headers, byte[] body) { |
| super(code + " " + message); |
| mCode = code; |
| mMessage = message; |
| mHeaders = headers; |
| mBody = body; |
| } |
| |
| public int getResponseCode() { |
| return mCode; |
| } |
| |
| public @NonNull String getResponseMessage() { |
| return mMessage; |
| } |
| |
| public @NonNull Headers getHeaders() { |
| return mHeaders; |
| } |
| |
| public @Nullable byte[] getResponseBody() { |
| return mBody; |
| } |
| } |
| } |