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