AndroidHttpClient.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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.CookieStore; 30import org.apache.http.client.HttpClient; 31import org.apache.http.client.ResponseHandler; 32import org.apache.http.client.ClientProtocolException; 33import org.apache.http.client.protocol.ClientContext; 34import org.apache.http.client.methods.HttpUriRequest; 35import org.apache.http.client.params.HttpClientParams; 36import org.apache.http.conn.ClientConnectionManager; 37import org.apache.http.conn.scheme.PlainSocketFactory; 38import org.apache.http.conn.scheme.Scheme; 39import org.apache.http.conn.scheme.SchemeRegistry; 40import org.apache.http.conn.ssl.SSLSocketFactory; 41import org.apache.http.impl.client.DefaultHttpClient; 42import org.apache.http.impl.client.RequestWrapper; 43import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; 44import org.apache.http.params.BasicHttpParams; 45import org.apache.http.params.HttpConnectionParams; 46import org.apache.http.params.HttpParams; 47import org.apache.http.params.HttpProtocolParams; 48import org.apache.http.protocol.BasicHttpProcessor; 49import org.apache.http.protocol.HttpContext; 50import org.apache.http.protocol.BasicHttpContext; 51 52import java.io.IOException; 53import java.io.InputStream; 54import java.io.ByteArrayOutputStream; 55import java.io.OutputStream; 56import java.util.zip.GZIPInputStream; 57import java.util.zip.GZIPOutputStream; 58import java.net.URI; 59import java.util.concurrent.atomic.AtomicInteger; 60 61import android.util.Log; 62import android.content.ContentResolver; 63import android.provider.Settings; 64import android.text.TextUtils; 65 66/** 67 * Subclass of the Apache {@link DefaultHttpClient} that is configured with 68 * reasonable default settings and registered schemes for Android, and 69 * also lets the user add {@link HttpRequestInterceptor} classes. 70 * Don't create this directly, use the {@link #newInstance} factory method. 71 * 72 * <p>This client processes cookies but does not retain them by default. 73 * To retain cookies, simply add a cookie store to the HttpContext:</p> 74 * 75 * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> 76 * 77 * {@hide} 78 */ 79public final class AndroidHttpClient implements HttpClient { 80 81 // Gzip of data shorter than this probably won't be worthwhile 82 public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; 83 84 private static final String TAG = "AndroidHttpClient"; 85 86 87 /** Set if HTTP requests are blocked from being executed on this thread */ 88 private static final ThreadLocal<Boolean> sThreadBlocked = 89 new ThreadLocal<Boolean>(); 90 91 /** Interceptor throws an exception if the executing thread is blocked */ 92 private static final HttpRequestInterceptor sThreadCheckInterceptor = 93 new HttpRequestInterceptor() { 94 public void process(HttpRequest request, HttpContext context) { 95 if (sThreadBlocked.get() != null && sThreadBlocked.get()) { 96 throw new RuntimeException("This thread forbids HTTP requests"); 97 } 98 } 99 }; 100 101 /** 102 * Create a new HttpClient with reasonable defaults (which you can update). 103 * @param userAgent to report in your HTTP requests. 104 * @return AndroidHttpClient for you to use for all your requests. 105 */ 106 public static AndroidHttpClient newInstance(String userAgent) { 107 HttpParams params = new BasicHttpParams(); 108 109 // Turn off stale checking. Our connections break all the time anyway, 110 // and it's not worth it to pay the penalty of checking every time. 111 HttpConnectionParams.setStaleCheckingEnabled(params, false); 112 113 // Default connection and socket timeout of 20 seconds. Tweak to taste. 114 HttpConnectionParams.setConnectionTimeout(params, 20 * 1000); 115 HttpConnectionParams.setSoTimeout(params, 20 * 1000); 116 HttpConnectionParams.setSocketBufferSize(params, 8192); 117 118 // Don't handle redirects -- return them to the caller. Our code 119 // often wants to re-POST after a redirect, which we must do ourselves. 120 HttpClientParams.setRedirecting(params, false); 121 122 // Set the specified user agent and register standard protocols. 123 HttpProtocolParams.setUserAgent(params, userAgent); 124 SchemeRegistry schemeRegistry = new SchemeRegistry(); 125 schemeRegistry.register(new Scheme("http", 126 PlainSocketFactory.getSocketFactory(), 80)); 127 schemeRegistry.register(new Scheme("https", 128 SSLSocketFactory.getSocketFactory(), 443)); 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 private final HttpClient delegate; 138 139 private RuntimeException mLeakedException = new IllegalStateException( 140 "AndroidHttpClient created and never closed"); 141 142 private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { 143 this.delegate = new DefaultHttpClient(ccm, params) { 144 @Override 145 protected BasicHttpProcessor createHttpProcessor() { 146 // Add interceptor to prevent making requests from main thread. 147 BasicHttpProcessor processor = super.createHttpProcessor(); 148 processor.addRequestInterceptor(sThreadCheckInterceptor); 149 processor.addRequestInterceptor(new CurlLogger()); 150 151 return processor; 152 } 153 154 @Override 155 protected HttpContext createHttpContext() { 156 // Same as DefaultHttpClient.createHttpContext() minus the 157 // cookie store. 158 HttpContext context = new BasicHttpContext(); 159 context.setAttribute( 160 ClientContext.AUTHSCHEME_REGISTRY, 161 getAuthSchemes()); 162 context.setAttribute( 163 ClientContext.COOKIESPEC_REGISTRY, 164 getCookieSpecs()); 165 context.setAttribute( 166 ClientContext.CREDS_PROVIDER, 167 getCredentialsProvider()); 168 return context; 169 } 170 }; 171 } 172 173 @Override 174 protected void finalize() throws Throwable { 175 super.finalize(); 176 if (mLeakedException != null) { 177 Log.e(TAG, "Leak found", mLeakedException); 178 mLeakedException = null; 179 } 180 } 181 182 /** 183 * Block this thread from executing HTTP requests. 184 * Used to guard against HTTP requests blocking the main application thread. 185 * @param blocked if HTTP requests run on this thread should be denied 186 */ 187 public static void setThreadBlocked(boolean blocked) { 188 sThreadBlocked.set(blocked); 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 String sMinGzipBytes = Settings.Gservices.getString(resolver, 313 Settings.Gservices.SYNC_MIN_GZIP_BYTES); 314 315 if (!TextUtils.isEmpty(sMinGzipBytes)) { 316 try { 317 return Long.parseLong(sMinGzipBytes); 318 } catch (NumberFormatException nfe) { 319 Log.w(TAG, "Unable to parse " + 320 Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " + 321 sMinGzipBytes, nfe); 322 } 323 } 324 return DEFAULT_SYNC_MIN_GZIP_BYTES; 325 } 326 327 /* cURL logging support. */ 328 329 /** 330 * Logging tag and level. 331 */ 332 private static class LoggingConfiguration { 333 334 private final String tag; 335 private final int level; 336 337 private LoggingConfiguration(String tag, int level) { 338 this.tag = tag; 339 this.level = level; 340 } 341 342 /** 343 * Returns true if logging is turned on for this configuration. 344 */ 345 private boolean isLoggable() { 346 return Log.isLoggable(tag, level); 347 } 348 349 /** 350 * Prints a message using this configuration. 351 */ 352 private void println(String message) { 353 Log.println(level, tag, message); 354 } 355 } 356 357 /** cURL logging configuration. */ 358 private volatile LoggingConfiguration curlConfiguration; 359 360 /** 361 * Enables cURL request logging for this client. 362 * 363 * @param name to log messages with 364 * @param level at which to log messages (see {@link android.util.Log}) 365 */ 366 public void enableCurlLogging(String name, int level) { 367 if (name == null) { 368 throw new NullPointerException("name"); 369 } 370 if (level < Log.VERBOSE || level > Log.ASSERT) { 371 throw new IllegalArgumentException("Level is out of range [" 372 + Log.VERBOSE + ".." + Log.ASSERT + "]"); 373 } 374 375 curlConfiguration = new LoggingConfiguration(name, level); 376 } 377 378 /** 379 * Disables cURL logging for this client. 380 */ 381 public void disableCurlLogging() { 382 curlConfiguration = null; 383 } 384 385 /** 386 * Logs cURL commands equivalent to requests. 387 */ 388 private class CurlLogger implements HttpRequestInterceptor { 389 public void process(HttpRequest request, HttpContext context) 390 throws HttpException, IOException { 391 LoggingConfiguration configuration = curlConfiguration; 392 if (configuration != null 393 && configuration.isLoggable() 394 && request instanceof HttpUriRequest) { 395 configuration.println(toCurl((HttpUriRequest) request)); 396 } 397 } 398 } 399 400 /** 401 * Generates a cURL command equivalent to the given request. 402 */ 403 private static String toCurl(HttpUriRequest request) throws IOException { 404 StringBuilder builder = new StringBuilder(); 405 406 builder.append("curl "); 407 408 for (Header header: request.getAllHeaders()) { 409 builder.append("--header \""); 410 builder.append(header.toString().trim()); 411 builder.append("\" "); 412 } 413 414 URI uri = request.getURI(); 415 416 // If this is a wrapped request, use the URI from the original 417 // request instead. getURI() on the wrapper seems to return a 418 // relative URI. We want an absolute URI. 419 if (request instanceof RequestWrapper) { 420 HttpRequest original = ((RequestWrapper) request).getOriginal(); 421 if (original instanceof HttpUriRequest) { 422 uri = ((HttpUriRequest) original).getURI(); 423 } 424 } 425 426 builder.append("\""); 427 builder.append(uri); 428 builder.append("\""); 429 430 if (request instanceof HttpEntityEnclosingRequest) { 431 HttpEntityEnclosingRequest entityRequest = 432 (HttpEntityEnclosingRequest) request; 433 HttpEntity entity = entityRequest.getEntity(); 434 if (entity != null && entity.isRepeatable()) { 435 if (entity.getContentLength() < 1024) { 436 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 437 entity.writeTo(stream); 438 String entityString = stream.toString(); 439 440 // TODO: Check the content type, too. 441 builder.append(" --data-ascii \"") 442 .append(entityString) 443 .append("\""); 444 } else { 445 builder.append(" [TOO MUCH DATA TO INCLUDE]"); 446 } 447 } 448 } 449 450 return builder.toString(); 451 } 452} 453