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