blob: 49130ba6c4003fbdf6cc11e429a41bcde3f8bac0 [file] [log] [blame]
/*
* 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;
}
}
}