| // 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.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.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.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"; |
| /** Flag for guarding Log.v() calls. */ |
| private static final boolean LOGV_ENABLED = false; |
| /** 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine()); |
| } |
| mResponse = mClient.execute(mMethod); |
| if (mResponse != null) { |
| if (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "response (status line): " |
| + mResponse.getStatusLine()); |
| } |
| mResponseLine = "" + mResponse.getStatusLine(); |
| } else { |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "Request complete (" |
| + mMethod.getRequestLine() + ")"); |
| } |
| } else { |
| mConnectionFailedLock.lock(); |
| mConnectionFailed = true; |
| mConnectionFailedLock.unlock(); |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "stopping loop on error"); |
| } |
| finished = true; |
| } |
| mConnectionFailedLock.unlock(); |
| } |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "NO OUTPUT STREAM !!!"); |
| } |
| return; |
| } |
| mOutputStream.write(packet.getBytes(), 0, packet.getLength()); |
| mOutputStream.flush(); |
| } catch (IOException e) { |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "waitUntilConnectionFinished(" |
| + mConnectionFinished + ")"); |
| } |
| if (!mConnectionFinished) { |
| if (mHttpThread != null) { |
| try { |
| mHttpThread.join(); |
| mConnectionFinished = true; |
| if (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "http thread joined"); |
| } |
| } catch (InterruptedException e) { |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "No CacheResult for " + url); |
| } |
| return false; |
| } |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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); |
| String expires = cacheResult.getExpiresString(); |
| if (expires != null) { |
| setResponseHeader(KEY_EXPIRES, expires); |
| } |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "Saving into cache"); |
| } |
| mCacheResult.setEncoding(encoding); |
| mCacheResultUrl = url; |
| return true; |
| } else { |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "appendCacheResult() called without a " |
| + "CacheResult initialized"); |
| } |
| return false; |
| } |
| try { |
| mCacheResult.getOutputStream().write(data, 0, bytes); |
| } catch (IOException ex) { |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| Log.i(LOG_TAG, "Tried to save cache result but " |
| + "createCacheResult not called"); |
| } |
| return false; |
| } |
| |
| if (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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 (LOGV_ENABLED) { |
| 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; |
| } |
| |
| } |