blob: ce6b1ad223adc5797822b94cdb9d8194b20328e4 [file] [log] [blame]
/*
* 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.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 {
/**
* Requests, indexed by HttpHost (scheme, host, port)
*/
private final LinkedHashMap<HttpHost, LinkedList<Request>> mPending;
private final Context mContext;
private final ActivePool mActivePool;
private final ConnectivityManager mConnectivityManager;
private HttpHost mProxyHost = null;
private BroadcastReceiver mProxyChangeReceiver;
/* default simultaneous connection count */
private static final int CONNECTION_COUNT = 4;
/**
* 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();
}
}
void startConnectionThread() {
synchronized (RequestQueue.this) {
RequestQueue.this.notify();
}
}
public void startTiming() {
for (int i = 0; i < mConnectionCount; i++) {
ConnectionThread rt = mThreads[i];
rt.mCurrentThreadTime = -1;
rt.mTotalThreadTime = 0;
}
mTotalRequest = 0;
mTotalConnection = 0;
}
public void stopTiming() {
int totalTime = 0;
for (int i = 0; i < mConnectionCount; i++) {
ConnectionThread rt = mThreads[i];
if (rt.mCurrentThreadTime != -1) {
totalTime += rt.mTotalThreadTime;
}
rt.mCurrentThreadTime = 0;
}
Log.d("Http", "Http thread used " + totalTime + " ms " + " for "
+ mTotalRequest + " requests and " + mTotalConnection
+ " new 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) {
host = RequestQueue.this.determineHost(host);
Connection con = mIdleCache.getConnection(host);
if (con == null) {
mTotalConnection++;
con = Connection.getConnection(mContext, host, mProxyHost,
RequestQueue.this);
}
return con;
}
public boolean recycleConnection(Connection connection) {
return mIdleCache.cacheConnection(connection.getHost(), 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();
mConnectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
/**
* 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));
}
// we need to resample the current proxy setup
setProxyConfig();
}
/**
* 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 (mProxyChangeReceiver != null) {
mContext.unregisterReceiver(mProxyChangeReceiver);
mProxyChangeReceiver = null;
}
}
/**
* Because our IntentReceiver can run within a different thread,
* synchronize setting the proxy
*/
private synchronized void setProxyConfig() {
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
if (info != null && info.getType() == 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
*/
public RequestHandle queueRequest(
String url, String method,
Map<String, String> headers, EventHandler eventHandler,
InputStream bodyProvider, int bodyLength) {
WebAddress uri = new WebAddress(url);
return queueRequest(url, uri, method, headers, eventHandler,
bodyProvider, bodyLength);
}
/**
* 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
*/
public RequestHandle queueRequest(
String url, WebAddress uri, String method, Map<String, String> headers,
EventHandler eventHandler,
InputStream bodyProvider, int bodyLength) {
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.getHost(), uri.getPort(), uri.getScheme());
// set up request
req = new Request(method, httpHost, mProxyHost, uri.getPath(), bodyProvider,
bodyLength, eventHandler, headers);
queueRequest(req, false);
mActivePool.mTotalRequest++;
// dump();
mActivePool.startConnectionThread();
return new RequestHandle(
this, url, uri, method, headers, bodyProvider, bodyLength,
req);
}
private static class SyncFeeder implements RequestFeeder {
// This is used in the case where the request fails and needs to be
// requeued into the RequestFeeder.
private Request mRequest;
SyncFeeder() {
}
public Request getRequest() {
Request r = mRequest;
mRequest = null;
return r;
}
public Request getRequest(HttpHost host) {
return getRequest();
}
public boolean haveRequest(HttpHost host) {
return mRequest != null;
}
public void requeueRequest(Request r) {
mRequest = r;
}
}
public RequestHandle queueSynchronousRequest(String url, WebAddress uri,
String method, Map<String, String> headers,
EventHandler eventHandler, InputStream bodyProvider,
int bodyLength) {
if (HttpLog.LOGV) {
HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri);
}
HttpHost host = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
Request req = new Request(method, host, mProxyHost, uri.getPath(),
bodyProvider, bodyLength, eventHandler, headers);
// Open a new connection that uses our special RequestFeeder
// implementation.
host = determineHost(host);
Connection conn = Connection.getConnection(mContext, host, mProxyHost,
new SyncFeeder());
// TODO: I would like to process the request here but LoadListener
// needs a RequestHandle to process some messages.
return new RequestHandle(this, url, uri, method, headers, bodyProvider,
bodyLength, req, conn);
}
// Chooses between the proxy and the request's host.
private HttpHost determineHost(HttpHost host) {
// There used to be a comment in ConnectionThread about t-mob's proxy
// being really bad about https. But, HttpsConnection actually looks
// for a proxy and connects through it anyway. I think that this check
// is still valid because if a site is https, we will use
// HttpsConnection rather than HttpConnection if the proxy address is
// not secure.
return (mProxyHost == null || "https".equals(host.getSchemeName()))
? host : mProxyHost;
}
/**
* @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 (!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 (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 {
HttpHost getProxyHost();
Connection getConnection(Context context, HttpHost host);
boolean recycleConnection(Connection connection);
}
}