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