FileDocCategorySizeDatePackage
ApacheHttpRequestAndroid.javaAPI DocAndroid 1.5 API42345Wed May 06 22:41:56 BST 2009android.webkit.gears

ApacheHttpRequestAndroid

public 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_TAG
Debug logging tag.
private static final String
HTTP_LINE_ENDING
HTTP response header line endings are CR-LF style.
private static final String
DEFAULT_MIME_TYPE
Safe MIME type to use whenever it isn't specified.
public static final String
KEY_CONTENT_LENGTH
Case-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_SIZE
Number of bytes to send and receive on the HTTP connection in one go.
public static final int
HEADERS_MAP_INDEX_KEY
The first element of the String[] value in a headers map is the unmodified (case-sensitive) key.
public static final int
HEADERS_MAP_INDEX_VALUE
The second element of the String[] value in a headers map is the associated value.
private Map
mRequestHeaders
Request headers, as key -> value map.
private Map
mResponseHeaders
Response headers, as a lowercase key -> value map.
private String
mCacheResultUrl
The URL used for createCacheResult()
private android.webkit.CacheManager.CacheResult
mCacheResult
CacheResult being saved into, if inserting a new cache entry.
private Thread
mBridgeThread
Initialized by initChildThread(). Used to target abort().
private AbstractHttpClient
mClient
Our HttpClient
private HttpRequestBase
mMethod
The HttpMethod associated with this request
private String
mResponseLine
The complete response line e.g "HTTP/1.0 200 OK"
private InputStream
mBodyInputStream
HTTP body stream, setup after connection.
private HttpResponse
mResponse
HTTP Response Entity
private StreamEntity
mPostEntity
Post Entity, used to stream the request to the server
private long
mContentLength
Content lenght, mandatory when using POST
private Thread
mHttpThread
The request executes in a parallel thread
private Lock
mHttpThreadLock
protect mHttpThread, if interrupt() is called concurrently
private boolean
mConnectionFinished
Flag set to true when the request thread is joined
private boolean
mConnectionFailed
Flag set to true by interrupt() and/or connection errors
private Lock
mConnectionFailedLock
Lock protecting the access to mConnectionFailed
private Lock
mStreamingReadyLock
Lock on the loop in StreamEntity
private Condition
mStreamingReady
Condition variable used to signal the loop is ready...
private Buffer
mBuffer
Used to pass around the block of data POSTed
private SignalConsumed
mSignal
Used to signal that the block of data has been written
Constructors Summary
Methods Summary
public synchronized voidabort()
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 booleanappendCacheResult(byte[] data, int bytes)
Add data from the response body to the CacheResult created with createCacheResult().

param
data A byte array of the next sequential bytes in the response body.
param
bytes The number of bytes to write from the start of the array.
return
True if all bytes successfully written, false on failure.

        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 voidapplyRequestHeaders()

        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 booleanconnectToRemote()
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 booleancreateCacheResult(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().

param
url The fully qualified URL to add to the cache.
param
responseCode The response code returned for the request, e.g 200.
param
mimeType The MIME type of the body, e.g "text/plain".
param
encoding The encoding, e.g "utf-8". Use "" for unknown.

        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.StringgetAllResponseHeaders()
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.

return
A string containing the entire response header.

        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.StringgetCookieForUrl(java.lang.String url)
Get the cookie for the given URL.

param
url The fully qualified URL.
return
A string containing the cookie for the URL if it exists, or null if not.

        // Get the cookie for this URL, set as a header
        return CookieManager.getInstance().getCookie(url);
    
public synchronized java.lang.StringgetRequestHeader(java.lang.String name)
Returns the value associated with the given request header.

param
name The name of the request header, non-null, case-insensitive.
return
The value associated with the request header, or null if not set, or error.

        String[] value = mRequestHeaders.get(name.toLowerCase());
        if (value != null) {
            return value[HEADERS_MAP_INDEX_VALUE];
        } else {
            return null;
        }
    
public synchronized java.lang.StringgetResponseHeader(java.lang.String name)
Returns the value associated with the given response header.

param
name The name of the response header, non-null, case-insensitive.
return
The value associated with the response header, or null if not set or error.

        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.StringgetResponseLine()
Get the complete response line of the HTTP request. Only valid on completion of the transaction.

return
The complete HTTP response line, e.g "HTTP/1.0 200 OK".

        return mResponseLine;
    
public synchronized voidinitChildThread()
Initialize mBridgeThread using the TLS value of Thread.currentThread(). Called on start up of the native child thread.

        mBridgeThread = Thread.currentThread();
    
public synchronized voidinterrupt()
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 booleanopen(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.

param
method The HTTP method, e.g GET or POST.
param
url The URL to open.
return
True on success with a complete HTTP response. False on failure.

        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 booleanparseHeaders()
Receive all headers from the server and populate mResponseHeaders.

return
True if headers are successfully received, False on connection error.

        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 intreceive(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.

param
buf A pre-allocated byte array to receive data into.
return
The number of bytes from the start of the array which have been filled, 0 on EOF, or negative on error.

        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 booleansaveCacheResult()
Save the completed CacheResult into the CacheManager. This must have been created first with createCacheResult().

return
Returns true if the entry has been successfully saved.

        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 booleansendPostData(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.

param
data A byte array containing the data to sent, or null if indicating EOF.
param
bytes The number of bytes from the start of the array to send, or 0 if indicating EOF.
return
True if all bytes were successfully sent, false on error.

        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 voidsetContentLength(long length)

        mContentLength = length;
    
public static voidsetCookieForUrl(java.lang.String url, java.lang.String cookie)
Set the cookie for the given URL.

param
url The fully qualified URL.
param
cookie The new cookie value.
return
A string containing the cookie for the URL if it exists, or null if not.

        // Get the cookie for this URL, set as a header
        CookieManager.getInstance().setCookie(url, cookie);
    
public synchronized voidsetRequestHeader(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.

param
name The name of the header, e.g "Set-Cookie".
param
value The value for this header, e.g "text/html".

        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 voidsetResponseHeader(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.

param
name Case sensitive request header key.
param
value The associated value.

        if (Config.LOGV) {
            Log.i(LOG_TAG, "Set response header " + name + ": " + value);
        }
        String mapValue[] = { name, value };
        mResponseHeaders.put(name.toLowerCase(), mapValue);
    
private voidsynthesizeHeadersFromCacheResult(android.webkit.CacheManager.CacheResult cacheResult)
Take the limited set of headers in a CacheResult and synthesize response headers.

param
cacheResult A CacheResult to populate mResponseHeaders with.

        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 booleanuseCacheResult(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.

param
url The fully qualified URL to try in the cache.
return
True is the url was found and is now setup to receive from cache. False if not found, with no side-effect.

        // 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 booleanuseLocalServerResult(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.

param
url The fully qualified URL to try in LocalServer.
return
True if the url was found and is now setup to receive. False if not found, with no side-effect.

        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 voidwaitUntilConnectionFinished()
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!");
            }
        }