From 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 3 Mar 2009 19:31:44 -0800 Subject: auto import from //depot/cupcake/@135843 --- core/java/android/net/http/RequestQueue.java | 647 +++++++++++++++++++++++++++ 1 file changed, 647 insertions(+) create mode 100644 core/java/android/net/http/RequestQueue.java (limited to 'core/java/android/net/http/RequestQueue.java') diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java new file mode 100644 index 0000000..66d5722 --- /dev/null +++ b/core/java/android/net/http/RequestQueue.java @@ -0,0 +1,647 @@ +/* + * 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> 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> 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>(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 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 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>> iter; + + // mActivePool.log(dump); + + if (!mPending.isEmpty()) { + iter = mPending.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry> entry = iter.next(); + String hostName = entry.getKey().getHostName(); + StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); + + LinkedList 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 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 reqList; + if (mPending.containsKey(host)) { + reqList = mPending.get(host); + } else { + reqList = new LinkedList(); + 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> requestQueue) { + Request ret = null; + Iterator>> iter = requestQueue.entrySet().iterator(); + if (iter.hasNext()) { + Map.Entry> entry = iter.next(); + LinkedList 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); + } +} -- cgit v1.1