2 * Copyright (C) 2008 ZXing authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.zxing.client.android;
19 import android.util.Log;
20 import org.apache.http.Header;
21 import org.apache.http.HttpEntity;
22 import org.apache.http.HttpEntityEnclosingRequest;
23 import org.apache.http.HttpHost;
24 import org.apache.http.HttpMessage;
25 import org.apache.http.HttpRequest;
26 import org.apache.http.HttpRequestInterceptor;
27 import org.apache.http.HttpResponse;
28 import org.apache.http.client.HttpClient;
29 import org.apache.http.client.ResponseHandler;
30 import org.apache.http.client.methods.HttpUriRequest;
31 import org.apache.http.client.params.HttpClientParams;
32 import org.apache.http.client.protocol.ClientContext;
33 import org.apache.http.conn.ClientConnectionManager;
34 import org.apache.http.conn.scheme.PlainSocketFactory;
35 import org.apache.http.conn.scheme.Scheme;
36 import org.apache.http.conn.scheme.SchemeRegistry;
37 import org.apache.http.conn.ssl.SSLSocketFactory;
38 import org.apache.http.entity.AbstractHttpEntity;
39 import org.apache.http.entity.ByteArrayEntity;
40 import org.apache.http.impl.client.DefaultHttpClient;
41 import org.apache.http.impl.client.RequestWrapper;
42 import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
43 import org.apache.http.params.BasicHttpParams;
44 import org.apache.http.params.HttpConnectionParams;
45 import org.apache.http.params.HttpParams;
46 import org.apache.http.params.HttpProtocolParams;
47 import org.apache.http.protocol.BasicHttpContext;
48 import org.apache.http.protocol.BasicHttpProcessor;
49 import org.apache.http.protocol.HttpContext;
51 import java.io.ByteArrayOutputStream;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.OutputStream;
56 import java.util.zip.GZIPInputStream;
57 import java.util.zip.GZIPOutputStream;
60 * <p>Subclass of the Apache {@link DefaultHttpClient} that is configured with
61 * reasonable default settings and registered schemes for Android, and
62 * also lets the user add {@link HttpRequestInterceptor} classes.
63 * Don't create this directly, use the {@link #newInstance} factory method.</p>
65 * <p>This client processes cookies but does not retain them by default.
66 * To retain cookies, simply add a cookie store to the HttpContext:
67 * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
70 public final class AndroidHttpClient implements HttpClient {
72 // Gzip of data shorter than this probably won't be worthwhile
73 private static final long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
75 private static final String TAG = "AndroidHttpClient";
79 * Set if HTTP requests are blocked from being executed on this thread
81 private static final ThreadLocal<Boolean> sThreadBlocked =
82 new ThreadLocal<Boolean>();
85 * Interceptor throws an exception if the executing thread is blocked
87 private static final HttpRequestInterceptor sThreadCheckInterceptor =
88 new HttpRequestInterceptor() {
89 public void process(HttpRequest request, HttpContext context) {
90 if (sThreadBlocked.get() != null && sThreadBlocked.get()) {
91 throw new RuntimeException("This thread forbids HTTP requests");
97 * Create a new HttpClient with reasonable defaults (which you can update).
99 * @param userAgent to report in your HTTP requests.
100 * @return AndroidHttpClient for you to use for all your requests.
102 public static AndroidHttpClient newInstance(String userAgent) {
103 HttpParams params = new BasicHttpParams();
105 // Turn off stale checking. Our connections break all the time anyway,
106 // and it's not worth it to pay the penalty of checking every time.
107 HttpConnectionParams.setStaleCheckingEnabled(params, false);
109 // Default connection and socket timeout of 20 seconds. Tweak to taste.
110 HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
111 HttpConnectionParams.setSoTimeout(params, 20 * 1000);
112 HttpConnectionParams.setSocketBufferSize(params, 8192);
114 // Don't handle redirects -- return them to the caller. Our code
115 // often wants to re-POST after a redirect, which we must do ourselves.
116 HttpClientParams.setRedirecting(params, false);
118 // Set the specified user agent and register standard protocols.
119 HttpProtocolParams.setUserAgent(params, userAgent);
120 SchemeRegistry schemeRegistry = new SchemeRegistry();
121 schemeRegistry.register(new Scheme("http",
122 PlainSocketFactory.getSocketFactory(), 80));
123 schemeRegistry.register(new Scheme("https",
124 SSLSocketFactory.getSocketFactory(), 443));
125 ClientConnectionManager manager =
126 new ThreadSafeClientConnManager(params, schemeRegistry);
128 // We use a factory method to modify superclass initialization
129 // parameters without the funny call-a-static-method dance.
130 return new AndroidHttpClient(manager, params);
133 private final HttpClient delegate;
135 private RuntimeException mLeakedException = new IllegalStateException(
136 "AndroidHttpClient created and never closed");
138 private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
139 this.delegate = new DefaultHttpClient(ccm, params) {
141 protected BasicHttpProcessor createHttpProcessor() {
142 // Add interceptor to prevent making requests from main thread.
143 BasicHttpProcessor processor = super.createHttpProcessor();
144 processor.addRequestInterceptor(sThreadCheckInterceptor);
145 processor.addRequestInterceptor(new CurlLogger());
151 protected HttpContext createHttpContext() {
152 // Same as DefaultHttpClient.createHttpContext() minus the
154 HttpContext context = new BasicHttpContext();
155 context.setAttribute(ClientContext.AUTHSCHEME_REGISTRY, getAuthSchemes());
156 context.setAttribute(ClientContext.COOKIESPEC_REGISTRY, getCookieSpecs());
157 context.setAttribute(ClientContext.CREDS_PROVIDER, getCredentialsProvider());
164 protected void finalize() throws Throwable {
166 if (mLeakedException != null) {
167 Log.e(TAG, "Leak found", mLeakedException);
168 mLeakedException = null;
173 * Block this thread from executing HTTP requests.
174 * Used to guard against HTTP requests blocking the main application thread.
176 * @param blocked if HTTP requests run on this thread should be denied
178 public static void setThreadBlocked(boolean blocked) {
179 sThreadBlocked.set(blocked);
183 * Modifies a request to indicate to the server that we would like a
184 * gzipped response. (Uses the "Accept-Encoding" HTTP header.)
186 * @param request the request to modify
187 * @see #getUngzippedContent
189 public static void modifyRequestToAcceptGzipResponse(HttpMessage request) {
190 request.addHeader("Accept-Encoding", "gzip");
194 * Gets the input stream from a response entity. If the entity is gzipped
195 * then this will get a stream over the uncompressed data.
197 * @param entity the entity whose content should be read
198 * @return the input stream to read from
199 * @throws IOException
201 public static InputStream getUngzippedContent(HttpEntity entity) throws IOException {
202 InputStream responseStream = entity.getContent();
203 if (responseStream == null) {
204 return responseStream;
206 Header header = entity.getContentEncoding();
207 if (header == null) {
208 return responseStream;
210 String contentEncoding = header.getValue();
211 if (contentEncoding == null) {
212 return responseStream;
214 if (contentEncoding.contains("gzip")) {
215 responseStream = new GZIPInputStream(responseStream);
217 return responseStream;
221 * Release resources associated with this client. You must call this,
222 * or significant resources (sockets and memory) may be leaked.
224 public void close() {
225 if (mLeakedException != null) {
226 getConnectionManager().shutdown();
227 mLeakedException = null;
231 public HttpParams getParams() {
232 return delegate.getParams();
235 public ClientConnectionManager getConnectionManager() {
236 return delegate.getConnectionManager();
239 public HttpResponse execute(HttpUriRequest request) throws IOException {
240 return delegate.execute(request);
243 public HttpResponse execute(HttpUriRequest request, HttpContext context) throws IOException {
244 return delegate.execute(request, context);
247 public HttpResponse execute(HttpHost target, HttpRequest request) throws IOException {
248 return delegate.execute(target, request);
251 public HttpResponse execute(HttpHost target, HttpRequest request,
252 HttpContext context) throws IOException {
253 return delegate.execute(target, request, context);
256 public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler) throws IOException {
257 return delegate.execute(request, responseHandler);
260 public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
262 return delegate.execute(request, responseHandler, context);
265 public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)
267 return delegate.execute(target, request, responseHandler);
270 public <T> T execute(HttpHost target, HttpRequest request,
271 ResponseHandler<? extends T> responseHandler,
274 return delegate.execute(target, request, responseHandler, context);
278 * Compress data to send to server.
279 * Creates a Http Entity holding the gzipped data.
280 * The data will not be compressed if it is too short.
282 * @param data The bytes to compress
283 * @return Entity holding the data
285 public static AbstractHttpEntity getCompressedEntity(byte[] data) throws IOException {
286 AbstractHttpEntity entity;
287 if (data.length < getMinGzipSize()) {
288 entity = new ByteArrayEntity(data);
290 ByteArrayOutputStream arr = new ByteArrayOutputStream();
291 OutputStream zipper = new GZIPOutputStream(arr);
297 entity = new ByteArrayEntity(arr.toByteArray());
298 entity.setContentEncoding("gzip");
304 * Retrieves the minimum size for compressing data.
305 * Shorter data will not be compressed.
307 private static long getMinGzipSize() {
308 return DEFAULT_SYNC_MIN_GZIP_BYTES;
311 /* cURL logging support. */
314 * Logging tag and level.
316 private static final class LoggingConfiguration {
318 private final String tag;
319 private final int level;
321 private LoggingConfiguration(String tag, int level) {
327 * Returns true if logging is turned on for this configuration.
329 private boolean isLoggable() {
330 return Log.isLoggable(tag, level);
334 * Prints a message using this configuration.
336 private void println(String message) {
337 Log.println(level, tag, message);
342 * cURL logging configuration.
344 private volatile LoggingConfiguration curlConfiguration;
347 * Enables cURL request logging for this client.
349 * @param name to log messages with
350 * @param level at which to log messages (see {@link android.util.Log})
352 public void enableCurlLogging(String name, int level) {
354 throw new NullPointerException("name");
356 if (level < Log.VERBOSE || level > Log.ASSERT) {
357 throw new IllegalArgumentException("Level is out of range ["
358 + Log.VERBOSE + ".." + Log.ASSERT + ']');
361 curlConfiguration = new LoggingConfiguration(name, level);
365 * Disables cURL logging for this client.
367 public void disableCurlLogging() {
368 curlConfiguration = null;
372 * Logs cURL commands equivalent to requests.
374 private final class CurlLogger implements HttpRequestInterceptor {
375 public void process(HttpRequest request, HttpContext context)
377 LoggingConfiguration configuration = curlConfiguration;
378 if (configuration != null
379 && configuration.isLoggable()
380 && request instanceof HttpUriRequest) {
381 configuration.println(toCurl((HttpUriRequest) request));
386 * Generates a cURL command equivalent to the given request.
388 private String toCurl(HttpUriRequest request) throws IOException {
389 StringBuilder builder = new StringBuilder();
391 builder.append("curl ");
393 for (Header header : request.getAllHeaders()) {
394 builder.append("--header \"");
395 builder.append(header.toString().trim());
396 builder.append("\" ");
399 URI uri = request.getURI();
401 // If this is a wrapped request, use the URI from the original
402 // request instead. getURI() on the wrapper seems to return a
403 // relative URI. We want an absolute URI.
404 if (request instanceof RequestWrapper) {
405 HttpRequest original = ((RequestWrapper) request).getOriginal();
406 if (original instanceof HttpUriRequest) {
407 uri = ((HttpUriRequest) original).getURI();
415 if (request instanceof HttpEntityEnclosingRequest) {
416 HttpEntityEnclosingRequest entityRequest =
417 (HttpEntityEnclosingRequest) request;
418 HttpEntity entity = entityRequest.getEntity();
419 if (entity != null && entity.isRepeatable()) {
420 if (entity.getContentLength() < 1024) {
421 OutputStream stream = new ByteArrayOutputStream();
422 entity.writeTo(stream);
423 String entityString = stream.toString();
424 // TODO: Check the content type, too.
425 builder.append(" --data-ascii \"").append(entityString).append('"');
427 builder.append(" [TOO MUCH DATA TO INCLUDE]");
432 return builder.toString();