// 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.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";
    /** 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);
        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 (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;
    }

}
