RequestQueue.java revision b5b33c734ab2a178f07c667a8c1daf8beab837aa
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.NetworkConnectivityListener;
30import android.net.NetworkInfo;
31import android.net.Proxy;
32import android.net.WebAddress;
33import android.os.Handler;
34import android.os.Message;
35import android.os.SystemProperties;
36import android.text.TextUtils;
37import android.util.Log;
38
39import java.io.InputStream;
40import java.util.Iterator;
41import java.util.LinkedHashMap;
42import java.util.LinkedList;
43import java.util.ListIterator;
44import java.util.Map;
45
46import org.apache.http.HttpHost;
47
48/**
49 * {@hide}
50 */
51public class RequestQueue implements RequestFeeder {
52
53    private Context mContext;
54
55    private static class RequestSet {
56        private final LinkedList<Request> mHighPriority;
57        private final LinkedList<Request> mLowPriority;
58
59        RequestSet() {
60            mHighPriority = new LinkedList<Request>();
61            mLowPriority = new LinkedList<Request>();
62        }
63
64        void add(Request req, boolean head) {
65            LinkedList l = mLowPriority;
66            if (req.mHighPriority) {
67                l = mHighPriority;
68            }
69            if (head) {
70                l.addFirst(req);
71            } else {
72                l.add(req);
73            }
74        }
75
76        Request removeFirst() {
77            if (!mHighPriority.isEmpty()) {
78                return mHighPriority.removeFirst();
79            } else if (!mLowPriority.isEmpty()) {
80                return mLowPriority.removeFirst();
81            }
82            return null;
83        }
84
85        boolean isEmpty() {
86            return mHighPriority.isEmpty() && mLowPriority.isEmpty();
87        }
88    };
89    /**
90     * Requests, indexed by HttpHost (scheme, host, port)
91     */
92    private LinkedHashMap<HttpHost, RequestSet> mPending;
93
94    /* Support for notifying a client when queue is empty */
95    private boolean mClientWaiting = false;
96
97    /** true if connected */
98    boolean mNetworkConnected = true;
99
100    private HttpHost mProxyHost = null;
101    private BroadcastReceiver mProxyChangeReceiver;
102
103    private ActivePool mActivePool;
104
105    /* default simultaneous connection count */
106    private static final int CONNECTION_COUNT = 4;
107
108    /**
109     * This intent broadcast when http is paused or unpaused due to
110     * net availability toggling
111     */
112    public final static String HTTP_NETWORK_STATE_CHANGED_INTENT =
113            "android.net.http.NETWORK_STATE";
114    public final static String HTTP_NETWORK_STATE_UP = "up";
115
116    /**
117     * Listen to platform network state.  On a change,
118     * (1) kick stack on or off as appropriate
119     * (2) send an intent to my host app telling
120     *     it what I've done
121     */
122    private NetworkStateTracker mNetworkStateTracker;
123    class NetworkStateTracker {
124
125        final static int EVENT_DATA_STATE_CHANGED = 100;
126
127        Context mContext;
128        NetworkConnectivityListener mConnectivityListener;
129        NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED;
130        int mCurrentNetworkType;
131
132        NetworkStateTracker(Context context) {
133            mContext = context;
134        }
135
136        /**
137         * register for updates
138         */
139        protected void enable() {
140            if (mConnectivityListener == null) {
141                /*
142                 * Initializing the network type is really unnecessary,
143                 * since as soon as we register with the NCL, we'll
144                 * get a CONNECTED event for the active network, and
145                 * we'll configure the HTTP proxy accordingly. However,
146                 * as a fallback in case that doesn't happen for some
147                 * reason, initializing to type WIFI would mean that
148                 * we'd start out without a proxy. This seems better
149                 * than thinking we have a proxy (which is probably
150                 * private to the carrier network and therefore
151                 * unreachable outside of that network) when we really
152                 * shouldn't.
153                 */
154                mCurrentNetworkType = ConnectivityManager.TYPE_WIFI;
155                mConnectivityListener = new NetworkConnectivityListener();
156                mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED);
157                mConnectivityListener.startListening(mContext);
158            }
159        }
160
161        protected void disable() {
162            if (mConnectivityListener != null) {
163                mConnectivityListener.unregisterHandler(mHandler);
164                mConnectivityListener.stopListening();
165                mConnectivityListener = null;
166            }
167        }
168
169        private Handler mHandler = new Handler() {
170            public void handleMessage(Message msg) {
171                switch (msg.what) {
172                    case EVENT_DATA_STATE_CHANGED:
173                        networkStateChanged();
174                        break;
175                }
176            }
177        };
178
179        int getCurrentNetworkType() {
180            return mCurrentNetworkType;
181        }
182
183        void networkStateChanged() {
184            if (mConnectivityListener == null)
185                return;
186
187
188            NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState();
189            NetworkInfo info = mConnectivityListener.getNetworkInfo();
190            if (info == null) {
191                /**
192                 * We've been seeing occasional NPEs here. I believe recent changes
193                 * have made this impossible, but in the interest of being totally
194                 * paranoid, check and log this here.
195                 */
196                HttpLog.v("NetworkStateTracker: connectivity broadcast"
197                    + " has null network info - ignoring");
198                return;
199            }
200            NetworkInfo.State state = info.getState();
201
202            if (HttpLog.LOGV) {
203                HttpLog.v("NetworkStateTracker " + info.getTypeName() +
204                " state= " + state + " last= " + mLastNetworkState +
205                " connectivityState= " + connectivityState.toString());
206            }
207
208            boolean newConnection =
209                state != mLastNetworkState && state == NetworkInfo.State.CONNECTED;
210
211            if (state == NetworkInfo.State.CONNECTED) {
212                mCurrentNetworkType = info.getType();
213                setProxyConfig();
214            }
215
216            mLastNetworkState = state;
217            if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) {
218                setNetworkState(false);
219                broadcastState(false);
220            } else if (newConnection) {
221                setNetworkState(true);
222                broadcastState(true);
223            }
224
225        }
226
227        void broadcastState(boolean connected) {
228            Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT);
229            intent.putExtra(HTTP_NETWORK_STATE_UP, connected);
230            mContext.sendBroadcast(intent);
231        }
232    }
233
234    /**
235     * This class maintains active connection threads
236     */
237    class ActivePool implements ConnectionManager {
238        /** Threads used to process requests */
239        ConnectionThread[] mThreads;
240
241        IdleCache mIdleCache;
242
243        private int mTotalRequest;
244        private int mTotalConnection;
245        private int mConnectionCount;
246
247        ActivePool(int connectionCount) {
248            mIdleCache = new IdleCache();
249            mConnectionCount = connectionCount;
250            mThreads = new ConnectionThread[mConnectionCount];
251
252            for (int i = 0; i < mConnectionCount; i++) {
253                mThreads[i] = new ConnectionThread(
254                        mContext, i, this, RequestQueue.this);
255            }
256        }
257
258        void startup() {
259            for (int i = 0; i < mConnectionCount; i++) {
260                mThreads[i].start();
261            }
262        }
263
264        void shutdown() {
265            for (int i = 0; i < mConnectionCount; i++) {
266                mThreads[i].requestStop();
267            }
268        }
269
270        public boolean isNetworkConnected() {
271            return mNetworkConnected;
272        }
273
274        void startConnectionThread() {
275            synchronized (RequestQueue.this) {
276                RequestQueue.this.notify();
277            }
278        }
279
280        public void startTiming() {
281            for (int i = 0; i < mConnectionCount; i++) {
282                mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime;
283            }
284            mTotalRequest = 0;
285            mTotalConnection = 0;
286        }
287
288        public void stopTiming() {
289            int totalTime = 0;
290            for (int i = 0; i < mConnectionCount; i++) {
291                ConnectionThread rt = mThreads[i];
292                totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime);
293                rt.mStartThreadTime = -1;
294            }
295            Log.d("Http", "Http thread used " + totalTime + " ms " + " for "
296                    + mTotalRequest + " requests and " + mTotalConnection
297                    + " connections");
298        }
299
300        void logState() {
301            StringBuilder dump = new StringBuilder();
302            for (int i = 0; i < mConnectionCount; i++) {
303                dump.append(mThreads[i] + "\n");
304            }
305            HttpLog.v(dump.toString());
306        }
307
308
309        public HttpHost getProxyHost() {
310            return mProxyHost;
311        }
312
313        /**
314         * Turns off persistence on all live connections
315         */
316        void disablePersistence() {
317            for (int i = 0; i < mConnectionCount; i++) {
318                Connection connection = mThreads[i].mConnection;
319                if (connection != null) connection.setCanPersist(false);
320            }
321            mIdleCache.clear();
322        }
323
324        /* Linear lookup -- okay for small thread counts.  Might use
325           private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap;
326           if this turns out to be a hotspot */
327        ConnectionThread getThread(HttpHost host) {
328            synchronized(RequestQueue.this) {
329                for (int i = 0; i < mThreads.length; i++) {
330                    ConnectionThread ct = mThreads[i];
331                    Connection connection = ct.mConnection;
332                    if (connection != null && connection.mHost.equals(host)) {
333                        return ct;
334                    }
335                }
336            }
337            return null;
338        }
339
340        public Connection getConnection(Context context, HttpHost host) {
341            Connection con = mIdleCache.getConnection(host);
342            if (con == null) {
343                mTotalConnection++;
344                con = Connection.getConnection(
345                        mContext, host, this, RequestQueue.this);
346            }
347            return con;
348        }
349        public boolean recycleConnection(HttpHost host, Connection connection) {
350            return mIdleCache.cacheConnection(host, connection);
351        }
352
353    }
354
355    /**
356     * A RequestQueue class instance maintains a set of queued
357     * requests.  It orders them, makes the requests against HTTP
358     * servers, and makes callbacks to supplied eventHandlers as data
359     * is read.  It supports request prioritization, connection reuse
360     * and pipelining.
361     *
362     * @param context application context
363     */
364    public RequestQueue(Context context) {
365        this(context, CONNECTION_COUNT);
366    }
367
368    /**
369     * A RequestQueue class instance maintains a set of queued
370     * requests.  It orders them, makes the requests against HTTP
371     * servers, and makes callbacks to supplied eventHandlers as data
372     * is read.  It supports request prioritization, connection reuse
373     * and pipelining.
374     *
375     * @param context application context
376     * @param connectionCount The number of simultaneous connections
377     */
378    public RequestQueue(Context context, int connectionCount) {
379        mContext = context;
380
381        mPending = new LinkedHashMap<HttpHost, RequestSet>(32);
382
383        mActivePool = new ActivePool(connectionCount);
384        mActivePool.startup();
385    }
386
387    /**
388     * Enables data state and proxy tracking
389     */
390    public synchronized void enablePlatformNotifications() {
391        if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network");
392
393        if (mProxyChangeReceiver == null) {
394            mProxyChangeReceiver =
395                    new BroadcastReceiver() {
396                        @Override
397                        public void onReceive(Context ctx, Intent intent) {
398                            setProxyConfig();
399                        }
400                    };
401            mContext.registerReceiver(mProxyChangeReceiver,
402                                      new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
403        }
404
405        /* Network state notification is broken on the simulator
406           don't register for notifications on SIM */
407        String device = SystemProperties.get("ro.product.device");
408        boolean simulation = TextUtils.isEmpty(device);
409
410        if (!simulation) {
411            if (mNetworkStateTracker == null) {
412                mNetworkStateTracker = new NetworkStateTracker(mContext);
413            }
414            mNetworkStateTracker.enable();
415        }
416    }
417
418    /**
419     * If platform notifications have been enabled, call this method
420     * to disable before destroying RequestQueue
421     */
422    public synchronized void disablePlatformNotifications() {
423        if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network");
424
425        if (mNetworkStateTracker != null) {
426            mNetworkStateTracker.disable();
427        }
428
429        if (mProxyChangeReceiver != null) {
430            mContext.unregisterReceiver(mProxyChangeReceiver);
431            mProxyChangeReceiver = null;
432        }
433    }
434
435    /**
436     * Because our IntentReceiver can run within a different thread,
437     * synchronize setting the proxy
438     */
439    private synchronized void setProxyConfig() {
440        if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) {
441            mProxyHost = null;
442        } else {
443            String host = Proxy.getHost(mContext);
444            if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host);
445            if (host == null) {
446                mProxyHost = null;
447            } else {
448                mActivePool.disablePersistence();
449                mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http");
450            }
451        }
452    }
453
454    /**
455     * used by webkit
456     * @return proxy host if set, null otherwise
457     */
458    public HttpHost getProxyHost() {
459        return mProxyHost;
460    }
461
462    /**
463     * Queues an HTTP request
464     * @param url The url to load.
465     * @param method "GET" or "POST."
466     * @param headers A hashmap of http headers.
467     * @param eventHandler The event handler for handling returned
468     * data.  Callbacks will be made on the supplied instance.
469     * @param bodyProvider InputStream providing HTTP body, null if none
470     * @param bodyLength length of body, must be 0 if bodyProvider is null
471     * @param highPriority If true, queues before low priority
472     *     requests if possible
473     */
474    public RequestHandle queueRequest(
475            String url, String method,
476            Map<String, String> headers, EventHandler eventHandler,
477            InputStream bodyProvider, int bodyLength, boolean highPriority) {
478        WebAddress uri = new WebAddress(url);
479        return queueRequest(url, uri, method, headers, eventHandler,
480                            bodyProvider, bodyLength, highPriority);
481    }
482
483    /**
484     * Queues an HTTP request
485     * @param url The url to load.
486     * @param uri The uri of the url to load.
487     * @param method "GET" or "POST."
488     * @param headers A hashmap of http headers.
489     * @param eventHandler The event handler for handling returned
490     * data.  Callbacks will be made on the supplied instance.
491     * @param bodyProvider InputStream providing HTTP body, null if none
492     * @param bodyLength length of body, must be 0 if bodyProvider is null
493     * @param highPriority If true, queues before low priority
494     *     requests if possible
495     */
496    public RequestHandle queueRequest(
497            String url, WebAddress uri, String method, Map<String, String> headers,
498            EventHandler eventHandler,
499            InputStream bodyProvider, int bodyLength,
500            boolean highPriority) {
501
502        if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri);
503
504        // Ensure there is an eventHandler set
505        if (eventHandler == null) {
506            eventHandler = new LoggingEventHandler();
507        }
508
509        /* Create and queue request */
510        Request req;
511        HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme);
512
513        // set up request
514        req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider,
515                          bodyLength, eventHandler, headers, highPriority);
516
517        queueRequest(req, false);
518
519        mActivePool.mTotalRequest++;
520
521        // dump();
522        mActivePool.startConnectionThread();
523
524        return new RequestHandle(
525                this, url, uri, method, headers, bodyProvider, bodyLength,
526                req);
527    }
528
529    /**
530     * Called by the NetworkStateTracker -- updates when network connectivity
531     * is lost/restored.
532     *
533     * If isNetworkConnected is true, start processing requests
534     */
535    public void setNetworkState(boolean isNetworkConnected) {
536        if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected);
537        mNetworkConnected = isNetworkConnected;
538        if (isNetworkConnected)
539            mActivePool.startConnectionThread();
540    }
541
542    /**
543     * @return true iff there are any non-active requests pending
544     */
545    synchronized boolean requestsPending() {
546        return !mPending.isEmpty();
547    }
548
549
550    /**
551     * debug tool: prints request queue to log
552     */
553    synchronized void dump() {
554        HttpLog.v("dump()");
555        StringBuilder dump = new StringBuilder();
556        int count = 0;
557        Iterator<Map.Entry<HttpHost, RequestSet>> iter;
558
559        // mActivePool.log(dump);
560
561        if (!mPending.isEmpty()) {
562            iter = mPending.entrySet().iterator();
563            while (iter.hasNext()) {
564                Map.Entry<HttpHost, RequestSet> entry = iter.next();
565                String hostName = entry.getKey().getHostName();
566                StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " ");
567
568                RequestSet reqList = entry.getValue();
569                ListIterator reqIter = reqList.mHighPriority.listIterator(0);
570                while (iter.hasNext()) {
571                    Request request = (Request)iter.next();
572                    line.append(request + " ");
573                }
574                reqIter = reqList.mLowPriority.listIterator(0);
575                while (iter.hasNext()) {
576                    Request request = (Request)iter.next();
577                    line.append(request + " ");
578                }
579                dump.append(line);
580                dump.append("\n");
581            }
582        }
583        HttpLog.v(dump.toString());
584    }
585
586    /*
587     * RequestFeeder implementation
588     */
589    public synchronized Request getRequest() {
590        Request ret = null;
591
592        if (mNetworkConnected && !mPending.isEmpty()) {
593            ret = removeFirst(mPending);
594        }
595        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret);
596        return ret;
597    }
598
599    /**
600     * @return a request for given host if possible
601     */
602    public synchronized Request getRequest(HttpHost host) {
603        Request ret = null;
604
605        if (mNetworkConnected && mPending.containsKey(host)) {
606            RequestSet reqList = mPending.get(host);
607            ret = reqList.removeFirst();
608            if (reqList.isEmpty()) {
609                mPending.remove(host);
610            }
611        }
612        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret);
613        return ret;
614    }
615
616    /**
617     * @return true if a request for this host is available
618     */
619    public synchronized boolean haveRequest(HttpHost host) {
620        return mPending.containsKey(host);
621    }
622
623    /**
624     * Put request back on head of queue
625     */
626    public void requeueRequest(Request request) {
627        queueRequest(request, true);
628    }
629
630    /**
631     * This must be called to cleanly shutdown RequestQueue
632     */
633    public void shutdown() {
634        mActivePool.shutdown();
635    }
636
637    protected synchronized void queueRequest(Request request, boolean head) {
638        HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost;
639        RequestSet reqList;
640        if (mPending.containsKey(host)) {
641            reqList = mPending.get(host);
642        } else {
643            reqList = new RequestSet();
644            mPending.put(host, reqList);
645        }
646        reqList.add(request, head);
647    }
648
649
650    public void startTiming() {
651        mActivePool.startTiming();
652    }
653
654    public void stopTiming() {
655        mActivePool.stopTiming();
656    }
657
658    /* helper */
659    private Request removeFirst(LinkedHashMap<HttpHost, RequestSet> requestQueue) {
660        Request ret = null;
661        Iterator<Map.Entry<HttpHost, RequestSet>> iter = requestQueue.entrySet().iterator();
662        if (iter.hasNext()) {
663            Map.Entry<HttpHost, RequestSet> entry = iter.next();
664            RequestSet reqList = entry.getValue();
665            ret = reqList.removeFirst();
666            if (reqList.isEmpty()) {
667                requestQueue.remove(entry.getKey());
668            }
669        }
670        return ret;
671    }
672
673    /**
674     * This interface is exposed to each connection
675     */
676    interface ConnectionManager {
677        boolean isNetworkConnected();
678        HttpHost getProxyHost();
679        Connection getConnection(Context context, HttpHost host);
680        boolean recycleConnection(HttpHost host, Connection connection);
681    }
682}
683