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