RequestQueue.java revision aaebc86386c8bb44c25dd06fe573e52ef6b60fbe
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            Connection con = mIdleCache.getConnection(host);
175            if (con == null) {
176                mTotalConnection++;
177                con = Connection.getConnection(
178                        mContext, host, this, RequestQueue.this);
179            }
180            return con;
181        }
182        public boolean recycleConnection(HttpHost host, Connection connection) {
183            return mIdleCache.cacheConnection(host, connection);
184        }
185
186    }
187
188    /**
189     * A RequestQueue class instance maintains a set of queued
190     * requests.  It orders them, makes the requests against HTTP
191     * servers, and makes callbacks to supplied eventHandlers as data
192     * is read.  It supports request prioritization, connection reuse
193     * and pipelining.
194     *
195     * @param context application context
196     */
197    public RequestQueue(Context context) {
198        this(context, CONNECTION_COUNT);
199    }
200
201    /**
202     * A RequestQueue class instance maintains a set of queued
203     * requests.  It orders them, makes the requests against HTTP
204     * servers, and makes callbacks to supplied eventHandlers as data
205     * is read.  It supports request prioritization, connection reuse
206     * and pipelining.
207     *
208     * @param context application context
209     * @param connectionCount The number of simultaneous connections
210     */
211    public RequestQueue(Context context, int connectionCount) {
212        mContext = context;
213
214        mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32);
215
216        mActivePool = new ActivePool(connectionCount);
217        mActivePool.startup();
218
219        mConnectivityManager = (ConnectivityManager)
220                context.getSystemService(Context.CONNECTIVITY_SERVICE);
221    }
222
223    /**
224     * Enables data state and proxy tracking
225     */
226    public synchronized void enablePlatformNotifications() {
227        if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network");
228
229        if (mProxyChangeReceiver == null) {
230            mProxyChangeReceiver =
231                    new BroadcastReceiver() {
232                        @Override
233                        public void onReceive(Context ctx, Intent intent) {
234                            setProxyConfig();
235                        }
236                    };
237            mContext.registerReceiver(mProxyChangeReceiver,
238                                      new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
239        }
240    }
241
242    /**
243     * If platform notifications have been enabled, call this method
244     * to disable before destroying RequestQueue
245     */
246    public synchronized void disablePlatformNotifications() {
247        if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network");
248
249        if (mProxyChangeReceiver != null) {
250            mContext.unregisterReceiver(mProxyChangeReceiver);
251            mProxyChangeReceiver = null;
252        }
253    }
254
255    /**
256     * Because our IntentReceiver can run within a different thread,
257     * synchronize setting the proxy
258     */
259    private synchronized void setProxyConfig() {
260        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
261        if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) {
262            mProxyHost = null;
263        } else {
264            String host = Proxy.getHost(mContext);
265            if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host);
266            if (host == null) {
267                mProxyHost = null;
268            } else {
269                mActivePool.disablePersistence();
270                mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http");
271            }
272        }
273    }
274
275    /**
276     * used by webkit
277     * @return proxy host if set, null otherwise
278     */
279    public HttpHost getProxyHost() {
280        return mProxyHost;
281    }
282
283    /**
284     * Queues an HTTP request
285     * @param url The url to load.
286     * @param method "GET" or "POST."
287     * @param headers A hashmap of http headers.
288     * @param eventHandler The event handler for handling returned
289     * data.  Callbacks will be made on the supplied instance.
290     * @param bodyProvider InputStream providing HTTP body, null if none
291     * @param bodyLength length of body, must be 0 if bodyProvider is null
292     */
293    public RequestHandle queueRequest(
294            String url, String method,
295            Map<String, String> headers, EventHandler eventHandler,
296            InputStream bodyProvider, int bodyLength) {
297        WebAddress uri = new WebAddress(url);
298        return queueRequest(url, uri, method, headers, eventHandler,
299                            bodyProvider, bodyLength);
300    }
301
302    /**
303     * Queues an HTTP request
304     * @param url The url to load.
305     * @param uri The uri of the url to load.
306     * @param method "GET" or "POST."
307     * @param headers A hashmap of http headers.
308     * @param eventHandler The event handler for handling returned
309     * data.  Callbacks will be made on the supplied instance.
310     * @param bodyProvider InputStream providing HTTP body, null if none
311     * @param bodyLength length of body, must be 0 if bodyProvider is null
312     */
313    public RequestHandle queueRequest(
314            String url, WebAddress uri, String method, Map<String, String> headers,
315            EventHandler eventHandler,
316            InputStream bodyProvider, int bodyLength) {
317
318        if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri);
319
320        // Ensure there is an eventHandler set
321        if (eventHandler == null) {
322            eventHandler = new LoggingEventHandler();
323        }
324
325        /* Create and queue request */
326        Request req;
327        HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme);
328
329        // set up request
330        req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider,
331                          bodyLength, eventHandler, headers);
332
333        queueRequest(req, false);
334
335        mActivePool.mTotalRequest++;
336
337        // dump();
338        mActivePool.startConnectionThread();
339
340        return new RequestHandle(
341                this, url, uri, method, headers, bodyProvider, bodyLength,
342                req);
343    }
344
345    /**
346     * @return true iff there are any non-active requests pending
347     */
348    synchronized boolean requestsPending() {
349        return !mPending.isEmpty();
350    }
351
352
353    /**
354     * debug tool: prints request queue to log
355     */
356    synchronized void dump() {
357        HttpLog.v("dump()");
358        StringBuilder dump = new StringBuilder();
359        int count = 0;
360        Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter;
361
362        // mActivePool.log(dump);
363
364        if (!mPending.isEmpty()) {
365            iter = mPending.entrySet().iterator();
366            while (iter.hasNext()) {
367                Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
368                String hostName = entry.getKey().getHostName();
369                StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " ");
370
371                LinkedList<Request> reqList = entry.getValue();
372                ListIterator reqIter = reqList.listIterator(0);
373                while (iter.hasNext()) {
374                    Request request = (Request)iter.next();
375                    line.append(request + " ");
376                }
377                dump.append(line);
378                dump.append("\n");
379            }
380        }
381        HttpLog.v(dump.toString());
382    }
383
384    /*
385     * RequestFeeder implementation
386     */
387    public synchronized Request getRequest() {
388        Request ret = null;
389
390        if (!mPending.isEmpty()) {
391            ret = removeFirst(mPending);
392        }
393        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret);
394        return ret;
395    }
396
397    /**
398     * @return a request for given host if possible
399     */
400    public synchronized Request getRequest(HttpHost host) {
401        Request ret = null;
402
403        if (mPending.containsKey(host)) {
404            LinkedList<Request> reqList = mPending.get(host);
405            ret = reqList.removeFirst();
406            if (reqList.isEmpty()) {
407                mPending.remove(host);
408            }
409        }
410        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret);
411        return ret;
412    }
413
414    /**
415     * @return true if a request for this host is available
416     */
417    public synchronized boolean haveRequest(HttpHost host) {
418        return mPending.containsKey(host);
419    }
420
421    /**
422     * Put request back on head of queue
423     */
424    public void requeueRequest(Request request) {
425        queueRequest(request, true);
426    }
427
428    /**
429     * This must be called to cleanly shutdown RequestQueue
430     */
431    public void shutdown() {
432        mActivePool.shutdown();
433    }
434
435    protected synchronized void queueRequest(Request request, boolean head) {
436        HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost;
437        LinkedList<Request> reqList;
438        if (mPending.containsKey(host)) {
439            reqList = mPending.get(host);
440        } else {
441            reqList = new LinkedList<Request>();
442            mPending.put(host, reqList);
443        }
444        if (head) {
445            reqList.addFirst(request);
446        } else {
447            reqList.add(request);
448        }
449    }
450
451
452    public void startTiming() {
453        mActivePool.startTiming();
454    }
455
456    public void stopTiming() {
457        mActivePool.stopTiming();
458    }
459
460    /* helper */
461    private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) {
462        Request ret = null;
463        Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator();
464        if (iter.hasNext()) {
465            Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
466            LinkedList<Request> reqList = entry.getValue();
467            ret = reqList.removeFirst();
468            if (reqList.isEmpty()) {
469                requestQueue.remove(entry.getKey());
470            }
471        }
472        return ret;
473    }
474
475    /**
476     * This interface is exposed to each connection
477     */
478    interface ConnectionManager {
479        HttpHost getProxyHost();
480        Connection getConnection(Context context, HttpHost host);
481        boolean recycleConnection(HttpHost host, Connection connection);
482    }
483}
484