packages/apps/Dialer/java/com/android/incallui/calllocation/impl/HttpFetcher.java

287 lines
9.5 KiB
Java

/*
* Copyright (C) 2017 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.incallui.calllocation.impl;
import android.content.Context;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.SystemClock;
import android.util.Pair;
import com.android.dialer.common.LogUtil;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.MoreStrings;
import com.google.android.common.http.UrlRules;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/** Utility for making http requests. */
public class HttpFetcher {
// Phone number
public static final String PARAM_ID = "id";
// auth token
public static final String PARAM_ACCESS_TOKEN = "access_token";
private static final String TAG = HttpFetcher.class.getSimpleName();
/**
* Send a http request to the given url.
*
* @param urlString The url to request.
* @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
* @throws java.io.IOException when an error occurs.
*/
public static byte[] sendRequestAsByteArray(
Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
throws IOException, AuthException {
Objects.requireNonNull(urlString);
URL url = reWriteUrl(context, urlString);
if (url == null) {
return null;
}
HttpURLConnection conn = null;
InputStream is = null;
boolean isError = false;
final long start = SystemClock.uptimeMillis();
try {
conn = (HttpURLConnection) url.openConnection();
setMethodAndHeaders(conn, requestMethod, headers);
int responseCode = conn.getResponseCode();
LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
// All 2xx codes are successful.
if (responseCode / 100 == 2) {
is = conn.getInputStream();
} else {
is = conn.getErrorStream();
isError = true;
}
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
if (isError) {
handleBadResponse(url.toString(), baos.toByteArray());
if (responseCode == 401) {
throw new AuthException("Auth error");
}
return null;
}
byte[] response = baos.toByteArray();
LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
long end = SystemClock.uptimeMillis();
LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
return response;
} finally {
DialerUtils.closeQuietly(is);
if (conn != null) {
conn.disconnect();
}
}
}
/**
* Send a http request to the given url.
*
* @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
* @throws java.io.IOException when an error occurs.
*/
public static InputStream sendRequestAsInputStream(
Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
throws IOException, AuthException {
Objects.requireNonNull(urlString);
URL url = reWriteUrl(context, urlString);
if (url == null) {
return null;
}
HttpURLConnection httpUrlConnection = null;
boolean isSuccess = false;
try {
httpUrlConnection = (HttpURLConnection) url.openConnection();
setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
int responseCode = httpUrlConnection.getResponseCode();
LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
if (responseCode == 401) {
throw new AuthException("Auth error");
} else if (responseCode / 100 == 2) { // All 2xx codes are successful.
InputStream is = httpUrlConnection.getInputStream();
if (is != null) {
is = new HttpInputStreamWrapper(httpUrlConnection, is);
isSuccess = true;
return is;
}
}
return null;
} finally {
if (httpUrlConnection != null && !isSuccess) {
httpUrlConnection.disconnect();
}
}
}
/**
* Set http method and headers.
*
* @param conn The connection to add headers to.
* @param requestMethod request method
* @param headers http headers where the first item in the pair is the key and second item is the
* value.
*/
private static void setMethodAndHeaders(
HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
throws ProtocolException {
conn.setRequestMethod(requestMethod);
if (headers != null) {
for (Pair<String, String> pair : headers) {
conn.setRequestProperty(pair.first, pair.second);
}
}
}
private static String obfuscateUrl(String urlString) {
final Uri uri = Uri.parse(urlString);
final Builder builder =
new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
final Set<String> names = uri.getQueryParameterNames();
for (String name : names) {
if (PARAM_ACCESS_TOKEN.equals(name)) {
builder.appendQueryParameter(name, "token");
} else {
final String value = uri.getQueryParameter(name);
if (PARAM_ID.equals(name)) {
builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
} else {
builder.appendQueryParameter(name, value);
}
}
}
return builder.toString();
}
/** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
public static String getRequestAsString(Context context, String urlString)
throws IOException, AuthException {
return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
}
/**
* Send a http request to the given url.
*
* @param context The android context.
* @param urlString The url to request.
* @param headers Http headers to pass in the request. {@literal null} is allowed.
* @return The response body as a String. Or {@literal null} if status code is not 2xx.
* @throws java.io.IOException when an error occurs.
*/
public static String getRequestAsString(
Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
throws IOException, AuthException {
final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
if (byteArr == null) {
// Encountered error response... just return.
return null;
}
final String response = new String(byteArr);
LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
return response;
}
/**
* Lookup up url re-write rules from gServices and apply to the given url.
*
* @return The new url.
*/
private static URL reWriteUrl(Context context, String url) {
final UrlRules rules = UrlRules.getRules(context.getContentResolver());
final UrlRules.Rule rule = rules.matchRule(url);
final String newUrl = rule.apply(url);
if (newUrl == null) {
if (LogUtil.isDebugEnabled()) {
// Url is blocked by re-write.
LogUtil.i(
"HttpFetcher.reWriteUrl",
"url " + obfuscateUrl(url) + " is blocked. Ignoring request.");
}
return null;
}
if (LogUtil.isDebugEnabled()) {
LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
if (!newUrl.equals(url)) {
LogUtil.i(
"HttpFetcher.reWriteUrl",
"Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
}
}
URL urlObject = null;
try {
urlObject = new URL(newUrl);
} catch (MalformedURLException e) {
LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
}
return urlObject;
}
private static void handleBadResponse(String url, byte[] response) {
LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
}
/** Disconnect {@link HttpURLConnection} when InputStream is closed */
private static class HttpInputStreamWrapper extends FilterInputStream {
final HttpURLConnection httpUrlConnection;
final long startMillis = SystemClock.uptimeMillis();
public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
super(in);
httpUrlConnection = conn;
}
@Override
public void close() throws IOException {
super.close();
httpUrlConnection.disconnect();
if (LogUtil.isDebugEnabled()) {
long endMillis = SystemClock.uptimeMillis();
LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - startMillis) + " ms");
}
}
}
}