RequestQueue.java revision 86806ce11a89260147d7c2efa2c192b711d923db
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.WebAddress;
32import android.os.Handler;
33import android.os.Message;
34import android.os.SystemProperties;
35import android.text.TextUtils;
36import android.util.Log;
37
38import java.io.InputStream;
39import java.util.Iterator;
40import java.util.LinkedHashMap;
41import java.util.LinkedList;
42import java.util.ListIterator;
43import java.util.Map;
44
45import org.apache.http.HttpHost;
46
47/**
48 * {@hide}
49 */
50public 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    }
242
243    /**
244     * If platform notifications have been enabled, call this method
245     * to disable before destroying RequestQueue
246     */
247    public synchronized void disablePlatformNotifications() {
248        if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network");
249
250        if (mProxyChangeReceiver != null) {
251            mContext.unregisterReceiver(mProxyChangeReceiver);
252            mProxyChangeReceiver = null;
253        }
254    }
255
256    /**
257     * Because our IntentReceiver can run within a different thread,
258     * synchronize setting the proxy
259     */
260    private synchronized void setProxyConfig() {
261        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
262        if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) {
263            mProxyHost = null;
264        } else {
265            String host = Proxy.getHost(mContext);
266            if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host);
267            if (host == null) {
268                mProxyHost = null;
269            } else {
270                mActivePool.disablePersistence();
271                mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http");
272            }
273        }
274    }
275
276    /**
277     * used by webkit
278     * @return proxy host if set, null otherwise
279     */
280    public HttpHost getProxyHost() {
281        return mProxyHost;
282    }
283
284    /**
285     * Queues an HTTP request
286     * @param url The url to load.
287     * @param method "GET" or "POST."
288     * @param headers A hashmap of http headers.
289     * @param eventHandler The event handler for handling returned
290     * data.  Callbacks will be made on the supplied instance.
291     * @param bodyProvider InputStream providing HTTP body, null if none
292     * @param bodyLength length of body, must be 0 if bodyProvider is null
293     */
294    public RequestHandle queueRequest(
295            String url, String method,
296            Map<String, String> headers, EventHandler eventHandler,
297            InputStream bodyProvider, int bodyLength) {
298        WebAddress uri = new WebAddress(url);
299        return queueRequest(url, uri, method, headers, eventHandler,
300                            bodyProvider, bodyLength);
301    }
302
303    /**
304     * Queues an HTTP request
305     * @param url The url to load.
306     * @param uri The uri of the url to load.
307     * @param method "GET" or "POST."
308     * @param headers A hashmap of http headers.
309     * @param eventHandler The event handler for handling returned
310     * data.  Callbacks will be made on the supplied instance.
311     * @param bodyProvider InputStream providing HTTP body, null if none
312     * @param bodyLength length of body, must be 0 if bodyProvider is null
313     */
314    public RequestHandle queueRequest(
315            String url, WebAddress uri, String method, Map<String, String> headers,
316            EventHandler eventHandler,
317            InputStream bodyProvider, int bodyLength) {
318
319        if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri);
320
321        // Ensure there is an eventHandler set
322        if (eventHandler == null) {
323            eventHandler = new LoggingEventHandler();
324        }
325
326        /* Create and queue request */
327        Request req;
328        HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme);
329
330        // set up request
331        req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider,
332                          bodyLength, eventHandler, headers);
333
334        queueRequest(req, false);
335
336        mActivePool.mTotalRequest++;
337
338        // dump();
339        mActivePool.startConnectionThread();
340
341        return new RequestHandle(
342                this, url, uri, method, headers, bodyProvider, bodyLength,
343                req);
344    }
345
346    private static class SyncFeeder implements RequestFeeder {
347        // This is used in the case where the request fails and needs to be
348        // requeued into the RequestFeeder.
349        private Request mRequest;
350        SyncFeeder() {
351        }
352        public Request getRequest() {
353            Request r = mRequest;
354            mRequest = null;
355            return r;
356        }
357        public Request getRequest(HttpHost host) {
358            return getRequest();
359        }
360        public boolean haveRequest(HttpHost host) {
361            return mRequest != null;
362        }
363        public void requeueRequest(Request r) {
364            mRequest = r;
365        }
366    }
367
368    public RequestHandle queueSynchronousRequest(String url, WebAddress uri,
369            String method, Map<String, String> headers,
370            EventHandler eventHandler, InputStream bodyProvider,
371            int bodyLength) {
372        if (HttpLog.LOGV) {
373            HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri);
374        }
375
376        HttpHost host = new HttpHost(uri.mHost, uri.mPort, uri.mScheme);
377
378        Request req = new Request(method, host, mProxyHost, uri.mPath,
379                bodyProvider, bodyLength, eventHandler, headers);
380
381        // Open a new connection that uses our special RequestFeeder
382        // implementation.
383        host = determineHost(host);
384        Connection conn = Connection.getConnection(mContext, host, mProxyHost,
385                new SyncFeeder());
386
387        // TODO: I would like to process the request here but LoadListener
388        // needs a RequestHandle to process some messages.
389        return new RequestHandle(this, url, uri, method, headers, bodyProvider,
390                bodyLength, req, conn);
391
392    }
393
394    // Chooses between the proxy and the request's host.
395    private HttpHost determineHost(HttpHost host) {
396        // There used to be a comment in ConnectionThread about t-mob's proxy
397        // being really bad about https. But, HttpsConnection actually looks
398        // for a proxy and connects through it anyway. I think that this check
399        // is still valid because if a site is https, we will use
400        // HttpsConnection rather than HttpConnection if the proxy address is
401        // not secure.
402        return (mProxyHost == null || "https".equals(host.getSchemeName()))
403                ? host : mProxyHost;
404    }
405
406    /**
407     * @return true iff there are any non-active requests pending
408     */
409    synchronized boolean requestsPending() {
410        return !mPending.isEmpty();
411    }
412
413
414    /**
415     * debug tool: prints request queue to log
416     */
417    synchronized void dump() {
418        HttpLog.v("dump()");
419        StringBuilder dump = new StringBuilder();
420        int count = 0;
421        Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter;
422
423        // mActivePool.log(dump);
424
425        if (!mPending.isEmpty()) {
426            iter = mPending.entrySet().iterator();
427            while (iter.hasNext()) {
428                Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
429                String hostName = entry.getKey().getHostName();
430                StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " ");
431
432                LinkedList<Request> reqList = entry.getValue();
433                ListIterator reqIter = reqList.listIterator(0);
434                while (iter.hasNext()) {
435                    Request request = (Request)iter.next();
436                    line.append(request + " ");
437                }
438                dump.append(line);
439                dump.append("\n");
440            }
441        }
442        HttpLog.v(dump.toString());
443    }
444
445    /*
446     * RequestFeeder implementation
447     */
448    public synchronized Request getRequest() {
449        Request ret = null;
450
451        if (!mPending.isEmpty()) {
452            ret = removeFirst(mPending);
453        }
454        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret);
455        return ret;
456    }
457
458    /**
459     * @return a request for given host if possible
460     */
461    public synchronized Request getRequest(HttpHost host) {
462        Request ret = null;
463
464        if (mPending.containsKey(host)) {
465            LinkedList<Request> reqList = mPending.get(host);
466            ret = reqList.removeFirst();
467            if (reqList.isEmpty()) {
468                mPending.remove(host);
469            }
470        }
471        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret);
472        return ret;
473    }
474
475    /**
476     * @return true if a request for this host is available
477     */
478    public synchronized boolean haveRequest(HttpHost host) {
479        return mPending.containsKey(host);
480    }
481
482    /**
483     * Put request back on head of queue
484     */
485    public void requeueRequest(Request request) {
486        queueRequest(request, true);
487    }
488
489    /**
490     * This must be called to cleanly shutdown RequestQueue
491     */
492    public void shutdown() {
493        mActivePool.shutdown();
494    }
495
496    protected synchronized void queueRequest(Request request, boolean head) {
497        HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost;
498        LinkedList<Request> reqList;
499        if (mPending.containsKey(host)) {
500            reqList = mPending.get(host);
501        } else {
502            reqList = new LinkedList<Request>();
503            mPending.put(host, reqList);
504        }
505        if (head) {
506            reqList.addFirst(request);
507        } else {
508            reqList.add(request);
509        }
510    }
511
512
513    public void startTiming() {
514        mActivePool.startTiming();
515    }
516
517    public void stopTiming() {
518        mActivePool.stopTiming();
519    }
520
521    /* helper */
522    private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) {
523        Request ret = null;
524        Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator();
525        if (iter.hasNext()) {
526            Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
527            LinkedList<Request> reqList = entry.getValue();
528            ret = reqList.removeFirst();
529            if (reqList.isEmpty()) {
530                requestQueue.remove(entry.getKey());
531            }
532        }
533        return ret;
534    }
535
536    /**
537     * This interface is exposed to each connection
538     */
539    interface ConnectionManager {
540        HttpHost getProxyHost();
541        Connection getConnection(Context context, HttpHost host);
542        boolean recycleConnection(Connection connection);
543    }
544}
545