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.util.Log;
33
34import java.io.InputStream;
35import java.util.Iterator;
36import java.util.LinkedHashMap;
37import java.util.LinkedList;
38import java.util.ListIterator;
39import java.util.Map;
40
41import org.apache.http.HttpHost;
42
43/**
44 * {@hide}
45 */
46public class RequestQueue implements RequestFeeder {
47
48
49    /**
50     * Requests, indexed by HttpHost (scheme, host, port)
51     */
52    private final LinkedHashMap<HttpHost, LinkedList<Request>> mPending;
53    private final Context mContext;
54    private final ActivePool mActivePool;
55    private final ConnectivityManager mConnectivityManager;
56
57    private HttpHost mProxyHost = null;
58    private BroadcastReceiver mProxyChangeReceiver;
59
60    /* default simultaneous connection count */
61    private static final int CONNECTION_COUNT = 4;
62
63    /**
64     * This class maintains active connection threads
65     */
66    class ActivePool implements ConnectionManager {
67        /** Threads used to process requests */
68        ConnectionThread[] mThreads;
69
70        IdleCache mIdleCache;
71
72        private int mTotalRequest;
73        private int mTotalConnection;
74        private int mConnectionCount;
75
76        ActivePool(int connectionCount) {
77            mIdleCache = new IdleCache();
78            mConnectionCount = connectionCount;
79            mThreads = new ConnectionThread[mConnectionCount];
80
81            for (int i = 0; i < mConnectionCount; i++) {
82                mThreads[i] = new ConnectionThread(
83                        mContext, i, this, RequestQueue.this);
84            }
85        }
86
87        void startup() {
88            for (int i = 0; i < mConnectionCount; i++) {
89                mThreads[i].start();
90            }
91        }
92
93        void shutdown() {
94            for (int i = 0; i < mConnectionCount; i++) {
95                mThreads[i].requestStop();
96            }
97        }
98
99        void startConnectionThread() {
100            synchronized (RequestQueue.this) {
101                RequestQueue.this.notify();
102            }
103        }
104
105        public void startTiming() {
106            for (int i = 0; i < mConnectionCount; i++) {
107                ConnectionThread rt = mThreads[i];
108                rt.mCurrentThreadTime = -1;
109                rt.mTotalThreadTime = 0;
110            }
111            mTotalRequest = 0;
112            mTotalConnection = 0;
113        }
114
115        public void stopTiming() {
116            int totalTime = 0;
117            for (int i = 0; i < mConnectionCount; i++) {
118                ConnectionThread rt = mThreads[i];
119                if (rt.mCurrentThreadTime != -1) {
120                    totalTime += rt.mTotalThreadTime;
121                }
122                rt.mCurrentThreadTime = 0;
123            }
124            Log.d("Http", "Http thread used " + totalTime + " ms " + " for "
125                    + mTotalRequest + " requests and " + mTotalConnection
126                    + " new connections");
127        }
128
129        void logState() {
130            StringBuilder dump = new StringBuilder();
131            for (int i = 0; i < mConnectionCount; i++) {
132                dump.append(mThreads[i] + "\n");
133            }
134            HttpLog.v(dump.toString());
135        }
136
137
138        public HttpHost getProxyHost() {
139            return mProxyHost;
140        }
141
142        /**
143         * Turns off persistence on all live connections
144         */
145        void disablePersistence() {
146            for (int i = 0; i < mConnectionCount; i++) {
147                Connection connection = mThreads[i].mConnection;
148                if (connection != null) connection.setCanPersist(false);
149            }
150            mIdleCache.clear();
151        }
152
153        /* Linear lookup -- okay for small thread counts.  Might use
154           private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap;
155           if this turns out to be a hotspot */
156        ConnectionThread getThread(HttpHost host) {
157            synchronized(RequestQueue.this) {
158                for (int i = 0; i < mThreads.length; i++) {
159                    ConnectionThread ct = mThreads[i];
160                    Connection connection = ct.mConnection;
161                    if (connection != null && connection.mHost.equals(host)) {
162                        return ct;
163                    }
164                }
165            }
166            return null;
167        }
168
169        public Connection getConnection(Context context, HttpHost host) {
170            host = RequestQueue.this.determineHost(host);
171            Connection con = mIdleCache.getConnection(host);
172            if (con == null) {
173                mTotalConnection++;
174                con = Connection.getConnection(mContext, host, mProxyHost,
175                        RequestQueue.this);
176            }
177            return con;
178        }
179        public boolean recycleConnection(Connection connection) {
180            return mIdleCache.cacheConnection(connection.getHost(), connection);
181        }
182
183    }
184
185    /**
186     * A RequestQueue class instance maintains a set of queued
187     * requests.  It orders them, makes the requests against HTTP
188     * servers, and makes callbacks to supplied eventHandlers as data
189     * is read.  It supports request prioritization, connection reuse
190     * and pipelining.
191     *
192     * @param context application context
193     */
194    public RequestQueue(Context context) {
195        this(context, CONNECTION_COUNT);
196    }
197
198    /**
199     * A RequestQueue class instance maintains a set of queued
200     * requests.  It orders them, makes the requests against HTTP
201     * servers, and makes callbacks to supplied eventHandlers as data
202     * is read.  It supports request prioritization, connection reuse
203     * and pipelining.
204     *
205     * @param context application context
206     * @param connectionCount The number of simultaneous connections
207     */
208    public RequestQueue(Context context, int connectionCount) {
209        mContext = context;
210
211        mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32);
212
213        mActivePool = new ActivePool(connectionCount);
214        mActivePool.startup();
215
216        mConnectivityManager = (ConnectivityManager)
217                context.getSystemService(Context.CONNECTIVITY_SERVICE);
218    }
219
220    /**
221     * Enables data state and proxy tracking
222     */
223    public synchronized void enablePlatformNotifications() {
224        if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network");
225
226        if (mProxyChangeReceiver == null) {
227            mProxyChangeReceiver =
228                    new BroadcastReceiver() {
229                        @Override
230                        public void onReceive(Context ctx, Intent intent) {
231                            setProxyConfig();
232                        }
233                    };
234            mContext.registerReceiver(mProxyChangeReceiver,
235                                      new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
236        }
237        // we need to resample the current proxy setup
238        setProxyConfig();
239    }
240
241    /**
242     * If platform notifications have been enabled, call this method
243     * to disable before destroying RequestQueue
244     */
245    public synchronized void disablePlatformNotifications() {
246        if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network");
247
248        if (mProxyChangeReceiver != null) {
249            mContext.unregisterReceiver(mProxyChangeReceiver);
250            mProxyChangeReceiver = null;
251        }
252    }
253
254    /**
255     * Because our IntentReceiver can run within a different thread,
256     * synchronize setting the proxy
257     */
258    private synchronized void setProxyConfig() {
259        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
260        if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) {
261            mProxyHost = null;
262        } else {
263            String host = Proxy.getHost(mContext);
264            if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host);
265            if (host == null) {
266                mProxyHost = null;
267            } else {
268                mActivePool.disablePersistence();
269                mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http");
270            }
271        }
272    }
273
274    /**
275     * used by webkit
276     * @return proxy host if set, null otherwise
277     */
278    public HttpHost getProxyHost() {
279        return mProxyHost;
280    }
281
282    /**
283     * Queues an HTTP request
284     * @param url The url to load.
285     * @param method "GET" or "POST."
286     * @param headers A hashmap of http headers.
287     * @param eventHandler The event handler for handling returned
288     * data.  Callbacks will be made on the supplied instance.
289     * @param bodyProvider InputStream providing HTTP body, null if none
290     * @param bodyLength length of body, must be 0 if bodyProvider is null
291     */
292    public RequestHandle queueRequest(
293            String url, String method,
294            Map<String, String> headers, EventHandler eventHandler,
295            InputStream bodyProvider, int bodyLength) {
296        WebAddress uri = new WebAddress(url);
297        return queueRequest(url, uri, method, headers, eventHandler,
298                            bodyProvider, bodyLength);
299    }
300
301    /**
302     * Queues an HTTP request
303     * @param url The url to load.
304     * @param uri The uri of the url to load.
305     * @param method "GET" or "POST."
306     * @param headers A hashmap of http headers.
307     * @param eventHandler The event handler for handling returned
308     * data.  Callbacks will be made on the supplied instance.
309     * @param bodyProvider InputStream providing HTTP body, null if none
310     * @param bodyLength length of body, must be 0 if bodyProvider is null
311     */
312    public RequestHandle queueRequest(
313            String url, WebAddress uri, String method, Map<String, String> headers,
314            EventHandler eventHandler,
315            InputStream bodyProvider, int bodyLength) {
316
317        if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri);
318
319        // Ensure there is an eventHandler set
320        if (eventHandler == null) {
321            eventHandler = new LoggingEventHandler();
322        }
323
324        /* Create and queue request */
325        Request req;
326        HttpHost httpHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
327
328        // set up request
329        req = new Request(method, httpHost, mProxyHost, uri.getPath(), bodyProvider,
330                          bodyLength, eventHandler, headers);
331
332        queueRequest(req, false);
333
334        mActivePool.mTotalRequest++;
335
336        // dump();
337        mActivePool.startConnectionThread();
338
339        return new RequestHandle(
340                this, url, uri, method, headers, bodyProvider, bodyLength,
341                req);
342    }
343
344    private static class SyncFeeder implements RequestFeeder {
345        // This is used in the case where the request fails and needs to be
346        // requeued into the RequestFeeder.
347        private Request mRequest;
348        SyncFeeder() {
349        }
350        public Request getRequest() {
351            Request r = mRequest;
352            mRequest = null;
353            return r;
354        }
355        public Request getRequest(HttpHost host) {
356            return getRequest();
357        }
358        public boolean haveRequest(HttpHost host) {
359            return mRequest != null;
360        }
361        public void requeueRequest(Request r) {
362            mRequest = r;
363        }
364    }
365
366    public RequestHandle queueSynchronousRequest(String url, WebAddress uri,
367            String method, Map<String, String> headers,
368            EventHandler eventHandler, InputStream bodyProvider,
369            int bodyLength) {
370        if (HttpLog.LOGV) {
371            HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri);
372        }
373
374        HttpHost host = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
375
376        Request req = new Request(method, host, mProxyHost, uri.getPath(),
377                bodyProvider, bodyLength, eventHandler, headers);
378
379        // Open a new connection that uses our special RequestFeeder
380        // implementation.
381        host = determineHost(host);
382        Connection conn = Connection.getConnection(mContext, host, mProxyHost,
383                new SyncFeeder());
384
385        // TODO: I would like to process the request here but LoadListener
386        // needs a RequestHandle to process some messages.
387        return new RequestHandle(this, url, uri, method, headers, bodyProvider,
388                bodyLength, req, conn);
389
390    }
391
392    // Chooses between the proxy and the request's host.
393    private HttpHost determineHost(HttpHost host) {
394        // There used to be a comment in ConnectionThread about t-mob's proxy
395        // being really bad about https. But, HttpsConnection actually looks
396        // for a proxy and connects through it anyway. I think that this check
397        // is still valid because if a site is https, we will use
398        // HttpsConnection rather than HttpConnection if the proxy address is
399        // not secure.
400        return (mProxyHost == null || "https".equals(host.getSchemeName()))
401                ? host : mProxyHost;
402    }
403
404    /**
405     * @return true iff there are any non-active requests pending
406     */
407    synchronized boolean requestsPending() {
408        return !mPending.isEmpty();
409    }
410
411
412    /**
413     * debug tool: prints request queue to log
414     */
415    synchronized void dump() {
416        HttpLog.v("dump()");
417        StringBuilder dump = new StringBuilder();
418        int count = 0;
419        Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter;
420
421        // mActivePool.log(dump);
422
423        if (!mPending.isEmpty()) {
424            iter = mPending.entrySet().iterator();
425            while (iter.hasNext()) {
426                Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
427                String hostName = entry.getKey().getHostName();
428                StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " ");
429
430                LinkedList<Request> reqList = entry.getValue();
431                ListIterator reqIter = reqList.listIterator(0);
432                while (iter.hasNext()) {
433                    Request request = (Request)iter.next();
434                    line.append(request + " ");
435                }
436                dump.append(line);
437                dump.append("\n");
438            }
439        }
440        HttpLog.v(dump.toString());
441    }
442
443    /*
444     * RequestFeeder implementation
445     */
446    public synchronized Request getRequest() {
447        Request ret = null;
448
449        if (!mPending.isEmpty()) {
450            ret = removeFirst(mPending);
451        }
452        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret);
453        return ret;
454    }
455
456    /**
457     * @return a request for given host if possible
458     */
459    public synchronized Request getRequest(HttpHost host) {
460        Request ret = null;
461
462        if (mPending.containsKey(host)) {
463            LinkedList<Request> reqList = mPending.get(host);
464            ret = reqList.removeFirst();
465            if (reqList.isEmpty()) {
466                mPending.remove(host);
467            }
468        }
469        if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret);
470        return ret;
471    }
472
473    /**
474     * @return true if a request for this host is available
475     */
476    public synchronized boolean haveRequest(HttpHost host) {
477        return mPending.containsKey(host);
478    }
479
480    /**
481     * Put request back on head of queue
482     */
483    public void requeueRequest(Request request) {
484        queueRequest(request, true);
485    }
486
487    /**
488     * This must be called to cleanly shutdown RequestQueue
489     */
490    public void shutdown() {
491        mActivePool.shutdown();
492    }
493
494    protected synchronized void queueRequest(Request request, boolean head) {
495        HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost;
496        LinkedList<Request> reqList;
497        if (mPending.containsKey(host)) {
498            reqList = mPending.get(host);
499        } else {
500            reqList = new LinkedList<Request>();
501            mPending.put(host, reqList);
502        }
503        if (head) {
504            reqList.addFirst(request);
505        } else {
506            reqList.add(request);
507        }
508    }
509
510
511    public void startTiming() {
512        mActivePool.startTiming();
513    }
514
515    public void stopTiming() {
516        mActivePool.stopTiming();
517    }
518
519    /* helper */
520    private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) {
521        Request ret = null;
522        Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator();
523        if (iter.hasNext()) {
524            Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
525            LinkedList<Request> reqList = entry.getValue();
526            ret = reqList.removeFirst();
527            if (reqList.isEmpty()) {
528                requestQueue.remove(entry.getKey());
529            }
530        }
531        return ret;
532    }
533
534    /**
535     * This interface is exposed to each connection
536     */
537    interface ConnectionManager {
538        HttpHost getProxyHost();
539        Connection getConnection(Context context, HttpHost host);
540        boolean recycleConnection(Connection connection);
541    }
542}
543