| /* |
| * Copyright (C) 2008 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.google.android.net; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.net.http.AndroidHttpClient; |
| import android.os.Build; |
| import android.os.NetStat; |
| import android.os.SystemClock; |
| import android.provider.Checkin; |
| import android.util.Config; |
| import android.util.Log; |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpEntityEnclosingRequest; |
| import org.apache.http.HttpHost; |
| import org.apache.http.HttpRequest; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.ProtocolException; |
| import org.apache.http.client.ClientProtocolException; |
| import org.apache.http.client.HttpClient; |
| import org.apache.http.client.ResponseHandler; |
| import org.apache.http.client.methods.HttpUriRequest; |
| import org.apache.http.conn.ClientConnectionManager; |
| import org.apache.http.conn.scheme.LayeredSocketFactory; |
| import org.apache.http.conn.scheme.Scheme; |
| import org.apache.http.conn.scheme.SchemeRegistry; |
| import org.apache.http.conn.scheme.SocketFactory; |
| import org.apache.http.impl.client.EntityEnclosingRequestWrapper; |
| import org.apache.http.impl.client.RequestWrapper; |
| import org.apache.http.params.HttpParams; |
| import org.apache.http.protocol.HttpContext; |
| import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; |
| |
| import java.io.IOException; |
| import java.net.InetAddress; |
| import java.net.Socket; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| |
| /** |
| * {@link AndroidHttpClient} wrapper that uses {@link UrlRules} to rewrite URLs |
| * and otherwise tweak HTTP requests. |
| */ |
| public class GoogleHttpClient implements HttpClient { |
| |
| private static final String TAG = "GoogleHttpClient"; |
| |
| /** Exception thrown when a request is blocked by the URL rules. */ |
| public static class BlockedRequestException extends IOException { |
| private final UrlRules.Rule mRule; |
| BlockedRequestException(UrlRules.Rule rule) { |
| super("Blocked by rule: " + rule.mName); |
| mRule = rule; |
| } |
| } |
| |
| private final AndroidHttpClient mClient; |
| private final ContentResolver mResolver; |
| private final String mAppName, mUserAgent; |
| private final ThreadLocal<Boolean> mConnectionAllocated = new ThreadLocal<Boolean>(); |
| |
| /** |
| * Create an HTTP client without SSL session persistence. |
| * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} |
| */ |
| public GoogleHttpClient(ContentResolver resolver, String userAgent) { |
| mClient = AndroidHttpClient.newInstance(userAgent); |
| mResolver = resolver; |
| mUserAgent = mAppName = userAgent; |
| } |
| |
| /** |
| * Create an HTTP client without SSL session persistence. |
| * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} |
| */ |
| public GoogleHttpClient(ContentResolver resolver, String appAndVersion, |
| boolean gzipCapable) { |
| this(resolver, null /* cache */, appAndVersion, gzipCapable); |
| } |
| |
| /** |
| * Create an HTTP client. Normaly this client is shared throughout an app. |
| * The HTTP client will construct its User-Agent as follows: |
| * |
| * <appAndVersion> (<build device> <build id>) |
| * or |
| * <appAndVersion> (<build device> <build id>); gzip |
| * (if gzip capable) |
| * |
| * The context has settings for URL rewriting rules and is used to enable |
| * SSL session persistence. |
| * |
| * @param context application context. |
| * @param appAndVersion Base app and version to use in the User-Agent. |
| * e.g., "MyApp/1.0" |
| * @param gzipCapable Whether or not this client is able to consume gzip'd |
| * responses. Only used to modify the User-Agent, not other request |
| * headers. Needed because Google servers require gzip in the User-Agent |
| * in order to return gzip'd content. |
| */ |
| public GoogleHttpClient(Context context, String appAndVersion, boolean gzipCapable) { |
| this(context.getContentResolver(), |
| SSLClientSessionCacheFactory.getCache(context), |
| appAndVersion, gzipCapable); |
| } |
| |
| private GoogleHttpClient(ContentResolver resolver, |
| SSLClientSessionCache cache, |
| String appAndVersion, boolean gzipCapable) { |
| String userAgent = appAndVersion + " (" + Build.DEVICE + " " + Build.ID + ")"; |
| if (gzipCapable) { |
| userAgent = userAgent + "; gzip"; |
| } |
| |
| mClient = AndroidHttpClient.newInstance(userAgent, cache); |
| mResolver = resolver; |
| mAppName = appAndVersion; |
| mUserAgent = userAgent; |
| |
| // Wrap all the socket factories with the appropriate wrapper. (Apache |
| // HTTP, curse its black and stupid heart, inspects the SocketFactory to |
| // see if it's a LayeredSocketFactory, so we need two wrapper classes.) |
| SchemeRegistry registry = getConnectionManager().getSchemeRegistry(); |
| for (String name : registry.getSchemeNames()) { |
| Scheme scheme = registry.unregister(name); |
| SocketFactory sf = scheme.getSocketFactory(); |
| if (sf instanceof LayeredSocketFactory) { |
| sf = new WrappedLayeredSocketFactory((LayeredSocketFactory) sf); |
| } else { |
| sf = new WrappedSocketFactory(sf); |
| } |
| registry.register(new Scheme(name, sf, scheme.getDefaultPort())); |
| } |
| } |
| |
| /** |
| * Delegating wrapper for SocketFactory records when sockets are connected. |
| * We use this to know whether a connection was created vs reused, to |
| * gather per-app statistics about connection reuse rates. |
| * (Note, we record only *connection*, not *creation* of sockets -- |
| * what we care about is the network overhead of an actual TCP connect.) |
| */ |
| private class WrappedSocketFactory implements SocketFactory { |
| private SocketFactory mDelegate; |
| private WrappedSocketFactory(SocketFactory delegate) { mDelegate = delegate; } |
| public final Socket createSocket() throws IOException { return mDelegate.createSocket(); } |
| public final boolean isSecure(Socket s) { return mDelegate.isSecure(s); } |
| |
| public final Socket connectSocket( |
| Socket s, String h, int p, |
| InetAddress la, int lp, HttpParams params) throws IOException { |
| mConnectionAllocated.set(Boolean.TRUE); |
| return mDelegate.connectSocket(s, h, p, la, lp, params); |
| } |
| } |
| |
| /** Like WrappedSocketFactory, but for the LayeredSocketFactory subclass. */ |
| private class WrappedLayeredSocketFactory |
| extends WrappedSocketFactory implements LayeredSocketFactory { |
| private LayeredSocketFactory mDelegate; |
| private WrappedLayeredSocketFactory(LayeredSocketFactory sf) { super(sf); mDelegate = sf; } |
| |
| public final Socket createSocket(Socket s, String host, int port, boolean autoClose) |
| throws IOException { |
| return mDelegate.createSocket(s, host, port, autoClose); |
| } |
| } |
| |
| /** |
| * Release resources associated with this client. You must call this, |
| * or significant resources (sockets and memory) may be leaked. |
| */ |
| public void close() { |
| mClient.close(); |
| } |
| |
| /** Execute a request without applying and rewrite rules. */ |
| public HttpResponse executeWithoutRewriting( |
| HttpUriRequest request, HttpContext context) |
| throws IOException { |
| int code = -1; |
| long start = SystemClock.elapsedRealtime(); |
| try { |
| HttpResponse response; |
| mConnectionAllocated.set(null); |
| |
| if (NetworkStatsEntity.shouldLogNetworkStats()) { |
| // TODO: if we're logging network stats, and if the apache library is configured |
| // to follow redirects, count each redirect as an additional round trip. |
| |
| int uid = android.os.Process.myUid(); |
| long startTx = NetStat.getUidTxBytes(uid); |
| long startRx = NetStat.getUidRxBytes(uid); |
| |
| response = mClient.execute(request, context); |
| HttpEntity origEntity = response == null ? null : response.getEntity(); |
| if (origEntity != null) { |
| // yeah, we compute the same thing below. we do need to compute this here |
| // so we can wrap the HttpEntity in the response. |
| long now = SystemClock.elapsedRealtime(); |
| long elapsed = now - start; |
| NetworkStatsEntity entity = new NetworkStatsEntity(origEntity, |
| mAppName, uid, startTx, startRx, |
| elapsed /* response latency */, now /* processing start time */); |
| response.setEntity(entity); |
| } |
| } else { |
| response = mClient.execute(request, context); |
| } |
| |
| code = response.getStatusLine().getStatusCode(); |
| return response; |
| } finally { |
| // Record some statistics to the checkin service about the outcome. |
| // Note that this is only describing execute(), not body download. |
| // We assume the database writes are much faster than network I/O, |
| // and not worth running in a background thread or anything. |
| try { |
| long elapsed = SystemClock.elapsedRealtime() - start; |
| ContentValues values = new ContentValues(); |
| values.put(Checkin.Stats.COUNT, 1); |
| values.put(Checkin.Stats.SUM, elapsed / 1000.0); |
| |
| values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REQUEST + ":" + mAppName); |
| mResolver.insert(Checkin.Stats.CONTENT_URI, values); |
| |
| // No sockets and no exceptions means we successfully reused a connection |
| if (mConnectionAllocated.get() == null && code >= 0) { |
| values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REUSED + ":" + mAppName); |
| mResolver.insert(Checkin.Stats.CONTENT_URI, values); |
| } |
| |
| String status = code < 0 ? "IOException" : Integer.toString(code); |
| values.put(Checkin.Stats.TAG, |
| Checkin.Stats.Tag.HTTP_STATUS + ":" + mAppName + ":" + status); |
| mResolver.insert(Checkin.Stats.CONTENT_URI, values); |
| } catch (Exception e) { |
| Log.e(TAG, "Error recording stats", e); |
| } |
| } |
| } |
| |
| public String rewriteURI(String original) { |
| UrlRules rules = UrlRules.getRules(mResolver); |
| UrlRules.Rule rule = rules.matchRule(original); |
| return rule.apply(original); |
| } |
| |
| public HttpResponse execute(HttpUriRequest request, HttpContext context) |
| throws IOException { |
| // Rewrite the supplied URL... |
| URI uri = request.getURI(); |
| String original = uri.toString(); |
| UrlRules rules = UrlRules.getRules(mResolver); |
| UrlRules.Rule rule = rules.matchRule(original); |
| String rewritten = rule.apply(original); |
| |
| if (rewritten == null) { |
| Log.w(TAG, "Blocked by " + rule.mName + ": " + original); |
| throw new BlockedRequestException(rule); |
| } else if (rewritten == original) { |
| return executeWithoutRewriting(request, context); // Pass through |
| } |
| |
| try { |
| uri = new URI(rewritten); |
| } catch (URISyntaxException e) { |
| throw new RuntimeException("Bad URL from rule: " + rule.mName, e); |
| } |
| |
| // Wrap request so we can replace the URI. |
| RequestWrapper wrapper = wrapRequest(request); |
| wrapper.setURI(uri); |
| request = wrapper; |
| |
| if (Config.LOGV) { |
| Log.v(TAG, "Rule " + rule.mName + ": " + original + " -> " + rewritten); |
| } |
| return executeWithoutRewriting(request, context); |
| } |
| |
| /** |
| * Wraps the request making it mutable. |
| */ |
| private static RequestWrapper wrapRequest(HttpUriRequest request) |
| throws IOException { |
| try { |
| // We have to wrap it with the right type. Some code performs |
| // instanceof checks. |
| RequestWrapper wrapped; |
| if (request instanceof HttpEntityEnclosingRequest) { |
| wrapped = new EntityEnclosingRequestWrapper( |
| (HttpEntityEnclosingRequest) request); |
| } else { |
| wrapped = new RequestWrapper(request); |
| } |
| |
| // Copy the headers from the original request into the wrapper. |
| wrapped.resetHeaders(); |
| |
| return wrapped; |
| } catch (ProtocolException e) { |
| throw new ClientProtocolException(e); |
| } |
| } |
| |
| /** |
| * Mark a user agent as one Google will trust to handle gzipped content. |
| * {@link AndroidHttpClient#modifyRequestToAcceptGzipResponse} is (also) |
| * necessary but not sufficient -- many browsers claim to accept gzip but |
| * have broken handling, so Google checks the user agent as well. |
| * |
| * @param originalUserAgent to modify (however you identify yourself) |
| * @return user agent with a "yes, I really can handle gzip" token added. |
| * @deprecated Use {@link #GoogleHttpClient(android.content.ContentResolver, String, boolean)} |
| */ |
| public static String getGzipCapableUserAgent(String originalUserAgent) { |
| return originalUserAgent + "; gzip"; |
| } |
| |
| // HttpClient wrapper methods. |
| |
| public HttpParams getParams() { |
| return mClient.getParams(); |
| } |
| |
| public ClientConnectionManager getConnectionManager() { |
| return mClient.getConnectionManager(); |
| } |
| |
| public HttpResponse execute(HttpUriRequest request) throws IOException { |
| return execute(request, (HttpContext) null); |
| } |
| |
| public HttpResponse execute(HttpHost target, HttpRequest request) |
| throws IOException { |
| return mClient.execute(target, request); |
| } |
| |
| public HttpResponse execute(HttpHost target, HttpRequest request, |
| HttpContext context) throws IOException { |
| return mClient.execute(target, request, context); |
| } |
| |
| public <T> T execute(HttpUriRequest request, |
| ResponseHandler<? extends T> responseHandler) |
| throws IOException, ClientProtocolException { |
| return mClient.execute(request, responseHandler); |
| } |
| |
| public <T> T execute(HttpUriRequest request, |
| ResponseHandler<? extends T> responseHandler, HttpContext context) |
| throws IOException, ClientProtocolException { |
| return mClient.execute(request, responseHandler, context); |
| } |
| |
| public <T> T execute(HttpHost target, HttpRequest request, |
| ResponseHandler<? extends T> responseHandler) throws IOException, |
| ClientProtocolException { |
| return mClient.execute(target, request, responseHandler); |
| } |
| |
| public <T> T execute(HttpHost target, HttpRequest request, |
| ResponseHandler<? extends T> responseHandler, HttpContext context) |
| throws IOException, ClientProtocolException { |
| return mClient.execute(target, request, responseHandler, context); |
| } |
| |
| /** |
| * Enables cURL request logging for this client. |
| * |
| * @param name to log messages with |
| * @param level at which to log messages (see {@link android.util.Log}) |
| */ |
| public void enableCurlLogging(String name, int level) { |
| mClient.enableCurlLogging(name, level); |
| } |
| |
| /** |
| * Disables cURL logging for this client. |
| */ |
| public void disableCurlLogging() { |
| mClient.disableCurlLogging(); |
| } |
| } |