AndroidHttpClient.java revision d24b8183b93e781080b2c16c487e60d51c12da31
1/*
2 * Copyright (C) 2007 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
17package android.net.http;
18
19import org.apache.http.Header;
20import org.apache.http.HttpEntity;
21import org.apache.http.HttpEntityEnclosingRequest;
22import org.apache.http.HttpException;
23import org.apache.http.HttpHost;
24import org.apache.http.HttpRequest;
25import org.apache.http.HttpRequestInterceptor;
26import org.apache.http.HttpResponse;
27import org.apache.http.entity.AbstractHttpEntity;
28import org.apache.http.entity.ByteArrayEntity;
29import org.apache.http.client.HttpClient;
30import org.apache.http.client.ResponseHandler;
31import org.apache.http.client.ClientProtocolException;
32import org.apache.http.client.protocol.ClientContext;
33import org.apache.http.client.methods.HttpUriRequest;
34import org.apache.http.client.params.HttpClientParams;
35import org.apache.http.conn.ClientConnectionManager;
36import org.apache.http.conn.scheme.PlainSocketFactory;
37import org.apache.http.conn.scheme.Scheme;
38import org.apache.http.conn.scheme.SchemeRegistry;
39import org.apache.http.conn.ssl.SSLSocketFactory;
40import org.apache.http.impl.client.DefaultHttpClient;
41import org.apache.http.impl.client.RequestWrapper;
42import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
43import org.apache.http.params.BasicHttpParams;
44import org.apache.http.params.HttpConnectionParams;
45import org.apache.http.params.HttpParams;
46import org.apache.http.params.HttpProtocolParams;
47import org.apache.http.protocol.BasicHttpProcessor;
48import org.apache.http.protocol.HttpContext;
49import org.apache.http.protocol.BasicHttpContext;
50
51import java.io.IOException;
52import java.io.InputStream;
53import java.io.ByteArrayOutputStream;
54import java.io.OutputStream;
55import java.util.zip.GZIPInputStream;
56import java.util.zip.GZIPOutputStream;
57import java.net.URI;
58
59import android.util.Log;
60import android.content.ContentResolver;
61import android.provider.Settings;
62import android.text.TextUtils;
63
64/**
65 * Subclass of the Apache {@link DefaultHttpClient} that is configured with
66 * reasonable default settings and registered schemes for Android, and
67 * also lets the user add {@link HttpRequestInterceptor} classes.
68 * Don't create this directly, use the {@link #newInstance} factory method.
69 *
70 * <p>This client processes cookies but does not retain them by default.
71 * To retain cookies, simply add a cookie store to the HttpContext:</p>
72 *
73 * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
74 *
75 * {@hide}
76 */
77public final class AndroidHttpClient implements HttpClient {
78
79    // Gzip of data shorter than this probably won't be worthwhile
80    public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
81
82    private static final String TAG = "AndroidHttpClient";
83
84
85    /** Set if HTTP requests are blocked from being executed on this thread */
86    private static final ThreadLocal<Boolean> sThreadBlocked =
87            new ThreadLocal<Boolean>();
88
89    /** Interceptor throws an exception if the executing thread is blocked */
90    private static final HttpRequestInterceptor sThreadCheckInterceptor =
91            new HttpRequestInterceptor() {
92        public void process(HttpRequest request, HttpContext context) {
93            if (sThreadBlocked.get() != null && sThreadBlocked.get()) {
94                throw new RuntimeException("This thread forbids HTTP requests");
95            }
96        }
97    };
98
99    /**
100     * Create a new HttpClient with reasonable defaults (which you can update).
101     * @param userAgent to report in your HTTP requests.
102     * @return AndroidHttpClient for you to use for all your requests.
103     */
104    public static AndroidHttpClient newInstance(String userAgent) {
105        HttpParams params = new BasicHttpParams();
106
107        // Turn off stale checking.  Our connections break all the time anyway,
108        // and it's not worth it to pay the penalty of checking every time.
109        HttpConnectionParams.setStaleCheckingEnabled(params, false);
110
111        // Default connection and socket timeout of 20 seconds.  Tweak to taste.
112        HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
113        HttpConnectionParams.setSoTimeout(params, 20 * 1000);
114        HttpConnectionParams.setSocketBufferSize(params, 8192);
115
116        // Don't handle redirects -- return them to the caller.  Our code
117        // often wants to re-POST after a redirect, which we must do ourselves.
118        HttpClientParams.setRedirecting(params, false);
119
120        // Set the specified user agent and register standard protocols.
121        HttpProtocolParams.setUserAgent(params, userAgent);
122        SchemeRegistry schemeRegistry = new SchemeRegistry();
123        schemeRegistry.register(new Scheme("http",
124                PlainSocketFactory.getSocketFactory(), 80));
125        schemeRegistry.register(new Scheme("https",
126                SSLSocketFactory.getSocketFactory(), 443));
127        ClientConnectionManager manager =
128                new ThreadSafeClientConnManager(params, schemeRegistry);
129
130        // We use a factory method to modify superclass initialization
131        // parameters without the funny call-a-static-method dance.
132        return new AndroidHttpClient(manager, params);
133    }
134
135    private final HttpClient delegate;
136
137    private RuntimeException mLeakedException = new IllegalStateException(
138            "AndroidHttpClient created and never closed");
139
140    private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
141        this.delegate = new DefaultHttpClient(ccm, params) {
142            @Override
143            protected BasicHttpProcessor createHttpProcessor() {
144                // Add interceptor to prevent making requests from main thread.
145                BasicHttpProcessor processor = super.createHttpProcessor();
146                processor.addRequestInterceptor(sThreadCheckInterceptor);
147                processor.addRequestInterceptor(new CurlLogger());
148
149                return processor;
150            }
151
152            @Override
153            protected HttpContext createHttpContext() {
154                // Same as DefaultHttpClient.createHttpContext() minus the
155                // cookie store.
156                HttpContext context = new BasicHttpContext();
157                context.setAttribute(
158                        ClientContext.AUTHSCHEME_REGISTRY,
159                        getAuthSchemes());
160                context.setAttribute(
161                        ClientContext.COOKIESPEC_REGISTRY,
162                        getCookieSpecs());
163                context.setAttribute(
164                        ClientContext.CREDS_PROVIDER,
165                        getCredentialsProvider());
166                return context;
167            }
168        };
169    }
170
171    @Override
172    protected void finalize() throws Throwable {
173        super.finalize();
174        if (mLeakedException != null) {
175            Log.e(TAG, "Leak found", mLeakedException);
176            mLeakedException = null;
177        }
178    }
179
180    /**
181     * Block this thread from executing HTTP requests.
182     * Used to guard against HTTP requests blocking the main application thread.
183     * @param blocked if HTTP requests run on this thread should be denied
184     */
185    public static void setThreadBlocked(boolean blocked) {
186        sThreadBlocked.set(blocked);
187    }
188
189    /**
190     * Modifies a request to indicate to the server that we would like a
191     * gzipped response.  (Uses the "Accept-Encoding" HTTP header.)
192     * @param request the request to modify
193     * @see #getUngzippedContent
194     */
195    public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
196        request.addHeader("Accept-Encoding", "gzip");
197    }
198
199    /**
200     * Gets the input stream from a response entity.  If the entity is gzipped
201     * then this will get a stream over the uncompressed data.
202     *
203     * @param entity the entity whose content should be read
204     * @return the input stream to read from
205     * @throws IOException
206     */
207    public static InputStream getUngzippedContent(HttpEntity entity)
208            throws IOException {
209        InputStream responseStream = entity.getContent();
210        if (responseStream == null) return responseStream;
211        Header header = entity.getContentEncoding();
212        if (header == null) return responseStream;
213        String contentEncoding = header.getValue();
214        if (contentEncoding == null) return responseStream;
215        if (contentEncoding.contains("gzip")) responseStream
216                = new GZIPInputStream(responseStream);
217        return responseStream;
218    }
219
220    /**
221     * Release resources associated with this client.  You must call this,
222     * or significant resources (sockets and memory) may be leaked.
223     */
224    public void close() {
225        if (mLeakedException != null) {
226            getConnectionManager().shutdown();
227            mLeakedException = null;
228        }
229    }
230
231    public HttpParams getParams() {
232        return delegate.getParams();
233    }
234
235    public ClientConnectionManager getConnectionManager() {
236        return delegate.getConnectionManager();
237    }
238
239    public HttpResponse execute(HttpUriRequest request) throws IOException {
240        return delegate.execute(request);
241    }
242
243    public HttpResponse execute(HttpUriRequest request, HttpContext context)
244            throws IOException {
245        return delegate.execute(request, context);
246    }
247
248    public HttpResponse execute(HttpHost target, HttpRequest request)
249            throws IOException {
250        return delegate.execute(target, request);
251    }
252
253    public HttpResponse execute(HttpHost target, HttpRequest request,
254            HttpContext context) throws IOException {
255        return delegate.execute(target, request, context);
256    }
257
258    public <T> T execute(HttpUriRequest request,
259            ResponseHandler<? extends T> responseHandler)
260            throws IOException, ClientProtocolException {
261        return delegate.execute(request, responseHandler);
262    }
263
264    public <T> T execute(HttpUriRequest request,
265            ResponseHandler<? extends T> responseHandler, HttpContext context)
266            throws IOException, ClientProtocolException {
267        return delegate.execute(request, responseHandler, context);
268    }
269
270    public <T> T execute(HttpHost target, HttpRequest request,
271            ResponseHandler<? extends T> responseHandler) throws IOException,
272            ClientProtocolException {
273        return delegate.execute(target, request, responseHandler);
274    }
275
276    public <T> T execute(HttpHost target, HttpRequest request,
277            ResponseHandler<? extends T> responseHandler, HttpContext context)
278            throws IOException, ClientProtocolException {
279        return delegate.execute(target, request, responseHandler, context);
280    }
281
282    /**
283     * Compress data to send to server.
284     * Creates a Http Entity holding the gzipped data.
285     * The data will not be compressed if it is too short.
286     * @param data The bytes to compress
287     * @return Entity holding the data
288     */
289    public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
290            throws IOException {
291        AbstractHttpEntity entity;
292        if (data.length < getMinGzipSize(resolver)) {
293            entity = new ByteArrayEntity(data);
294        } else {
295            ByteArrayOutputStream arr = new ByteArrayOutputStream();
296            OutputStream zipper = new GZIPOutputStream(arr);
297            zipper.write(data);
298            zipper.close();
299            entity = new ByteArrayEntity(arr.toByteArray());
300            entity.setContentEncoding("gzip");
301        }
302        return entity;
303    }
304
305    /**
306     * Retrieves the minimum size for compressing data.
307     * Shorter data will not be compressed.
308     */
309    public static long getMinGzipSize(ContentResolver resolver) {
310        String sMinGzipBytes = Settings.Gservices.getString(resolver,
311                Settings.Gservices.SYNC_MIN_GZIP_BYTES);
312
313        if (!TextUtils.isEmpty(sMinGzipBytes)) {
314            try {
315                return Long.parseLong(sMinGzipBytes);
316            } catch (NumberFormatException nfe) {
317                Log.w(TAG, "Unable to parse " +
318                        Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " +
319                        sMinGzipBytes, nfe);
320            }
321        }
322        return DEFAULT_SYNC_MIN_GZIP_BYTES;
323    }
324
325    /* cURL logging support. */
326
327    /**
328     * Logging tag and level.
329     */
330    private static class LoggingConfiguration {
331
332        private final String tag;
333        private final int level;
334
335        private LoggingConfiguration(String tag, int level) {
336            this.tag = tag;
337            this.level = level;
338        }
339
340        /**
341         * Returns true if logging is turned on for this configuration.
342         */
343        private boolean isLoggable() {
344            return Log.isLoggable(tag, level);
345        }
346
347        /**
348         * Returns true if auth logging is turned on for this configuration.
349         */
350        private boolean isAuthLoggable() {
351            return Log.isLoggable(tag + "-auth", level);
352        }
353
354        /**
355         * Prints a message using this configuration.
356         */
357        private void println(String message) {
358            Log.println(level, tag, message);
359        }
360    }
361
362    /** cURL logging configuration. */
363    private volatile LoggingConfiguration curlConfiguration;
364
365    /**
366     * Enables cURL request logging for this client.
367     *
368     * @param name to log messages with
369     * @param level at which to log messages (see {@link android.util.Log})
370     */
371    public void enableCurlLogging(String name, int level) {
372        if (name == null) {
373            throw new NullPointerException("name");
374        }
375        if (level < Log.VERBOSE || level > Log.ASSERT) {
376            throw new IllegalArgumentException("Level is out of range ["
377                + Log.VERBOSE + ".." + Log.ASSERT + "]");
378        }
379
380        curlConfiguration = new LoggingConfiguration(name, level);
381    }
382
383    /**
384     * Disables cURL logging for this client.
385     */
386    public void disableCurlLogging() {
387        curlConfiguration = null;
388    }
389
390    /**
391     * Logs cURL commands equivalent to requests.
392     */
393    private class CurlLogger implements HttpRequestInterceptor {
394        public void process(HttpRequest request, HttpContext context)
395                throws HttpException, IOException {
396            LoggingConfiguration configuration = curlConfiguration;
397            if (configuration != null
398                    && configuration.isLoggable()
399                    && request instanceof HttpUriRequest) {
400                configuration.println(toCurl((HttpUriRequest) request,
401                        configuration.isAuthLoggable()));
402            }
403        }
404    }
405
406    /**
407     * Generates a cURL command equivalent to the given request.
408     */
409    private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
410        StringBuilder builder = new StringBuilder();
411
412        builder.append("curl ");
413
414        for (Header header: request.getAllHeaders()) {
415            if (!logAuthToken
416                    && (header.getName().equals("Authorization") ||
417                        header.getName().equals("Cookie"))) {
418                continue;
419            }
420            builder.append("--header \"");
421            builder.append(header.toString().trim());
422            builder.append("\" ");
423        }
424
425        URI uri = request.getURI();
426
427        // If this is a wrapped request, use the URI from the original
428        // request instead. getURI() on the wrapper seems to return a
429        // relative URI. We want an absolute URI.
430        if (request instanceof RequestWrapper) {
431            HttpRequest original = ((RequestWrapper) request).getOriginal();
432            if (original instanceof HttpUriRequest) {
433                uri = ((HttpUriRequest) original).getURI();
434            }
435        }
436
437        builder.append("\"");
438        builder.append(uri);
439        builder.append("\"");
440
441        if (request instanceof HttpEntityEnclosingRequest) {
442            HttpEntityEnclosingRequest entityRequest =
443                    (HttpEntityEnclosingRequest) request;
444            HttpEntity entity = entityRequest.getEntity();
445            if (entity != null && entity.isRepeatable()) {
446                if (entity.getContentLength() < 1024) {
447                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
448                    entity.writeTo(stream);
449                    String entityString = stream.toString();
450
451                    // TODO: Check the content type, too.
452                    builder.append(" --data-ascii \"")
453                            .append(entityString)
454                            .append("\"");
455                } else {
456                    builder.append(" [TOO MUCH DATA TO INCLUDE]");
457                }
458            }
459        }
460
461        return builder.toString();
462    }
463}
464