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