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.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