ApacheHttpRequestAndroidpublic final class ApacheHttpRequestAndroid extends Object Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests.
These are performed synchronously (blocking). The caller should
ensure that it is in a background thread if asynchronous behavior
is required. All data is pushed, so there is no need for JNI native
callbacks.
This uses Apache's HttpClient framework to perform most
of the underlying network activity. The Android brower's cache,
android.webkit.CacheManager, is also used when caching is enabled,
and updated with new data. The android.webkit.CookieManager is also
queried and updated as necessary.
The public interface is designed to be called by native code
through JNI, and to simplify coding none of the public methods will
surface a checked exception. Unchecked exceptions may still be
raised but only if the system is in an ill state, such as out of
memory.
TODO: This isn't plumbed into LocalServer yet. Mutually
dependent on LocalServer - will attach the two together once both
are submitted. |
Fields Summary |
---|
private static final String | LOG_TAGDebug logging tag. | private static final String | HTTP_LINE_ENDINGHTTP response header line endings are CR-LF style. | private static final String | DEFAULT_MIME_TYPESafe MIME type to use whenever it isn't specified. | public static final String | KEY_CONTENT_LENGTHCase-sensitive header keys | public static final String | KEY_EXPIRES | public static final String | KEY_LAST_MODIFIED | public static final String | KEY_ETAG | public static final String | KEY_LOCATION | public static final String | KEY_CONTENT_TYPE | private static final int | BUFFER_SIZENumber of bytes to send and receive on the HTTP connection in
one go. | public static final int | HEADERS_MAP_INDEX_KEYThe first element of the String[] value in a headers map is the
unmodified (case-sensitive) key. | public static final int | HEADERS_MAP_INDEX_VALUEThe second element of the String[] value in a headers map is the
associated value. | private Map | mRequestHeadersRequest headers, as key -> value map. | private Map | mResponseHeadersResponse headers, as a lowercase key -> value map. | private String | mCacheResultUrlThe URL used for createCacheResult() | private android.webkit.CacheManager.CacheResult | mCacheResultCacheResult being saved into, if inserting a new cache entry. | private Thread | mBridgeThreadInitialized by initChildThread(). Used to target abort(). | private AbstractHttpClient | mClientOur HttpClient | private HttpRequestBase | mMethodThe HttpMethod associated with this request | private String | mResponseLineThe complete response line e.g "HTTP/1.0 200 OK" | private InputStream | mBodyInputStreamHTTP body stream, setup after connection. | private HttpResponse | mResponseHTTP Response Entity | private StreamEntity | mPostEntityPost Entity, used to stream the request to the server | private long | mContentLengthContent lenght, mandatory when using POST | private Thread | mHttpThreadThe request executes in a parallel thread | private Lock | mHttpThreadLockprotect mHttpThread, if interrupt() is called concurrently | private boolean | mConnectionFinishedFlag set to true when the request thread is joined | private boolean | mConnectionFailedFlag set to true by interrupt() and/or connection errors | private Lock | mConnectionFailedLockLock protecting the access to mConnectionFailed | private Lock | mStreamingReadyLockLock on the loop in StreamEntity | private Condition | mStreamingReadyCondition variable used to signal the loop is ready... | private Buffer | mBufferUsed to pass around the block of data POSTed | private SignalConsumed | mSignalUsed to signal that the block of data has been written |
Methods Summary |
---|
public synchronized void | abort()Called by the main thread to interrupt the child thread.
We do not set mConnectionFailed here as we still need the
ability to receive a null packet for sendPostData().
if (Config.LOGV) {
Log.i(LOG_TAG, "ABORT CALLED");
}
if (mMethod != null) {
mMethod.abort();
}
| public synchronized boolean | appendCacheResult(byte[] data, int bytes)Add data from the response body to the CacheResult created with
createCacheResult().
if (mCacheResult == null) {
if (Config.LOGV) {
Log.i(LOG_TAG, "appendCacheResult() called without a "
+ "CacheResult initialized");
}
return false;
}
try {
mCacheResult.getOutputStream().write(data, 0, bytes);
} catch (IOException ex) {
if (Config.LOGV) {
Log.i(LOG_TAG, "Got IOException writing cache data: " + ex);
}
return false;
}
return true;
| private void | applyRequestHeaders()
if (mMethod == null)
return;
Iterator<String[]> it = mRequestHeaders.values().iterator();
while (it.hasNext()) {
// Set the key case-sensitive.
String[] entry = it.next();
if (Config.LOGV) {
Log.i(LOG_TAG, "apply header " + entry[HEADERS_MAP_INDEX_KEY] +
" => " + entry[HEADERS_MAP_INDEX_VALUE]);
}
mMethod.setHeader(entry[HEADERS_MAP_INDEX_KEY],
entry[HEADERS_MAP_INDEX_VALUE]);
}
| public synchronized boolean | connectToRemote()We use this to start the connection thread (doing the method execute).
We usually always return true here, as the connection will run its
course in the thread.
We only return false if interrupted beforehand -- if a connection
problem happens, we will thus fail in either sendPostData() or
parseHeaders().
boolean ret = false;
applyRequestHeaders();
mConnectionFailedLock.lock();
if (!mConnectionFailed) {
mHttpThread = new Thread(new Connection());
mHttpThread.start();
}
ret = mConnectionFailed;
mConnectionFailedLock.unlock();
return !ret;
| public synchronized boolean | createCacheResult(java.lang.String url, int responseCode, java.lang.String mimeType, java.lang.String encoding)Create a CacheResult for this URL. This enables the repsonse body
to be sent in calls to appendCacheResult().
if (Config.LOGV) {
Log.i(LOG_TAG, "Making cache entry for " + url);
}
// Take the headers and parse them into a format needed by
// CacheManager.
Headers cacheHeaders = new Headers();
Iterator<Map.Entry<String, String[]>> it =
mResponseHeaders.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String[]> entry = it.next();
// Headers.parseHeader() expects lowercase keys.
String keyValue = entry.getKey() + ": "
+ entry.getValue()[HEADERS_MAP_INDEX_VALUE];
CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length());
buffer.append(keyValue);
// Parse it into the header container.
cacheHeaders.parseHeader(buffer);
}
mCacheResult = CacheManager.createCacheFile(
url, responseCode, cacheHeaders, mimeType, true);
if (mCacheResult != null) {
if (Config.LOGV) {
Log.i(LOG_TAG, "Saving into cache");
}
mCacheResult.setEncoding(encoding);
mCacheResultUrl = url;
return true;
} else {
if (Config.LOGV) {
Log.i(LOG_TAG, "Couldn't create mCacheResult");
}
return false;
}
| public synchronized java.lang.String | getAllResponseHeaders()Return all response headers, separated by CR-LF line endings, and
ending with a trailing blank line. This mimics the format of the
raw response header up to but not including the body.
if (mResponseHeaders == null) {
if (Config.LOGV) {
Log.i(LOG_TAG, "getAllResponseHeaders() called but "
+ "response not received");
}
return null;
}
StringBuilder result = new StringBuilder();
Iterator<String[]> it = mResponseHeaders.values().iterator();
while (it.hasNext()) {
String[] entry = it.next();
// Output the "key: value" lines.
result.append(entry[HEADERS_MAP_INDEX_KEY]);
result.append(": ");
result.append(entry[HEADERS_MAP_INDEX_VALUE]);
result.append(HTTP_LINE_ENDING);
}
result.append(HTTP_LINE_ENDING);
return result.toString();
| public static java.lang.String | getCookieForUrl(java.lang.String url)Get the cookie for the given URL.
// Get the cookie for this URL, set as a header
return CookieManager.getInstance().getCookie(url);
| public synchronized java.lang.String | getRequestHeader(java.lang.String name)Returns the value associated with the given request header.
String[] value = mRequestHeaders.get(name.toLowerCase());
if (value != null) {
return value[HEADERS_MAP_INDEX_VALUE];
} else {
return null;
}
| public synchronized java.lang.String | getResponseHeader(java.lang.String name)Returns the value associated with the given response header.
if (mResponseHeaders != null) {
String[] value = mResponseHeaders.get(name.toLowerCase());
if (value != null) {
return value[HEADERS_MAP_INDEX_VALUE];
} else {
return null;
}
} else {
if (Config.LOGV) {
Log.i(LOG_TAG, "getResponseHeader() called but "
+ "response not received");
}
return null;
}
| public synchronized java.lang.String | getResponseLine()Get the complete response line of the HTTP request. Only valid on
completion of the transaction.
return mResponseLine;
| public synchronized void | initChildThread()Initialize mBridgeThread using the TLS value of
Thread.currentThread(). Called on start up of the native child
thread.
mBridgeThread = Thread.currentThread();
| public synchronized void | interrupt()Interrupt a blocking IO operation and wait for the
thread to complete.
if (Config.LOGV) {
Log.i(LOG_TAG, "INTERRUPT CALLED");
}
mConnectionFailedLock.lock();
mConnectionFailed = true;
mConnectionFailedLock.unlock();
if (mMethod != null) {
mMethod.abort();
}
if (mHttpThread != null) {
waitUntilConnectionFinished();
}
| public synchronized boolean | open(java.lang.String method, java.lang.String url)Analagous to the native-side HttpRequest::open() function. This
initializes an underlying HttpClient method, but does
not go to the wire. On success, this enables a call to send() to
initiate the transaction.
if (Config.LOGV) {
Log.i(LOG_TAG, "open " + method + " " + url);
}
// Create the client
if (mConnectionFailed) {
// interrupt() could have been called even before open()
return false;
}
mClient = new DefaultHttpClient();
mClient.setHttpRequestRetryHandler(
new DefaultHttpRequestRetryHandler(0, false));
mBodyInputStream = null;
mResponseLine = null;
mResponseHeaders = null;
mPostEntity = null;
mHttpThread = null;
mConnectionFailed = false;
mConnectionFinished = false;
// Create the method. We support everything that
// Apache HttpClient supports, apart from TRACE.
if ("GET".equalsIgnoreCase(method)) {
mMethod = new HttpGet(url);
} else if ("POST".equalsIgnoreCase(method)) {
mMethod = new HttpPost(url);
mPostEntity = new StreamEntity();
((HttpPost)mMethod).setEntity(mPostEntity);
} else if ("HEAD".equalsIgnoreCase(method)) {
mMethod = new HttpHead(url);
} else if ("PUT".equalsIgnoreCase(method)) {
mMethod = new HttpPut(url);
} else if ("DELETE".equalsIgnoreCase(method)) {
mMethod = new HttpDelete(url);
} else {
if (Config.LOGV) {
Log.i(LOG_TAG, "Method " + method + " not supported");
}
return false;
}
HttpParams params = mClient.getParams();
// We handle the redirections C++-side
HttpClientParams.setRedirecting(params, false);
HttpProtocolParams.setUseExpectContinue(params, false);
return true;
| public synchronized boolean | parseHeaders()Receive all headers from the server and populate
mResponseHeaders.
mConnectionFailedLock.lock();
if (mConnectionFailed) {
mConnectionFailedLock.unlock();
return false;
}
mConnectionFailedLock.unlock();
waitUntilConnectionFinished();
mResponseHeaders = new HashMap<String, String[]>();
if (mResponse == null)
return false;
Header[] headers = mResponse.getAllHeaders();
for (int i = 0; i < headers.length; i++) {
Header header = headers[i];
if (Config.LOGV) {
Log.i(LOG_TAG, "header " + header.getName()
+ " -> " + header.getValue());
}
setResponseHeader(header.getName(), header.getValue());
}
return true;
| public synchronized int | receive(byte[] buf)Receive the next sequential bytes of the response body after
successful connection. This will receive up to the size of the
provided byte array. If there is no body, this will return 0
bytes on the first call after connection.
if (mBodyInputStream == null) {
// If this is the first call, setup the InputStream. This may
// fail if there were headers, but no body returned by the
// server.
try {
if (mResponse != null) {
HttpEntity entity = mResponse.getEntity();
mBodyInputStream = entity.getContent();
}
} catch (IOException inputException) {
if (Config.LOGV) {
Log.i(LOG_TAG, "Failed to connect InputStream: "
+ inputException);
}
// Not unexpected. For example, 404 response return headers,
// and sometimes a body with a detailed error.
}
if (mBodyInputStream == null) {
// No error stream either. Treat as a 0 byte response.
if (Config.LOGV) {
Log.i(LOG_TAG, "No InputStream");
}
return 0; // EOF.
}
}
int ret;
try {
int got = mBodyInputStream.read(buf);
if (got > 0) {
// Got some bytes, not EOF.
ret = got;
} else {
// EOF.
mBodyInputStream.close();
ret = 0;
}
} catch (IOException e) {
// An abort() interrupts us by calling close() on our stream.
if (Config.LOGV) {
Log.i(LOG_TAG, "Got IOException in mBodyInputStream.read(): ", e);
}
ret = -1;
}
return ret;
| public synchronized boolean | saveCacheResult()Save the completed CacheResult into the CacheManager. This must
have been created first with createCacheResult().
if (mCacheResult == null || mCacheResultUrl == null) {
if (Config.LOGV) {
Log.i(LOG_TAG, "Tried to save cache result but "
+ "createCacheResult not called");
}
return false;
}
if (Config.LOGV) {
Log.i(LOG_TAG, "Saving cache result");
}
CacheManager.saveCacheFile(mCacheResultUrl, mCacheResult);
mCacheResult = null;
mCacheResultUrl = null;
return true;
| public boolean | sendPostData(byte[] data, int bytes)For POST method requests, send a stream of data provided by the
native side in repeated callbacks.
We put the data in mBuffer, and wait until it is consumed
by the StreamEntity in the request thread.
mConnectionFailedLock.lock();
if (mConnectionFailed) {
mConnectionFailedLock.unlock();
return false;
}
mConnectionFailedLock.unlock();
if (mPostEntity == null) return false;
// We block until the outputstream is available
// (or in case of connection error)
if (!mPostEntity.isReady()) return false;
if (data == null && bytes == 0) {
mBuffer.put(null);
} else {
mBuffer.put(new DataPacket(data, bytes));
}
mSignal.waitUntilPacketConsumed();
mConnectionFailedLock.lock();
if (mConnectionFailed) {
Log.e(LOG_TAG, "failure");
mConnectionFailedLock.unlock();
return false;
}
mConnectionFailedLock.unlock();
return true;
| public void | setContentLength(long length)
mContentLength = length;
| public static void | setCookieForUrl(java.lang.String url, java.lang.String cookie)Set the cookie for the given URL.
// Get the cookie for this URL, set as a header
CookieManager.getInstance().setCookie(url, cookie);
| public synchronized void | setRequestHeader(java.lang.String name, java.lang.String value)Set a header to send with the HTTP request. Will not take effect
on a transaction already in progress. The key is associated
case-insensitive, but stored case-sensitive.
String[] mapValue = { name, value };
if (Config.LOGV) {
Log.i(LOG_TAG, "setRequestHeader: " + name + " => " + value);
}
if (name.equalsIgnoreCase(KEY_CONTENT_LENGTH)) {
setContentLength(Long.parseLong(value));
} else {
mRequestHeaders.put(name.toLowerCase(), mapValue);
}
| private void | setResponseHeader(java.lang.String name, java.lang.String value)Set a response header and associated value. The key is associated
case-insensitively, but stored case-sensitively.
if (Config.LOGV) {
Log.i(LOG_TAG, "Set response header " + name + ": " + value);
}
String mapValue[] = { name, value };
mResponseHeaders.put(name.toLowerCase(), mapValue);
| private void | synthesizeHeadersFromCacheResult(android.webkit.CacheManager.CacheResult cacheResult)Take the limited set of headers in a CacheResult and synthesize
response headers.
int statusCode = cacheResult.getHttpStatusCode();
// The status message is informal, so we can greatly simplify it.
String statusMessage;
if (statusCode >= 200 && statusCode < 300) {
statusMessage = "OK";
} else if (statusCode >= 300 && statusCode < 400) {
statusMessage = "MOVED";
} else {
statusMessage = "UNAVAILABLE";
}
// Synthesize the response line.
mResponseLine = "HTTP/1.1 " + statusCode + " " + statusMessage;
if (Config.LOGV) {
Log.i(LOG_TAG, "Synthesized " + mResponseLine);
}
// Synthesize the returned headers from cache.
mResponseHeaders = new HashMap<String, String[]>();
String contentLength = Long.toString(cacheResult.getContentLength());
setResponseHeader(KEY_CONTENT_LENGTH, contentLength);
long expires = cacheResult.getExpires();
if (expires >= 0) {
// "Expires" header is valid and finite. Milliseconds since 1970
// epoch, formatted as RFC-1123.
String expiresString = DateUtils.formatDate(new Date(expires));
setResponseHeader(KEY_EXPIRES, expiresString);
}
String lastModified = cacheResult.getLastModified();
if (lastModified != null) {
// Last modification time of the page. Passed end-to-end, but
// not used by us.
setResponseHeader(KEY_LAST_MODIFIED, lastModified);
}
String eTag = cacheResult.getETag();
if (eTag != null) {
// Entity tag. A kind of GUID to identify identical resources.
setResponseHeader(KEY_ETAG, eTag);
}
String location = cacheResult.getLocation();
if (location != null) {
// If valid, refers to the location of a redirect.
setResponseHeader(KEY_LOCATION, location);
}
String mimeType = cacheResult.getMimeType();
if (mimeType == null) {
// Use a safe default MIME type when none is
// specified. "text/plain" is safe to render in the browser
// window (even if large) and won't be intepreted as anything
// that would cause execution.
mimeType = DEFAULT_MIME_TYPE;
}
String encoding = cacheResult.getEncoding();
// Encoding may not be specified. No default.
String contentType = mimeType;
if (encoding != null) {
if (encoding.length() > 0) {
contentType += "; charset=" + encoding;
}
}
setResponseHeader(KEY_CONTENT_TYPE, contentType);
| public synchronized boolean | useCacheResult(java.lang.String url)Perform a request using the cache result if present. Initializes
class members so that receive() will obtain data from the cache.
// Try the browser's cache. CacheManager wants a Map<String, String>.
Map<String, String> cacheRequestHeaders = new HashMap<String, String>();
Iterator<Map.Entry<String, String[]>> it =
mRequestHeaders.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String[]> entry = it.next();
cacheRequestHeaders.put(
entry.getKey(),
entry.getValue()[HEADERS_MAP_INDEX_VALUE]);
}
CacheResult mCacheResult =
CacheManager.getCacheFile(url, cacheRequestHeaders);
if (mCacheResult == null) {
if (Config.LOGV) {
Log.i(LOG_TAG, "No CacheResult for " + url);
}
return false;
}
if (Config.LOGV) {
Log.i(LOG_TAG, "Got CacheResult from browser cache");
}
// Check for expiry. -1 is "never", otherwise milliseconds since 1970.
// Can be compared to System.currentTimeMillis().
long expires = mCacheResult.getExpires();
if (expires >= 0 && System.currentTimeMillis() >= expires) {
if (Config.LOGV) {
Log.i(LOG_TAG, "CacheResult expired "
+ (System.currentTimeMillis() - expires)
+ " milliseconds ago");
}
// Cache hit has expired. Do not return it.
return false;
}
// Setup the mBodyInputStream to come from the cache.
mBodyInputStream = mCacheResult.getInputStream();
if (mBodyInputStream == null) {
// Cache result may have gone away.
if (Config.LOGV) {
Log.i(LOG_TAG, "No mBodyInputStream for CacheResult " + url);
}
return false;
}
// Cache hit. Parse headers.
synthesizeHeadersFromCacheResult(mCacheResult);
return true;
| public synchronized boolean | useLocalServerResult(java.lang.String url)Perform a request using LocalServer if possible. Initializes
class members so that receive() will obtain data from the stream
provided by the response.
UrlInterceptHandlerGears handler =
UrlInterceptHandlerGears.getInstance();
if (handler == null) {
return false;
}
UrlInterceptHandlerGears.ServiceResponse serviceResponse =
handler.getServiceResponse(url, mRequestHeaders);
if (serviceResponse == null) {
if (Config.LOGV) {
Log.i(LOG_TAG, "No response in LocalServer");
}
return false;
}
// LocalServer will handle this URL. Initialize stream and
// response.
mBodyInputStream = serviceResponse.getInputStream();
mResponseLine = serviceResponse.getStatusLine();
mResponseHeaders = serviceResponse.getResponseHeaders();
if (Config.LOGV) {
Log.i(LOG_TAG, "Got response from LocalServer: " + mResponseLine);
}
return true;
| private void | waitUntilConnectionFinished()Wait for the request thread completion
(unless already finished)
if (Config.LOGV) {
Log.i(LOG_TAG, "waitUntilConnectionFinished("
+ mConnectionFinished + ")");
}
if (!mConnectionFinished) {
if (mHttpThread != null) {
try {
mHttpThread.join();
mConnectionFinished = true;
if (Config.LOGV) {
Log.i(LOG_TAG, "http thread joined");
}
} catch (InterruptedException e) {
if (Config.LOGV) {
Log.i(LOG_TAG, "interrupted: " + e);
}
}
} else {
Log.e(LOG_TAG, ">>> Trying to join on mHttpThread " +
"when it does not exist!");
}
}
|
|