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