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 // we need to resample the current proxy setup 242 setProxyConfig(); 243 } 244 245 /** 246 * If platform notifications have been enabled, call this method 247 * to disable before destroying RequestQueue 248 */ 249 public synchronized void disablePlatformNotifications() { 250 if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); 251 252 if (mProxyChangeReceiver != null) { 253 mContext.unregisterReceiver(mProxyChangeReceiver); 254 mProxyChangeReceiver = null; 255 } 256 } 257 258 /** 259 * Because our IntentReceiver can run within a different thread, 260 * synchronize setting the proxy 261 */ 262 private synchronized void setProxyConfig() { 263 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); 264 if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) { 265 mProxyHost = null; 266 } else { 267 String host = Proxy.getHost(mContext); 268 if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host); 269 if (host == null) { 270 mProxyHost = null; 271 } else { 272 mActivePool.disablePersistence(); 273 mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http"); 274 } 275 } 276 } 277 278 /** 279 * used by webkit 280 * @return proxy host if set, null otherwise 281 */ 282 public HttpHost getProxyHost() { 283 return mProxyHost; 284 } 285 286 /** 287 * Queues an HTTP request 288 * @param url The url to load. 289 * @param method "GET" or "POST." 290 * @param headers A hashmap of http headers. 291 * @param eventHandler The event handler for handling returned 292 * data. Callbacks will be made on the supplied instance. 293 * @param bodyProvider InputStream providing HTTP body, null if none 294 * @param bodyLength length of body, must be 0 if bodyProvider is null 295 */ 296 public RequestHandle queueRequest( 297 String url, String method, 298 Map<String, String> headers, EventHandler eventHandler, 299 InputStream bodyProvider, int bodyLength) { 300 WebAddress uri = new WebAddress(url); 301 return queueRequest(url, uri, method, headers, eventHandler, 302 bodyProvider, bodyLength); 303 } 304 305 /** 306 * Queues an HTTP request 307 * @param url The url to load. 308 * @param uri The uri of the url to load. 309 * @param method "GET" or "POST." 310 * @param headers A hashmap of http headers. 311 * @param eventHandler The event handler for handling returned 312 * data. Callbacks will be made on the supplied instance. 313 * @param bodyProvider InputStream providing HTTP body, null if none 314 * @param bodyLength length of body, must be 0 if bodyProvider is null 315 */ 316 public RequestHandle queueRequest( 317 String url, WebAddress uri, String method, Map<String, String> headers, 318 EventHandler eventHandler, 319 InputStream bodyProvider, int bodyLength) { 320 321 if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); 322 323 // Ensure there is an eventHandler set 324 if (eventHandler == null) { 325 eventHandler = new LoggingEventHandler(); 326 } 327 328 /* Create and queue request */ 329 Request req; 330 HttpHost httpHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); 331 332 // set up request 333 req = new Request(method, httpHost, mProxyHost, uri.getPath(), bodyProvider, 334 bodyLength, eventHandler, headers); 335 336 queueRequest(req, false); 337 338 mActivePool.mTotalRequest++; 339 340 // dump(); 341 mActivePool.startConnectionThread(); 342 343 return new RequestHandle( 344 this, url, uri, method, headers, bodyProvider, bodyLength, 345 req); 346 } 347 348 private static class SyncFeeder implements RequestFeeder { 349 // This is used in the case where the request fails and needs to be 350 // requeued into the RequestFeeder. 351 private Request mRequest; 352 SyncFeeder() { 353 } 354 public Request getRequest() { 355 Request r = mRequest; 356 mRequest = null; 357 return r; 358 } 359 public Request getRequest(HttpHost host) { 360 return getRequest(); 361 } 362 public boolean haveRequest(HttpHost host) { 363 return mRequest != null; 364 } 365 public void requeueRequest(Request r) { 366 mRequest = r; 367 } 368 } 369 370 public RequestHandle queueSynchronousRequest(String url, WebAddress uri, 371 String method, Map<String, String> headers, 372 EventHandler eventHandler, InputStream bodyProvider, 373 int bodyLength) { 374 if (HttpLog.LOGV) { 375 HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri); 376 } 377 378 HttpHost host = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); 379 380 Request req = new Request(method, host, mProxyHost, uri.getPath(), 381 bodyProvider, bodyLength, eventHandler, headers); 382 383 // Open a new connection that uses our special RequestFeeder 384 // implementation. 385 host = determineHost(host); 386 Connection conn = Connection.getConnection(mContext, host, mProxyHost, 387 new SyncFeeder()); 388 389 // TODO: I would like to process the request here but LoadListener 390 // needs a RequestHandle to process some messages. 391 return new RequestHandle(this, url, uri, method, headers, bodyProvider, 392 bodyLength, req, conn); 393 394 } 395 396 // Chooses between the proxy and the request's host. 397 private HttpHost determineHost(HttpHost host) { 398 // There used to be a comment in ConnectionThread about t-mob's proxy 399 // being really bad about https. But, HttpsConnection actually looks 400 // for a proxy and connects through it anyway. I think that this check 401 // is still valid because if a site is https, we will use 402 // HttpsConnection rather than HttpConnection if the proxy address is 403 // not secure. 404 return (mProxyHost == null || "https".equals(host.getSchemeName())) 405 ? host : mProxyHost; 406 } 407 408 /** 409 * @return true iff there are any non-active requests pending 410 */ 411 synchronized boolean requestsPending() { 412 return !mPending.isEmpty(); 413 } 414 415 416 /** 417 * debug tool: prints request queue to log 418 */ 419 synchronized void dump() { 420 HttpLog.v("dump()"); 421 StringBuilder dump = new StringBuilder(); 422 int count = 0; 423 Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter; 424 425 // mActivePool.log(dump); 426 427 if (!mPending.isEmpty()) { 428 iter = mPending.entrySet().iterator(); 429 while (iter.hasNext()) { 430 Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); 431 String hostName = entry.getKey().getHostName(); 432 StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); 433 434 LinkedList<Request> reqList = entry.getValue(); 435 ListIterator reqIter = reqList.listIterator(0); 436 while (iter.hasNext()) { 437 Request request = (Request)iter.next(); 438 line.append(request + " "); 439 } 440 dump.append(line); 441 dump.append("\n"); 442 } 443 } 444 HttpLog.v(dump.toString()); 445 } 446 447 /* 448 * RequestFeeder implementation 449 */ 450 public synchronized Request getRequest() { 451 Request ret = null; 452 453 if (!mPending.isEmpty()) { 454 ret = removeFirst(mPending); 455 } 456 if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); 457 return ret; 458 } 459 460 /** 461 * @return a request for given host if possible 462 */ 463 public synchronized Request getRequest(HttpHost host) { 464 Request ret = null; 465 466 if (mPending.containsKey(host)) { 467 LinkedList<Request> reqList = mPending.get(host); 468 ret = reqList.removeFirst(); 469 if (reqList.isEmpty()) { 470 mPending.remove(host); 471 } 472 } 473 if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret); 474 return ret; 475 } 476 477 /** 478 * @return true if a request for this host is available 479 */ 480 public synchronized boolean haveRequest(HttpHost host) { 481 return mPending.containsKey(host); 482 } 483 484 /** 485 * Put request back on head of queue 486 */ 487 public void requeueRequest(Request request) { 488 queueRequest(request, true); 489 } 490 491 /** 492 * This must be called to cleanly shutdown RequestQueue 493 */ 494 public void shutdown() { 495 mActivePool.shutdown(); 496 } 497 498 protected synchronized void queueRequest(Request request, boolean head) { 499 HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; 500 LinkedList<Request> reqList; 501 if (mPending.containsKey(host)) { 502 reqList = mPending.get(host); 503 } else { 504 reqList = new LinkedList<Request>(); 505 mPending.put(host, reqList); 506 } 507 if (head) { 508 reqList.addFirst(request); 509 } else { 510 reqList.add(request); 511 } 512 } 513 514 515 public void startTiming() { 516 mActivePool.startTiming(); 517 } 518 519 public void stopTiming() { 520 mActivePool.stopTiming(); 521 } 522 523 /* helper */ 524 private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) { 525 Request ret = null; 526 Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator(); 527 if (iter.hasNext()) { 528 Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); 529 LinkedList<Request> reqList = entry.getValue(); 530 ret = reqList.removeFirst(); 531 if (reqList.isEmpty()) { 532 requestQueue.remove(entry.getKey()); 533 } 534 } 535 return ret; 536 } 537 538 /** 539 * This interface is exposed to each connection 540 */ 541 interface ConnectionManager { 542 HttpHost getProxyHost(); 543 Connection getConnection(Context context, HttpHost host); 544 boolean recycleConnection(Connection connection); 545 } 546} 547