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