diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-01-15 16:12:10 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-01-15 16:12:10 -0800 |
commit | 9266c558bf1d21ff647525ff99f7dadbca417309 (patch) | |
tree | 1630b1ba80f4793caf39d865528e662bdb1037fe /core/java/android/webkit/gears | |
parent | b798689749c64baba81f02e10cf2157c747d6b46 (diff) | |
download | frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.zip frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.tar.gz frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.tar.bz2 |
auto import from //branches/cupcake/...@126645
Diffstat (limited to 'core/java/android/webkit/gears')
-rw-r--r-- | core/java/android/webkit/gears/ApacheHttpRequestAndroid.java | 1122 |
1 files changed, 1122 insertions, 0 deletions
diff --git a/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java new file mode 100644 index 0000000..0569255 --- /dev/null +++ b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java @@ -0,0 +1,1122 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.net.http.Headers; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Config; +import android.util.Log; +import android.webkit.CacheManager; +import android.webkit.CacheManager.CacheResult; +import android.webkit.CookieManager; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.lang.StringBuilder; +import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.HttpResponse; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.client.*; +import org.apache.http.client.methods.*; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.util.CharArrayBuffer; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests. + * <p> 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. + * <p> 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. + * <p> 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. + * <p> TODO: This isn't plumbed into LocalServer yet. Mutually + * dependent on LocalServer - will attach the two together once both + * are submitted. + */ +public final class ApacheHttpRequestAndroid { + /** Debug logging tag. */ + private static final String LOG_TAG = "Gears-J"; + /** HTTP response header line endings are CR-LF style. */ + private static final String HTTP_LINE_ENDING = "\r\n"; + /** Safe MIME type to use whenever it isn't specified. */ + private static final String DEFAULT_MIME_TYPE = "text/plain"; + /** Case-sensitive header keys */ + public static final String KEY_CONTENT_LENGTH = "Content-Length"; + public static final String KEY_EXPIRES = "Expires"; + public static final String KEY_LAST_MODIFIED = "Last-Modified"; + public static final String KEY_ETAG = "ETag"; + public static final String KEY_LOCATION = "Location"; + public static final String KEY_CONTENT_TYPE = "Content-Type"; + /** Number of bytes to send and receive on the HTTP connection in + * one go. */ + private static final int BUFFER_SIZE = 4096; + + /** The first element of the String[] value in a headers map is the + * unmodified (case-sensitive) key. */ + public static final int HEADERS_MAP_INDEX_KEY = 0; + /** The second element of the String[] value in a headers map is the + * associated value. */ + public static final int HEADERS_MAP_INDEX_VALUE = 1; + + /** Request headers, as key -> value map. */ + // TODO: replace this design by a simpler one (the C++ side has to + // be modified too), where we do not store both the original header + // and the lowercase one. + private Map<String, String[]> mRequestHeaders = + new HashMap<String, String[]>(); + /** Response headers, as a lowercase key -> value map. */ + private Map<String, String[]> mResponseHeaders = + new HashMap<String, String[]>(); + /** The URL used for createCacheResult() */ + private String mCacheResultUrl; + /** CacheResult being saved into, if inserting a new cache entry. */ + private CacheResult mCacheResult; + /** Initialized by initChildThread(). Used to target abort(). */ + private Thread mBridgeThread; + + /** Our HttpClient */ + private AbstractHttpClient mClient; + /** The HttpMethod associated with this request */ + private HttpRequestBase mMethod; + /** The complete response line e.g "HTTP/1.0 200 OK" */ + private String mResponseLine; + /** HTTP body stream, setup after connection. */ + private InputStream mBodyInputStream; + + /** HTTP Response Entity */ + private HttpResponse mResponse; + + /** Post Entity, used to stream the request to the server */ + private StreamEntity mPostEntity = null; + /** Content lenght, mandatory when using POST */ + private long mContentLength; + + /** The request executes in a parallel thread */ + private Thread mHttpThread = null; + /** protect mHttpThread, if interrupt() is called concurrently */ + private Lock mHttpThreadLock = new ReentrantLock(); + /** Flag set to true when the request thread is joined */ + private boolean mConnectionFinished = false; + /** Flag set to true by interrupt() and/or connection errors */ + private boolean mConnectionFailed = false; + /** Lock protecting the access to mConnectionFailed */ + private Lock mConnectionFailedLock = new ReentrantLock(); + + /** Lock on the loop in StreamEntity */ + private Lock mStreamingReadyLock = new ReentrantLock(); + /** Condition variable used to signal the loop is ready... */ + private Condition mStreamingReady = mStreamingReadyLock.newCondition(); + + /** Used to pass around the block of data POSTed */ + private Buffer mBuffer = new Buffer(); + /** Used to signal that the block of data has been written */ + private SignalConsumed mSignal = new SignalConsumed(); + + // inner classes + + /** + * Implements the http request + */ + class Connection implements Runnable { + public void run() { + boolean problem = false; + try { + if (Config.LOGV) { + Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine()); + } + mResponse = mClient.execute(mMethod); + if (mResponse != null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "response (status line): " + + mResponse.getStatusLine()); + } + mResponseLine = "" + mResponse.getStatusLine(); + } else { + if (Config.LOGV) { + Log.i(LOG_TAG, "problem, response == null"); + } + problem = true; + } + } catch (IOException e) { + Log.e(LOG_TAG, "Connection IO exception ", e); + problem = true; + } catch (RuntimeException e) { + Log.e(LOG_TAG, "Connection runtime exception ", e); + problem = true; + } + + if (!problem) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Request complete (" + + mMethod.getRequestLine() + ")"); + } + } else { + mConnectionFailedLock.lock(); + mConnectionFailed = true; + mConnectionFailedLock.unlock(); + if (Config.LOGV) { + Log.i(LOG_TAG, "Request FAILED (" + + mMethod.getRequestLine() + ")"); + } + // We abort the execution in order to shutdown and release + // the underlying connection + mMethod.abort(); + if (mPostEntity != null) { + // If there is a post entity, we need to wake it up from + // a potential deadlock + mPostEntity.signalOutputStream(); + } + } + } + } + + /** + * simple buffer class implementing a producer/consumer model + */ + class Buffer { + private DataPacket mPacket; + private boolean mEmpty = true; + public synchronized void put(DataPacket packet) { + while (!mEmpty) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while putting " + + "a DataPacket in the Buffer: " + e); + } + } + } + mPacket = packet; + mEmpty = false; + notify(); + } + public synchronized DataPacket get() { + while (mEmpty) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while getting " + + "a DataPacket in the Buffer: " + e); + } + } + } + mEmpty = true; + notify(); + return mPacket; + } + } + + /** + * utility class used to block until the packet is signaled as being + * consumed + */ + class SignalConsumed { + private boolean mConsumed = false; + public synchronized void waitUntilPacketConsumed() { + while (!mConsumed) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while waiting " + + "until a DataPacket is consumed: " + e); + } + } + } + mConsumed = false; + notify(); + } + public synchronized void packetConsumed() { + while (mConsumed) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while indicating " + + "that the DataPacket has been consumed: " + e); + } + } + } + mConsumed = true; + notify(); + } + } + + /** + * Utility class encapsulating a packet of data + */ + class DataPacket { + private byte[] mContent; + private int mLength; + public DataPacket(byte[] content, int length) { + mContent = content; + mLength = length; + } + public byte[] getBytes() { + return mContent; + } + public int getLength() { + return mLength; + } + } + + /** + * HttpEntity class to write the bytes received by the C++ thread + * on the connection outputstream, in a streaming way. + * This entity is executed in the request thread. + * The writeTo() method is automatically called by the + * HttpPost execution; upon reception, we loop while receiving + * the data packets from the main thread, until completion + * or error. When done, we flush the outputstream. + * The main thread (sendPostData()) also blocks until the + * outputstream is made available (or an error happens) + */ + class StreamEntity implements HttpEntity { + private OutputStream mOutputStream; + + // HttpEntity interface methods + + public boolean isRepeatable() { + return false; + } + + public boolean isChunked() { + return false; + } + + public long getContentLength() { + return mContentLength; + } + + public Header getContentType() { + return null; + } + + public Header getContentEncoding() { + return null; + } + + public InputStream getContent() throws IOException { + return null; + } + + public void writeTo(final OutputStream out) throws IOException { + // We signal that the outputstream is available + mStreamingReadyLock.lock(); + mOutputStream = out; + mStreamingReady.signal(); + mStreamingReadyLock.unlock(); + + // We then loop waiting on messages to process. + boolean finished = false; + while (!finished) { + DataPacket packet = mBuffer.get(); + if (packet == null) { + finished = true; + } else { + write(packet); + } + mSignal.packetConsumed(); + mConnectionFailedLock.lock(); + if (mConnectionFailed) { + if (Config.LOGV) { + Log.i(LOG_TAG, "stopping loop on error"); + } + finished = true; + } + mConnectionFailedLock.unlock(); + } + if (Config.LOGV) { + Log.i(LOG_TAG, "flushing the outputstream..."); + } + mOutputStream.flush(); + } + + public boolean isStreaming() { + return true; + } + + public void consumeContent() throws IOException { + // Nothing to release + } + + // local methods + + private void write(DataPacket packet) { + try { + if (mOutputStream == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "NO OUTPUT STREAM !!!"); + } + return; + } + mOutputStream.write(packet.getBytes(), 0, packet.getLength()); + mOutputStream.flush(); + } catch (IOException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "exc: " + e); + } + mConnectionFailedLock.lock(); + mConnectionFailed = true; + mConnectionFailedLock.unlock(); + } + } + + public boolean isReady() { + mStreamingReadyLock.lock(); + try { + if (mOutputStream == null) { + mStreamingReady.await(); + } + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException in " + + "StreamEntity::isReady() : ", e); + } + } finally { + mStreamingReadyLock.unlock(); + } + if (mOutputStream == null) { + return false; + } + return true; + } + + public void signalOutputStream() { + mStreamingReadyLock.lock(); + mStreamingReady.signal(); + mStreamingReadyLock.unlock(); + } + } + + /** + * Initialize mBridgeThread using the TLS value of + * Thread.currentThread(). Called on start up of the native child + * thread. + */ + public synchronized void initChildThread() { + mBridgeThread = Thread.currentThread(); + } + + public void setContentLength(long length) { + mContentLength = length; + } + + /** + * 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. + */ + public synchronized boolean open(String method, String url) { + 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; + } + + /** + * 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(). + */ + public synchronized boolean connectToRemote() { + boolean ret = false; + applyRequestHeaders(); + mConnectionFailedLock.lock(); + if (!mConnectionFailed) { + mHttpThread = new Thread(new Connection()); + mHttpThread.start(); + } + ret = mConnectionFailed; + mConnectionFailedLock.unlock(); + return !ret; + } + + /** + * 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". + */ + public synchronized String getResponseLine() { + return mResponseLine; + } + + /** + * Wait for the request thread completion + * (unless already finished) + */ + private void waitUntilConnectionFinished() { + 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!"); + } + } + } + + // Headers handling + + /** + * Receive all headers from the server and populate + * mResponseHeaders. + * @return True if headers are successfully received, False on + * connection error. + */ + public synchronized boolean parseHeaders() { + 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; + } + + /** + * 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". + */ + public synchronized void setRequestHeader(String name, String value) { + 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); + } + } + + /** + * 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. + */ + public synchronized String getRequestHeader(String name) { + String[] value = mRequestHeaders.get(name.toLowerCase()); + if (value != null) { + return value[HEADERS_MAP_INDEX_VALUE]; + } else { + return null; + } + } + + 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]); + } + } + + /** + * 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. + */ + public synchronized String getResponseHeader(String name) { + 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; + } + } + + /** + * 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. + */ + public synchronized String getAllResponseHeaders() { + 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(); + } + + + /** + * 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. + */ + private void setResponseHeader(String name, String value) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Set response header " + name + ": " + value); + } + String mapValue[] = { name, value }; + mResponseHeaders.put(name.toLowerCase(), mapValue); + } + + // Cookie handling + + /** + * 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. + */ + public static String getCookieForUrl(String url) { + // Get the cookie for this URL, set as a header + return CookieManager.getInstance().getCookie(url); + } + + /** + * 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. + */ + public static void setCookieForUrl(String url, String cookie) { + // Get the cookie for this URL, set as a header + CookieManager.getInstance().setCookie(url, cookie); + } + + // Cache handling + + /** + * 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. + */ + public synchronized boolean useLocalServerResult(String url) { + 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; + } + + /** + * 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. + */ + public synchronized boolean useCacheResult(String url) { + // 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; + } + + /** + * Take the limited set of headers in a CacheResult and synthesize + * response headers. + * @param cacheResult A CacheResult to populate mResponseHeaders with. + */ + private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) { + 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); + } + + /** + * 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. + */ + public synchronized boolean createCacheResult( + String url, int responseCode, String mimeType, String encoding) { + 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; + } + } + + /** + * 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. + */ + public synchronized boolean appendCacheResult(byte[] data, int bytes) { + 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; + } + + /** + * 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. + */ + public synchronized boolean saveCacheResult() { + 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; + } + + + /** + * Interrupt a blocking IO operation. This will cause the child + * thread to expediently return from an operation if it was stuck at + * the time. Note that this inherently races, and unfortunately + * requires the caller to loop. + */ + public synchronized void interrupt() { + if (Config.LOGV) { + Log.i(LOG_TAG, "INTERRUPT CALLED"); + } + mConnectionFailedLock.lock(); + mConnectionFailed = true; + mConnectionFailedLock.unlock(); + if (mMethod != null) { + mMethod.abort(); + } + if (mHttpThread != null) { + waitUntilConnectionFinished(); + } + } + + /** + * 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. + */ + public synchronized int receive(byte[] buf) { + 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; + } + + /** + * 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. + */ + public boolean sendPostData(byte[] data, int bytes) { + 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; + } + +} |