/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * High level HTTP Interface * Queues requests as necessary */ package android.net.http; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkConnectivityListener; import android.net.NetworkInfo; import android.net.Proxy; import android.net.WebAddress; import android.os.Handler; import android.os.Message; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Log; import java.io.InputStream; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.ListIterator; import java.util.Map; import org.apache.http.HttpHost; /** * {@hide} */ public class RequestQueue implements RequestFeeder { private Context mContext; /** * Requests, indexed by HttpHost (scheme, host, port) */ private LinkedHashMap<HttpHost, LinkedList<Request>> mPending; /* Support for notifying a client when queue is empty */ private boolean mClientWaiting = false; /** true if connected */ boolean mNetworkConnected = true; private HttpHost mProxyHost = null; private BroadcastReceiver mProxyChangeReceiver; private ActivePool mActivePool; /* default simultaneous connection count */ private static final int CONNECTION_COUNT = 4; /** * This intent broadcast when http is paused or unpaused due to * net availability toggling */ public final static String HTTP_NETWORK_STATE_CHANGED_INTENT = "android.net.http.NETWORK_STATE"; public final static String HTTP_NETWORK_STATE_UP = "up"; /** * Listen to platform network state. On a change, * (1) kick stack on or off as appropriate * (2) send an intent to my host app telling * it what I've done */ private NetworkStateTracker mNetworkStateTracker; class NetworkStateTracker { final static int EVENT_DATA_STATE_CHANGED = 100; Context mContext; NetworkConnectivityListener mConnectivityListener; NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED; int mCurrentNetworkType; NetworkStateTracker(Context context) { mContext = context; } /** * register for updates */ protected void enable() { if (mConnectivityListener == null) { /* * Initializing the network type is really unnecessary, * since as soon as we register with the NCL, we'll * get a CONNECTED event for the active network, and * we'll configure the HTTP proxy accordingly. However, * as a fallback in case that doesn't happen for some * reason, initializing to type WIFI would mean that * we'd start out without a proxy. This seems better * than thinking we have a proxy (which is probably * private to the carrier network and therefore * unreachable outside of that network) when we really * shouldn't. */ mCurrentNetworkType = ConnectivityManager.TYPE_WIFI; mConnectivityListener = new NetworkConnectivityListener(); mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED); mConnectivityListener.startListening(mContext); } } protected void disable() { if (mConnectivityListener != null) { mConnectivityListener.unregisterHandler(mHandler); mConnectivityListener.stopListening(); mConnectivityListener = null; } } private Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case EVENT_DATA_STATE_CHANGED: networkStateChanged(); break; } } }; int getCurrentNetworkType() { return mCurrentNetworkType; } void networkStateChanged() { if (mConnectivityListener == null) return; NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState(); NetworkInfo info = mConnectivityListener.getNetworkInfo(); if (info == null) { /** * We've been seeing occasional NPEs here. I believe recent changes * have made this impossible, but in the interest of being totally * paranoid, check and log this here. */ HttpLog.v("NetworkStateTracker: connectivity broadcast" + " has null network info - ignoring"); return; } NetworkInfo.State state = info.getState(); if (HttpLog.LOGV) { HttpLog.v("NetworkStateTracker " + info.getTypeName() + " state= " + state + " last= " + mLastNetworkState + " connectivityState= " + connectivityState.toString()); } boolean newConnection = state != mLastNetworkState && state == NetworkInfo.State.CONNECTED; if (state == NetworkInfo.State.CONNECTED) { mCurrentNetworkType = info.getType(); setProxyConfig(); } mLastNetworkState = state; if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) { setNetworkState(false); broadcastState(false); } else if (newConnection) { setNetworkState(true); broadcastState(true); } } void broadcastState(boolean connected) { Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT); intent.putExtra(HTTP_NETWORK_STATE_UP, connected); mContext.sendBroadcast(intent); } } /** * This class maintains active connection threads */ class ActivePool implements ConnectionManager { /** Threads used to process requests */ ConnectionThread[] mThreads; IdleCache mIdleCache; private int mTotalRequest; private int mTotalConnection; private int mConnectionCount; ActivePool(int connectionCount) { mIdleCache = new IdleCache(); mConnectionCount = connectionCount; mThreads = new ConnectionThread[mConnectionCount]; for (int i = 0; i < mConnectionCount; i++) { mThreads[i] = new ConnectionThread( mContext, i, this, RequestQueue.this); } } void startup() { for (int i = 0; i < mConnectionCount; i++) { mThreads[i].start(); } } void shutdown() { for (int i = 0; i < mConnectionCount; i++) { mThreads[i].requestStop(); } } public boolean isNetworkConnected() { return mNetworkConnected; } void startConnectionThread() { synchronized (RequestQueue.this) { RequestQueue.this.notify(); } } public void startTiming() { for (int i = 0; i < mConnectionCount; i++) { mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime; } mTotalRequest = 0; mTotalConnection = 0; } public void stopTiming() { int totalTime = 0; for (int i = 0; i < mConnectionCount; i++) { ConnectionThread rt = mThreads[i]; totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime); rt.mStartThreadTime = -1; } Log.d("Http", "Http thread used " + totalTime + " ms " + " for " + mTotalRequest + " requests and " + mTotalConnection + " connections"); } void logState() { StringBuilder dump = new StringBuilder(); for (int i = 0; i < mConnectionCount; i++) { dump.append(mThreads[i] + "\n"); } HttpLog.v(dump.toString()); } public HttpHost getProxyHost() { return mProxyHost; } /** * Turns off persistence on all live connections */ void disablePersistence() { for (int i = 0; i < mConnectionCount; i++) { Connection connection = mThreads[i].mConnection; if (connection != null) connection.setCanPersist(false); } mIdleCache.clear(); } /* Linear lookup -- okay for small thread counts. Might use private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap; if this turns out to be a hotspot */ ConnectionThread getThread(HttpHost host) { synchronized(RequestQueue.this) { for (int i = 0; i < mThreads.length; i++) { ConnectionThread ct = mThreads[i]; Connection connection = ct.mConnection; if (connection != null && connection.mHost.equals(host)) { return ct; } } } return null; } public Connection getConnection(Context context, HttpHost host) { Connection con = mIdleCache.getConnection(host); if (con == null) { mTotalConnection++; con = Connection.getConnection( mContext, host, this, RequestQueue.this); } return con; } public boolean recycleConnection(HttpHost host, Connection connection) { return mIdleCache.cacheConnection(host, connection); } } /** * A RequestQueue class instance maintains a set of queued * requests. It orders them, makes the requests against HTTP * servers, and makes callbacks to supplied eventHandlers as data * is read. It supports request prioritization, connection reuse * and pipelining. * * @param context application context */ public RequestQueue(Context context) { this(context, CONNECTION_COUNT); } /** * A RequestQueue class instance maintains a set of queued * requests. It orders them, makes the requests against HTTP * servers, and makes callbacks to supplied eventHandlers as data * is read. It supports request prioritization, connection reuse * and pipelining. * * @param context application context * @param connectionCount The number of simultaneous connections */ public RequestQueue(Context context, int connectionCount) { mContext = context; mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32); mActivePool = new ActivePool(connectionCount); mActivePool.startup(); } /** * Enables data state and proxy tracking */ public synchronized void enablePlatformNotifications() { if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network"); if (mProxyChangeReceiver == null) { mProxyChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context ctx, Intent intent) { setProxyConfig(); } }; mContext.registerReceiver(mProxyChangeReceiver, new IntentFilter(Proxy.PROXY_CHANGE_ACTION)); } /* Network state notification is broken on the simulator don't register for notifications on SIM */ String device = SystemProperties.get("ro.product.device"); boolean simulation = TextUtils.isEmpty(device); if (!simulation) { if (mNetworkStateTracker == null) { mNetworkStateTracker = new NetworkStateTracker(mContext); } mNetworkStateTracker.enable(); } } /** * If platform notifications have been enabled, call this method * to disable before destroying RequestQueue */ public synchronized void disablePlatformNotifications() { if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); if (mNetworkStateTracker != null) { mNetworkStateTracker.disable(); } if (mProxyChangeReceiver != null) { mContext.unregisterReceiver(mProxyChangeReceiver); mProxyChangeReceiver = null; } } /** * Because our IntentReceiver can run within a different thread, * synchronize setting the proxy */ private synchronized void setProxyConfig() { if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) { mProxyHost = null; } else { String host = Proxy.getHost(mContext); if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host); if (host == null) { mProxyHost = null; } else { mActivePool.disablePersistence(); mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http"); } } } /** * used by webkit * @return proxy host if set, null otherwise */ public HttpHost getProxyHost() { return mProxyHost; } /** * Queues an HTTP request * @param url The url to load. * @param method "GET" or "POST." * @param headers A hashmap of http headers. * @param eventHandler The event handler for handling returned * data. Callbacks will be made on the supplied instance. * @param bodyProvider InputStream providing HTTP body, null if none * @param bodyLength length of body, must be 0 if bodyProvider is null * @param highPriority If true, queues before low priority * requests if possible */ public RequestHandle queueRequest( String url, String method, Map<String, String> headers, EventHandler eventHandler, InputStream bodyProvider, int bodyLength, boolean highPriority) { WebAddress uri = new WebAddress(url); return queueRequest(url, uri, method, headers, eventHandler, bodyProvider, bodyLength, highPriority); } /** * Queues an HTTP request * @param url The url to load. * @param uri The uri of the url to load. * @param method "GET" or "POST." * @param headers A hashmap of http headers. * @param eventHandler The event handler for handling returned * data. Callbacks will be made on the supplied instance. * @param bodyProvider InputStream providing HTTP body, null if none * @param bodyLength length of body, must be 0 if bodyProvider is null * @param highPriority If true, queues before low priority * requests if possible */ public RequestHandle queueRequest( String url, WebAddress uri, String method, Map<String, String> headers, EventHandler eventHandler, InputStream bodyProvider, int bodyLength, boolean highPriority) { if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); // Ensure there is an eventHandler set if (eventHandler == null) { eventHandler = new LoggingEventHandler(); } /* Create and queue request */ Request req; HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); // set up request req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider, bodyLength, eventHandler, headers, highPriority); queueRequest(req, highPriority); mActivePool.mTotalRequest++; // dump(); mActivePool.startConnectionThread(); return new RequestHandle( this, url, uri, method, headers, bodyProvider, bodyLength, req); } /** * Called by the NetworkStateTracker -- updates when network connectivity * is lost/restored. * * If isNetworkConnected is true, start processing requests */ public void setNetworkState(boolean isNetworkConnected) { if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected); mNetworkConnected = isNetworkConnected; if (isNetworkConnected) mActivePool.startConnectionThread(); } /** * @return true iff there are any non-active requests pending */ synchronized boolean requestsPending() { return !mPending.isEmpty(); } /** * debug tool: prints request queue to log */ synchronized void dump() { HttpLog.v("dump()"); StringBuilder dump = new StringBuilder(); int count = 0; Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter; // mActivePool.log(dump); if (!mPending.isEmpty()) { iter = mPending.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); String hostName = entry.getKey().getHostName(); StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); LinkedList<Request> reqList = entry.getValue(); ListIterator reqIter = reqList.listIterator(0); while (iter.hasNext()) { Request request = (Request)iter.next(); line.append(request + " "); } dump.append(line); dump.append("\n"); } } HttpLog.v(dump.toString()); } /* * RequestFeeder implementation */ public synchronized Request getRequest() { Request ret = null; if (mNetworkConnected && !mPending.isEmpty()) { ret = removeFirst(mPending); } if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); return ret; } /** * @return a request for given host if possible */ public synchronized Request getRequest(HttpHost host) { Request ret = null; if (mNetworkConnected && mPending.containsKey(host)) { LinkedList<Request> reqList = mPending.get(host); ret = reqList.removeFirst(); if (reqList.isEmpty()) { mPending.remove(host); } } if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret); return ret; } /** * @return true if a request for this host is available */ public synchronized boolean haveRequest(HttpHost host) { return mPending.containsKey(host); } /** * Put request back on head of queue */ public void requeueRequest(Request request) { queueRequest(request, true); } /** * This must be called to cleanly shutdown RequestQueue */ public void shutdown() { mActivePool.shutdown(); } protected synchronized void queueRequest(Request request, boolean head) { HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; LinkedList<Request> reqList; if (mPending.containsKey(host)) { reqList = mPending.get(host); } else { reqList = new LinkedList<Request>(); mPending.put(host, reqList); } if (head) { reqList.addFirst(request); } else { reqList.add(request); } } public void startTiming() { mActivePool.startTiming(); } public void stopTiming() { mActivePool.stopTiming(); } /* helper */ private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) { Request ret = null; Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator(); if (iter.hasNext()) { Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); LinkedList<Request> reqList = entry.getValue(); ret = reqList.removeFirst(); if (reqList.isEmpty()) { requestQueue.remove(entry.getKey()); } } return ret; } /** * This interface is exposed to each connection */ interface ConnectionManager { boolean isNetworkConnected(); HttpHost getProxyHost(); Connection getConnection(Context context, HttpHost host); boolean recycleConnection(HttpHost host, Connection connection); } }