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