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