// 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; } /** * 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(). */ public synchronized void abort() { if (Config.LOGV) { Log.i(LOG_TAG, "ABORT CALLED"); } if (mMethod != null) { mMethod.abort(); } } /** * Interrupt a blocking IO operation and wait for the * thread to complete. */ 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; } }