RequestQueue.java revision b5b33c734ab2a178f07c667a8c1daf8beab837aa
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.NetworkConnectivityListener; 30import android.net.NetworkInfo; 31import android.net.Proxy; 32import android.net.WebAddress; 33import android.os.Handler; 34import android.os.Message; 35import android.os.SystemProperties; 36import android.text.TextUtils; 37import android.util.Log; 38 39import java.io.InputStream; 40import java.util.Iterator; 41import java.util.LinkedHashMap; 42import java.util.LinkedList; 43import java.util.ListIterator; 44import java.util.Map; 45 46import org.apache.http.HttpHost; 47 48/** 49 * {@hide} 50 */ 51public class RequestQueue implements RequestFeeder { 52 53 private Context mContext; 54 55 private static class RequestSet { 56 private final LinkedList<Request> mHighPriority; 57 private final LinkedList<Request> mLowPriority; 58 59 RequestSet() { 60 mHighPriority = new LinkedList<Request>(); 61 mLowPriority = new LinkedList<Request>(); 62 } 63 64 void add(Request req, boolean head) { 65 LinkedList l = mLowPriority; 66 if (req.mHighPriority) { 67 l = mHighPriority; 68 } 69 if (head) { 70 l.addFirst(req); 71 } else { 72 l.add(req); 73 } 74 } 75 76 Request removeFirst() { 77 if (!mHighPriority.isEmpty()) { 78 return mHighPriority.removeFirst(); 79 } else if (!mLowPriority.isEmpty()) { 80 return mLowPriority.removeFirst(); 81 } 82 return null; 83 } 84 85 boolean isEmpty() { 86 return mHighPriority.isEmpty() && mLowPriority.isEmpty(); 87 } 88 }; 89 /** 90 * Requests, indexed by HttpHost (scheme, host, port) 91 */ 92 private LinkedHashMap<HttpHost, RequestSet> mPending; 93 94 /* Support for notifying a client when queue is empty */ 95 private boolean mClientWaiting = false; 96 97 /** true if connected */ 98 boolean mNetworkConnected = true; 99 100 private HttpHost mProxyHost = null; 101 private BroadcastReceiver mProxyChangeReceiver; 102 103 private ActivePool mActivePool; 104 105 /* default simultaneous connection count */ 106 private static final int CONNECTION_COUNT = 4; 107 108 /** 109 * This intent broadcast when http is paused or unpaused due to 110 * net availability toggling 111 */ 112 public final static String HTTP_NETWORK_STATE_CHANGED_INTENT = 113 "android.net.http.NETWORK_STATE"; 114 public final static String HTTP_NETWORK_STATE_UP = "up"; 115 116 /** 117 * Listen to platform network state. On a change, 118 * (1) kick stack on or off as appropriate 119 * (2) send an intent to my host app telling 120 * it what I've done 121 */ 122 private NetworkStateTracker mNetworkStateTracker; 123 class NetworkStateTracker { 124 125 final static int EVENT_DATA_STATE_CHANGED = 100; 126 127 Context mContext; 128 NetworkConnectivityListener mConnectivityListener; 129 NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED; 130 int mCurrentNetworkType; 131 132 NetworkStateTracker(Context context) { 133 mContext = context; 134 } 135 136 /** 137 * register for updates 138 */ 139 protected void enable() { 140 if (mConnectivityListener == null) { 141 /* 142 * Initializing the network type is really unnecessary, 143 * since as soon as we register with the NCL, we'll 144 * get a CONNECTED event for the active network, and 145 * we'll configure the HTTP proxy accordingly. However, 146 * as a fallback in case that doesn't happen for some 147 * reason, initializing to type WIFI would mean that 148 * we'd start out without a proxy. This seems better 149 * than thinking we have a proxy (which is probably 150 * private to the carrier network and therefore 151 * unreachable outside of that network) when we really 152 * shouldn't. 153 */ 154 mCurrentNetworkType = ConnectivityManager.TYPE_WIFI; 155 mConnectivityListener = new NetworkConnectivityListener(); 156 mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED); 157 mConnectivityListener.startListening(mContext); 158 } 159 } 160 161 protected void disable() { 162 if (mConnectivityListener != null) { 163 mConnectivityListener.unregisterHandler(mHandler); 164 mConnectivityListener.stopListening(); 165 mConnectivityListener = null; 166 } 167 } 168 169 private Handler mHandler = new Handler() { 170 public void handleMessage(Message msg) { 171 switch (msg.what) { 172 case EVENT_DATA_STATE_CHANGED: 173 networkStateChanged(); 174 break; 175 } 176 } 177 }; 178 179 int getCurrentNetworkType() { 180 return mCurrentNetworkType; 181 } 182 183 void networkStateChanged() { 184 if (mConnectivityListener == null) 185 return; 186 187 188 NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState(); 189 NetworkInfo info = mConnectivityListener.getNetworkInfo(); 190 if (info == null) { 191 /** 192 * We've been seeing occasional NPEs here. I believe recent changes 193 * have made this impossible, but in the interest of being totally 194 * paranoid, check and log this here. 195 */ 196 HttpLog.v("NetworkStateTracker: connectivity broadcast" 197 + " has null network info - ignoring"); 198 return; 199 } 200 NetworkInfo.State state = info.getState(); 201 202 if (HttpLog.LOGV) { 203 HttpLog.v("NetworkStateTracker " + info.getTypeName() + 204 " state= " + state + " last= " + mLastNetworkState + 205 " connectivityState= " + connectivityState.toString()); 206 } 207 208 boolean newConnection = 209 state != mLastNetworkState && state == NetworkInfo.State.CONNECTED; 210 211 if (state == NetworkInfo.State.CONNECTED) { 212 mCurrentNetworkType = info.getType(); 213 setProxyConfig(); 214 } 215 216 mLastNetworkState = state; 217 if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) { 218 setNetworkState(false); 219 broadcastState(false); 220 } else if (newConnection) { 221 setNetworkState(true); 222 broadcastState(true); 223 } 224 225 } 226 227 void broadcastState(boolean connected) { 228 Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT); 229 intent.putExtra(HTTP_NETWORK_STATE_UP, connected); 230 mContext.sendBroadcast(intent); 231 } 232 } 233 234 /** 235 * This class maintains active connection threads 236 */ 237 class ActivePool implements ConnectionManager { 238 /** Threads used to process requests */ 239 ConnectionThread[] mThreads; 240 241 IdleCache mIdleCache; 242 243 private int mTotalRequest; 244 private int mTotalConnection; 245 private int mConnectionCount; 246 247 ActivePool(int connectionCount) { 248 mIdleCache = new IdleCache(); 249 mConnectionCount = connectionCount; 250 mThreads = new ConnectionThread[mConnectionCount]; 251 252 for (int i = 0; i < mConnectionCount; i++) { 253 mThreads[i] = new ConnectionThread( 254 mContext, i, this, RequestQueue.this); 255 } 256 } 257 258 void startup() { 259 for (int i = 0; i < mConnectionCount; i++) { 260 mThreads[i].start(); 261 } 262 } 263 264 void shutdown() { 265 for (int i = 0; i < mConnectionCount; i++) { 266 mThreads[i].requestStop(); 267 } 268 } 269 270 public boolean isNetworkConnected() { 271 return mNetworkConnected; 272 } 273 274 void startConnectionThread() { 275 synchronized (RequestQueue.this) { 276 RequestQueue.this.notify(); 277 } 278 } 279 280 public void startTiming() { 281 for (int i = 0; i < mConnectionCount; i++) { 282 mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime; 283 } 284 mTotalRequest = 0; 285 mTotalConnection = 0; 286 } 287 288 public void stopTiming() { 289 int totalTime = 0; 290 for (int i = 0; i < mConnectionCount; i++) { 291 ConnectionThread rt = mThreads[i]; 292 totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime); 293 rt.mStartThreadTime = -1; 294 } 295 Log.d("Http", "Http thread used " + totalTime + " ms " + " for " 296 + mTotalRequest + " requests and " + mTotalConnection 297 + " connections"); 298 } 299 300 void logState() { 301 StringBuilder dump = new StringBuilder(); 302 for (int i = 0; i < mConnectionCount; i++) { 303 dump.append(mThreads[i] + "\n"); 304 } 305 HttpLog.v(dump.toString()); 306 } 307 308 309 public HttpHost getProxyHost() { 310 return mProxyHost; 311 } 312 313 /** 314 * Turns off persistence on all live connections 315 */ 316 void disablePersistence() { 317 for (int i = 0; i < mConnectionCount; i++) { 318 Connection connection = mThreads[i].mConnection; 319 if (connection != null) connection.setCanPersist(false); 320 } 321 mIdleCache.clear(); 322 } 323 324 /* Linear lookup -- okay for small thread counts. Might use 325 private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap; 326 if this turns out to be a hotspot */ 327 ConnectionThread getThread(HttpHost host) { 328 synchronized(RequestQueue.this) { 329 for (int i = 0; i < mThreads.length; i++) { 330 ConnectionThread ct = mThreads[i]; 331 Connection connection = ct.mConnection; 332 if (connection != null && connection.mHost.equals(host)) { 333 return ct; 334 } 335 } 336 } 337 return null; 338 } 339 340 public Connection getConnection(Context context, HttpHost host) { 341 Connection con = mIdleCache.getConnection(host); 342 if (con == null) { 343 mTotalConnection++; 344 con = Connection.getConnection( 345 mContext, host, this, RequestQueue.this); 346 } 347 return con; 348 } 349 public boolean recycleConnection(HttpHost host, Connection connection) { 350 return mIdleCache.cacheConnection(host, connection); 351 } 352 353 } 354 355 /** 356 * A RequestQueue class instance maintains a set of queued 357 * requests. It orders them, makes the requests against HTTP 358 * servers, and makes callbacks to supplied eventHandlers as data 359 * is read. It supports request prioritization, connection reuse 360 * and pipelining. 361 * 362 * @param context application context 363 */ 364 public RequestQueue(Context context) { 365 this(context, CONNECTION_COUNT); 366 } 367 368 /** 369 * A RequestQueue class instance maintains a set of queued 370 * requests. It orders them, makes the requests against HTTP 371 * servers, and makes callbacks to supplied eventHandlers as data 372 * is read. It supports request prioritization, connection reuse 373 * and pipelining. 374 * 375 * @param context application context 376 * @param connectionCount The number of simultaneous connections 377 */ 378 public RequestQueue(Context context, int connectionCount) { 379 mContext = context; 380 381 mPending = new LinkedHashMap<HttpHost, RequestSet>(32); 382 383 mActivePool = new ActivePool(connectionCount); 384 mActivePool.startup(); 385 } 386 387 /** 388 * Enables data state and proxy tracking 389 */ 390 public synchronized void enablePlatformNotifications() { 391 if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network"); 392 393 if (mProxyChangeReceiver == null) { 394 mProxyChangeReceiver = 395 new BroadcastReceiver() { 396 @Override 397 public void onReceive(Context ctx, Intent intent) { 398 setProxyConfig(); 399 } 400 }; 401 mContext.registerReceiver(mProxyChangeReceiver, 402 new IntentFilter(Proxy.PROXY_CHANGE_ACTION)); 403 } 404 405 /* Network state notification is broken on the simulator 406 don't register for notifications on SIM */ 407 String device = SystemProperties.get("ro.product.device"); 408 boolean simulation = TextUtils.isEmpty(device); 409 410 if (!simulation) { 411 if (mNetworkStateTracker == null) { 412 mNetworkStateTracker = new NetworkStateTracker(mContext); 413 } 414 mNetworkStateTracker.enable(); 415 } 416 } 417 418 /** 419 * If platform notifications have been enabled, call this method 420 * to disable before destroying RequestQueue 421 */ 422 public synchronized void disablePlatformNotifications() { 423 if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); 424 425 if (mNetworkStateTracker != null) { 426 mNetworkStateTracker.disable(); 427 } 428 429 if (mProxyChangeReceiver != null) { 430 mContext.unregisterReceiver(mProxyChangeReceiver); 431 mProxyChangeReceiver = null; 432 } 433 } 434 435 /** 436 * Because our IntentReceiver can run within a different thread, 437 * synchronize setting the proxy 438 */ 439 private synchronized void setProxyConfig() { 440 if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) { 441 mProxyHost = null; 442 } else { 443 String host = Proxy.getHost(mContext); 444 if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host); 445 if (host == null) { 446 mProxyHost = null; 447 } else { 448 mActivePool.disablePersistence(); 449 mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http"); 450 } 451 } 452 } 453 454 /** 455 * used by webkit 456 * @return proxy host if set, null otherwise 457 */ 458 public HttpHost getProxyHost() { 459 return mProxyHost; 460 } 461 462 /** 463 * Queues an HTTP request 464 * @param url The url to load. 465 * @param method "GET" or "POST." 466 * @param headers A hashmap of http headers. 467 * @param eventHandler The event handler for handling returned 468 * data. Callbacks will be made on the supplied instance. 469 * @param bodyProvider InputStream providing HTTP body, null if none 470 * @param bodyLength length of body, must be 0 if bodyProvider is null 471 * @param highPriority If true, queues before low priority 472 * requests if possible 473 */ 474 public RequestHandle queueRequest( 475 String url, String method, 476 Map<String, String> headers, EventHandler eventHandler, 477 InputStream bodyProvider, int bodyLength, boolean highPriority) { 478 WebAddress uri = new WebAddress(url); 479 return queueRequest(url, uri, method, headers, eventHandler, 480 bodyProvider, bodyLength, highPriority); 481 } 482 483 /** 484 * Queues an HTTP request 485 * @param url The url to load. 486 * @param uri The uri of the url to load. 487 * @param method "GET" or "POST." 488 * @param headers A hashmap of http headers. 489 * @param eventHandler The event handler for handling returned 490 * data. Callbacks will be made on the supplied instance. 491 * @param bodyProvider InputStream providing HTTP body, null if none 492 * @param bodyLength length of body, must be 0 if bodyProvider is null 493 * @param highPriority If true, queues before low priority 494 * requests if possible 495 */ 496 public RequestHandle queueRequest( 497 String url, WebAddress uri, String method, Map<String, String> headers, 498 EventHandler eventHandler, 499 InputStream bodyProvider, int bodyLength, 500 boolean highPriority) { 501 502 if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); 503 504 // Ensure there is an eventHandler set 505 if (eventHandler == null) { 506 eventHandler = new LoggingEventHandler(); 507 } 508 509 /* Create and queue request */ 510 Request req; 511 HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); 512 513 // set up request 514 req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider, 515 bodyLength, eventHandler, headers, highPriority); 516 517 queueRequest(req, false); 518 519 mActivePool.mTotalRequest++; 520 521 // dump(); 522 mActivePool.startConnectionThread(); 523 524 return new RequestHandle( 525 this, url, uri, method, headers, bodyProvider, bodyLength, 526 req); 527 } 528 529 /** 530 * Called by the NetworkStateTracker -- updates when network connectivity 531 * is lost/restored. 532 * 533 * If isNetworkConnected is true, start processing requests 534 */ 535 public void setNetworkState(boolean isNetworkConnected) { 536 if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected); 537 mNetworkConnected = isNetworkConnected; 538 if (isNetworkConnected) 539 mActivePool.startConnectionThread(); 540 } 541 542 /** 543 * @return true iff there are any non-active requests pending 544 */ 545 synchronized boolean requestsPending() { 546 return !mPending.isEmpty(); 547 } 548 549 550 /** 551 * debug tool: prints request queue to log 552 */ 553 synchronized void dump() { 554 HttpLog.v("dump()"); 555 StringBuilder dump = new StringBuilder(); 556 int count = 0; 557 Iterator<Map.Entry<HttpHost, RequestSet>> iter; 558 559 // mActivePool.log(dump); 560 561 if (!mPending.isEmpty()) { 562 iter = mPending.entrySet().iterator(); 563 while (iter.hasNext()) { 564 Map.Entry<HttpHost, RequestSet> entry = iter.next(); 565 String hostName = entry.getKey().getHostName(); 566 StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); 567 568 RequestSet reqList = entry.getValue(); 569 ListIterator reqIter = reqList.mHighPriority.listIterator(0); 570 while (iter.hasNext()) { 571 Request request = (Request)iter.next(); 572 line.append(request + " "); 573 } 574 reqIter = reqList.mLowPriority.listIterator(0); 575 while (iter.hasNext()) { 576 Request request = (Request)iter.next(); 577 line.append(request + " "); 578 } 579 dump.append(line); 580 dump.append("\n"); 581 } 582 } 583 HttpLog.v(dump.toString()); 584 } 585 586 /* 587 * RequestFeeder implementation 588 */ 589 public synchronized Request getRequest() { 590 Request ret = null; 591 592 if (mNetworkConnected && !mPending.isEmpty()) { 593 ret = removeFirst(mPending); 594 } 595 if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); 596 return ret; 597 } 598 599 /** 600 * @return a request for given host if possible 601 */ 602 public synchronized Request getRequest(HttpHost host) { 603 Request ret = null; 604 605 if (mNetworkConnected && mPending.containsKey(host)) { 606 RequestSet reqList = mPending.get(host); 607 ret = reqList.removeFirst(); 608 if (reqList.isEmpty()) { 609 mPending.remove(host); 610 } 611 } 612 if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret); 613 return ret; 614 } 615 616 /** 617 * @return true if a request for this host is available 618 */ 619 public synchronized boolean haveRequest(HttpHost host) { 620 return mPending.containsKey(host); 621 } 622 623 /** 624 * Put request back on head of queue 625 */ 626 public void requeueRequest(Request request) { 627 queueRequest(request, true); 628 } 629 630 /** 631 * This must be called to cleanly shutdown RequestQueue 632 */ 633 public void shutdown() { 634 mActivePool.shutdown(); 635 } 636 637 protected synchronized void queueRequest(Request request, boolean head) { 638 HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; 639 RequestSet reqList; 640 if (mPending.containsKey(host)) { 641 reqList = mPending.get(host); 642 } else { 643 reqList = new RequestSet(); 644 mPending.put(host, reqList); 645 } 646 reqList.add(request, head); 647 } 648 649 650 public void startTiming() { 651 mActivePool.startTiming(); 652 } 653 654 public void stopTiming() { 655 mActivePool.stopTiming(); 656 } 657 658 /* helper */ 659 private Request removeFirst(LinkedHashMap<HttpHost, RequestSet> requestQueue) { 660 Request ret = null; 661 Iterator<Map.Entry<HttpHost, RequestSet>> iter = requestQueue.entrySet().iterator(); 662 if (iter.hasNext()) { 663 Map.Entry<HttpHost, RequestSet> entry = iter.next(); 664 RequestSet reqList = entry.getValue(); 665 ret = reqList.removeFirst(); 666 if (reqList.isEmpty()) { 667 requestQueue.remove(entry.getKey()); 668 } 669 } 670 return ret; 671 } 672 673 /** 674 * This interface is exposed to each connection 675 */ 676 interface ConnectionManager { 677 boolean isNetworkConnected(); 678 HttpHost getProxyHost(); 679 Connection getConnection(Context context, HttpHost host); 680 boolean recycleConnection(HttpHost host, Connection connection); 681 } 682} 683