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