Home | History | Annotate | Download | only in http
      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.os.Handler;
     33 import android.os.Message;
     34 import android.os.SystemProperties;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 
     38 import java.io.InputStream;
     39 import java.util.Iterator;
     40 import java.util.LinkedHashMap;
     41 import java.util.LinkedList;
     42 import java.util.ListIterator;
     43 import java.util.Map;
     44 
     45 import org.apache.http.HttpHost;
     46 
     47 /**
     48  * {@hide}
     49  */
     50 public 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