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