RequestQueue.java revision 86806ce11a89260147d7c2efa2c192b711d923db
1/* 2 * Copyright (C) 2006 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 17/** 18 * High level HTTP Interface 19 * Queues requests as necessary 20 */ 21 22package android.net.http; 23 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.net.ConnectivityManager; 29import android.net.NetworkInfo; 30import android.net.Proxy; 31import android.net.WebAddress; 32import android.os.Handler; 33import android.os.Message; 34import android.os.SystemProperties; 35import android.text.TextUtils; 36import android.util.Log; 37 38import java.io.InputStream; 39import java.util.Iterator; 40import java.util.LinkedHashMap; 41import java.util.LinkedList; 42import java.util.ListIterator; 43import java.util.Map; 44 45import org.apache.http.HttpHost; 46 47/** 48 * {@hide} 49 */ 50public class RequestQueue implements RequestFeeder { 51 52 53 /** 54 * Requests, indexed by HttpHost (scheme, host, port) 55 */ 56 private final LinkedHashMap<HttpHost, LinkedList<Request>> mPending; 57 private final Context mContext; 58 private final ActivePool mActivePool; 59 private final ConnectivityManager mConnectivityManager; 60 61 private HttpHost mProxyHost = null; 62 private BroadcastReceiver mProxyChangeReceiver; 63 64 /* default simultaneous connection count */ 65 private static final int CONNECTION_COUNT = 4; 66 67 /** 68 * This class maintains active connection threads 69 */ 70 class ActivePool implements ConnectionManager { 71 /** Threads used to process requests */ 72 ConnectionThread[] mThreads; 73 74 IdleCache mIdleCache; 75 76 private int mTotalRequest; 77 private int mTotalConnection; 78 private int mConnectionCount; 79 80 ActivePool(int connectionCount) { 81 mIdleCache = new IdleCache(); 82 mConnectionCount = connectionCount; 83 mThreads = new ConnectionThread[mConnectionCount]; 84 85 for (int i = 0; i < mConnectionCount; i++) { 86 mThreads[i] = new ConnectionThread( 87 mContext, i, this, RequestQueue.this); 88 } 89 } 90 91 void startup() { 92 for (int i = 0; i < mConnectionCount; i++) { 93 mThreads[i].start(); 94 } 95 } 96 97 void shutdown() { 98 for (int i = 0; i < mConnectionCount; i++) { 99 mThreads[i].requestStop(); 100 } 101 } 102 103 void startConnectionThread() { 104 synchronized (RequestQueue.this) { 105 RequestQueue.this.notify(); 106 } 107 } 108 109 public void startTiming() { 110 for (int i = 0; i < mConnectionCount; i++) { 111 ConnectionThread rt = mThreads[i]; 112 rt.mCurrentThreadTime = -1; 113 rt.mTotalThreadTime = 0; 114 } 115 mTotalRequest = 0; 116 mTotalConnection = 0; 117 } 118 119 public void stopTiming() { 120 int totalTime = 0; 121 for (int i = 0; i < mConnectionCount; i++) { 122 ConnectionThread rt = mThreads[i]; 123 if (rt.mCurrentThreadTime != -1) { 124 totalTime += rt.mTotalThreadTime; 125 } 126 rt.mCurrentThreadTime = 0; 127 } 128 Log.d("Http", "Http thread used " + totalTime + " ms " + " for " 129 + mTotalRequest + " requests and " + mTotalConnection 130 + " new connections"); 131 } 132 133 void logState() { 134 StringBuilder dump = new StringBuilder(); 135 for (int i = 0; i < mConnectionCount; i++) { 136 dump.append(mThreads[i] + "\n"); 137 } 138 HttpLog.v(dump.toString()); 139 } 140 141 142 public HttpHost getProxyHost() { 143 return mProxyHost; 144 } 145 146 /** 147 * Turns off persistence on all live connections 148 */ 149 void disablePersistence() { 150 for (int i = 0; i < mConnectionCount; i++) { 151 Connection connection = mThreads[i].mConnection; 152 if (connection != null) connection.setCanPersist(false); 153 } 154 mIdleCache.clear(); 155 } 156 157 /* Linear lookup -- okay for small thread counts. Might use 158 private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap; 159 if this turns out to be a hotspot */ 160 ConnectionThread getThread(HttpHost host) { 161 synchronized(RequestQueue.this) { 162 for (int i = 0; i < mThreads.length; i++) { 163 ConnectionThread ct = mThreads[i]; 164 Connection connection = ct.mConnection; 165 if (connection != null && connection.mHost.equals(host)) { 166 return ct; 167 } 168 } 169 } 170 return null; 171 } 172 173 public Connection getConnection(Context context, HttpHost host) { 174 host = RequestQueue.this.determineHost(host); 175 Connection con = mIdleCache.getConnection(host); 176 if (con == null) { 177 mTotalConnection++; 178 con = Connection.getConnection(mContext, host, mProxyHost, 179 RequestQueue.this); 180 } 181 return con; 182 } 183 public boolean recycleConnection(Connection connection) { 184 return mIdleCache.cacheConnection(connection.getHost(), connection); 185 } 186 187 } 188 189 /** 190 * A RequestQueue class instance maintains a set of queued 191 * requests. It orders them, makes the requests against HTTP 192 * servers, and makes callbacks to supplied eventHandlers as data 193 * is read. It supports request prioritization, connection reuse 194 * and pipelining. 195 * 196 * @param context application context 197 */ 198 public RequestQueue(Context context) { 199 this(context, CONNECTION_COUNT); 200 } 201 202 /** 203 * A RequestQueue class instance maintains a set of queued 204 * requests. It orders them, makes the requests against HTTP 205 * servers, and makes callbacks to supplied eventHandlers as data 206 * is read. It supports request prioritization, connection reuse 207 * and pipelining. 208 * 209 * @param context application context 210 * @param connectionCount The number of simultaneous connections 211 */ 212 public RequestQueue(Context context, int connectionCount) { 213 mContext = context; 214 215 mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32); 216 217 mActivePool = new ActivePool(connectionCount); 218 mActivePool.startup(); 219 220 mConnectivityManager = (ConnectivityManager) 221 context.getSystemService(Context.CONNECTIVITY_SERVICE); 222 } 223 224 /** 225 * Enables data state and proxy tracking 226 */ 227 public synchronized void enablePlatformNotifications() { 228 if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network"); 229 230 if (mProxyChangeReceiver == null) { 231 mProxyChangeReceiver = 232 new BroadcastReceiver() { 233 @Override 234 public void onReceive(Context ctx, Intent intent) { 235 setProxyConfig(); 236 } 237 }; 238 mContext.registerReceiver(mProxyChangeReceiver, 239 new IntentFilter(Proxy.PROXY_CHANGE_ACTION)); 240 } 241 } 242 243 /** 244 * If platform notifications have been enabled, call this method 245 * to disable before destroying RequestQueue 246 */ 247 public synchronized void disablePlatformNotifications() { 248 if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); 249 250 if (mProxyChangeReceiver != null) { 251 mContext.unregisterReceiver(mProxyChangeReceiver); 252 mProxyChangeReceiver = null; 253 } 254 } 255 256 /** 257 * Because our IntentReceiver can run within a different thread, 258 * synchronize setting the proxy 259 */ 260 private synchronized void setProxyConfig() { 261 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); 262 if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) { 263 mProxyHost = null; 264 } else { 265 String host = Proxy.getHost(mContext); 266 if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host); 267 if (host == null) { 268 mProxyHost = null; 269 } else { 270 mActivePool.disablePersistence(); 271 mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http"); 272 } 273 } 274 } 275 276 /** 277 * used by webkit 278 * @return proxy host if set, null otherwise 279 */ 280 public HttpHost getProxyHost() { 281 return mProxyHost; 282 } 283 284 /** 285 * Queues an HTTP request 286 * @param url The url to load. 287 * @param method "GET" or "POST." 288 * @param headers A hashmap of http headers. 289 * @param eventHandler The event handler for handling returned 290 * data. Callbacks will be made on the supplied instance. 291 * @param bodyProvider InputStream providing HTTP body, null if none 292 * @param bodyLength length of body, must be 0 if bodyProvider is null 293 */ 294 public RequestHandle queueRequest( 295 String url, String method, 296 Map<String, String> headers, EventHandler eventHandler, 297 InputStream bodyProvider, int bodyLength) { 298 WebAddress uri = new WebAddress(url); 299 return queueRequest(url, uri, method, headers, eventHandler, 300 bodyProvider, bodyLength); 301 } 302 303 /** 304 * Queues an HTTP request 305 * @param url The url to load. 306 * @param uri The uri of the url to load. 307 * @param method "GET" or "POST." 308 * @param headers A hashmap of http headers. 309 * @param eventHandler The event handler for handling returned 310 * data. Callbacks will be made on the supplied instance. 311 * @param bodyProvider InputStream providing HTTP body, null if none 312 * @param bodyLength length of body, must be 0 if bodyProvider is null 313 */ 314 public RequestHandle queueRequest( 315 String url, WebAddress uri, String method, Map<String, String> headers, 316 EventHandler eventHandler, 317 InputStream bodyProvider, int bodyLength) { 318 319 if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); 320 321 // Ensure there is an eventHandler set 322 if (eventHandler == null) { 323 eventHandler = new LoggingEventHandler(); 324 } 325 326 /* Create and queue request */ 327 Request req; 328 HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); 329 330 // set up request 331 req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider, 332 bodyLength, eventHandler, headers); 333 334 queueRequest(req, false); 335 336 mActivePool.mTotalRequest++; 337 338 // dump(); 339 mActivePool.startConnectionThread(); 340 341 return new RequestHandle( 342 this, url, uri, method, headers, bodyProvider, bodyLength, 343 req); 344 } 345 346 private static class SyncFeeder implements RequestFeeder { 347 // This is used in the case where the request fails and needs to be 348 // requeued into the RequestFeeder. 349 private Request mRequest; 350 SyncFeeder() { 351 } 352 public Request getRequest() { 353 Request r = mRequest; 354 mRequest = null; 355 return r; 356 } 357 public Request getRequest(HttpHost host) { 358 return getRequest(); 359 } 360 public boolean haveRequest(HttpHost host) { 361 return mRequest != null; 362 } 363 public void requeueRequest(Request r) { 364 mRequest = r; 365 } 366 } 367 368 public RequestHandle queueSynchronousRequest(String url, WebAddress uri, 369 String method, Map<String, String> headers, 370 EventHandler eventHandler, InputStream bodyProvider, 371 int bodyLength) { 372 if (HttpLog.LOGV) { 373 HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri); 374 } 375 376 HttpHost host = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); 377 378 Request req = new Request(method, host, mProxyHost, uri.mPath, 379 bodyProvider, bodyLength, eventHandler, headers); 380 381 // Open a new connection that uses our special RequestFeeder 382 // implementation. 383 host = determineHost(host); 384 Connection conn = Connection.getConnection(mContext, host, mProxyHost, 385 new SyncFeeder()); 386 387 // TODO: I would like to process the request here but LoadListener 388 // needs a RequestHandle to process some messages. 389 return new RequestHandle(this, url, uri, method, headers, bodyProvider, 390 bodyLength, req, conn); 391 392 } 393 394 // Chooses between the proxy and the request's host. 395 private HttpHost determineHost(HttpHost host) { 396 // There used to be a comment in ConnectionThread about t-mob's proxy 397 // being really bad about https. But, HttpsConnection actually looks 398 // for a proxy and connects through it anyway. I think that this check 399 // is still valid because if a site is https, we will use 400 // HttpsConnection rather than HttpConnection if the proxy address is 401 // not secure. 402 return (mProxyHost == null || "https".equals(host.getSchemeName())) 403 ? host : mProxyHost; 404 } 405 406 /** 407 * @return true iff there are any non-active requests pending 408 */ 409 synchronized boolean requestsPending() { 410 return !mPending.isEmpty(); 411 } 412 413 414 /** 415 * debug tool: prints request queue to log 416 */ 417 synchronized void dump() { 418 HttpLog.v("dump()"); 419 StringBuilder dump = new StringBuilder(); 420 int count = 0; 421 Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter; 422 423 // mActivePool.log(dump); 424 425 if (!mPending.isEmpty()) { 426 iter = mPending.entrySet().iterator(); 427 while (iter.hasNext()) { 428 Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); 429 String hostName = entry.getKey().getHostName(); 430 StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); 431 432 LinkedList<Request> reqList = entry.getValue(); 433 ListIterator reqIter = reqList.listIterator(0); 434 while (iter.hasNext()) { 435 Request request = (Request)iter.next(); 436 line.append(request + " "); 437 } 438 dump.append(line); 439 dump.append("\n"); 440 } 441 } 442 HttpLog.v(dump.toString()); 443 } 444 445 /* 446 * RequestFeeder implementation 447 */ 448 public synchronized Request getRequest() { 449 Request ret = null; 450 451 if (!mPending.isEmpty()) { 452 ret = removeFirst(mPending); 453 } 454 if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); 455 return ret; 456 } 457 458 /** 459 * @return a request for given host if possible 460 */ 461 public synchronized Request getRequest(HttpHost host) { 462 Request ret = null; 463 464 if (mPending.containsKey(host)) { 465 LinkedList<Request> reqList = mPending.get(host); 466 ret = reqList.removeFirst(); 467 if (reqList.isEmpty()) { 468 mPending.remove(host); 469 } 470 } 471 if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret); 472 return ret; 473 } 474 475 /** 476 * @return true if a request for this host is available 477 */ 478 public synchronized boolean haveRequest(HttpHost host) { 479 return mPending.containsKey(host); 480 } 481 482 /** 483 * Put request back on head of queue 484 */ 485 public void requeueRequest(Request request) { 486 queueRequest(request, true); 487 } 488 489 /** 490 * This must be called to cleanly shutdown RequestQueue 491 */ 492 public void shutdown() { 493 mActivePool.shutdown(); 494 } 495 496 protected synchronized void queueRequest(Request request, boolean head) { 497 HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; 498 LinkedList<Request> reqList; 499 if (mPending.containsKey(host)) { 500 reqList = mPending.get(host); 501 } else { 502 reqList = new LinkedList<Request>(); 503 mPending.put(host, reqList); 504 } 505 if (head) { 506 reqList.addFirst(request); 507 } else { 508 reqList.add(request); 509 } 510 } 511 512 513 public void startTiming() { 514 mActivePool.startTiming(); 515 } 516 517 public void stopTiming() { 518 mActivePool.stopTiming(); 519 } 520 521 /* helper */ 522 private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) { 523 Request ret = null; 524 Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator(); 525 if (iter.hasNext()) { 526 Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); 527 LinkedList<Request> reqList = entry.getValue(); 528 ret = reqList.removeFirst(); 529 if (reqList.isEmpty()) { 530 requestQueue.remove(entry.getKey()); 531 } 532 } 533 return ret; 534 } 535 536 /** 537 * This interface is exposed to each connection 538 */ 539 interface ConnectionManager { 540 HttpHost getProxyHost(); 541 Connection getConnection(Context context, HttpHost host); 542 boolean recycleConnection(Connection connection); 543 } 544} 545