1// Copyright 2008, The Android Open Source Project
2//
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
5//
6//  1. Redistributions of source code must retain the above copyright notice,
7//     this list of conditions and the following disclaimer.
8//  2. Redistributions in binary form must reproduce the above copyright notice,
9//     this list of conditions and the following disclaimer in the documentation
10//     and/or other materials provided with the distribution.
11//  3. Neither the name of Google Inc. nor the names of its contributors may be
12//     used to endorse or promote products derived from this software without
13//     specific prior written permission.
14//
15// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
16// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
17// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
18// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
26package android.webkit.gears;
27
28import android.net.http.Headers;
29import android.os.Handler;
30import android.os.Looper;
31import android.os.Message;
32import android.util.Log;
33import android.webkit.CacheManager;
34import android.webkit.CacheManager.CacheResult;
35import android.webkit.CookieManager;
36
37import java.io.InputStream;
38import java.io.OutputStream;
39import java.io.IOException;
40import java.lang.StringBuilder;
41import java.util.Date;
42import java.util.Map;
43import java.util.HashMap;
44import java.util.Iterator;
45
46import org.apache.http.Header;
47import org.apache.http.HttpEntity;
48import org.apache.http.client.params.HttpClientParams;
49import org.apache.http.params.HttpParams;
50import org.apache.http.params.HttpConnectionParams;
51import org.apache.http.params.HttpProtocolParams;
52import org.apache.http.HttpResponse;
53import org.apache.http.entity.AbstractHttpEntity;
54import org.apache.http.client.*;
55import org.apache.http.client.methods.*;
56import org.apache.http.impl.client.AbstractHttpClient;
57import org.apache.http.impl.client.DefaultHttpClient;
58import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
59import org.apache.http.conn.ssl.StrictHostnameVerifier;
60import org.apache.http.impl.cookie.DateUtils;
61import org.apache.http.util.CharArrayBuffer;
62
63import java.util.concurrent.locks.Condition;
64import java.util.concurrent.locks.Lock;
65import java.util.concurrent.locks.ReentrantLock;
66
67/**
68 * Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests.
69 * <p> These are performed synchronously (blocking). The caller should
70 * ensure that it is in a background thread if asynchronous behavior
71 * is required. All data is pushed, so there is no need for JNI native
72 * callbacks.
73 * <p> This uses Apache's HttpClient framework to perform most
74 * of the underlying network activity. The Android brower's cache,
75 * android.webkit.CacheManager, is also used when caching is enabled,
76 * and updated with new data. The android.webkit.CookieManager is also
77 * queried and updated as necessary.
78 * <p> The public interface is designed to be called by native code
79 * through JNI, and to simplify coding none of the public methods will
80 * surface a checked exception. Unchecked exceptions may still be
81 * raised but only if the system is in an ill state, such as out of
82 * memory.
83 * <p> TODO: This isn't plumbed into LocalServer yet. Mutually
84 * dependent on LocalServer - will attach the two together once both
85 * are submitted.
86 */
87public final class ApacheHttpRequestAndroid {
88    /** Debug logging tag. */
89    private static final String LOG_TAG = "Gears-J";
90    /** Flag for guarding Log.v() calls. */
91    private static final boolean LOGV_ENABLED = false;
92    /** HTTP response header line endings are CR-LF style. */
93    private static final String HTTP_LINE_ENDING = "\r\n";
94    /** Safe MIME type to use whenever it isn't specified. */
95    private static final String DEFAULT_MIME_TYPE = "text/plain";
96    /** Case-sensitive header keys */
97    public static final String KEY_CONTENT_LENGTH = "Content-Length";
98    public static final String KEY_EXPIRES = "Expires";
99    public static final String KEY_LAST_MODIFIED = "Last-Modified";
100    public static final String KEY_ETAG = "ETag";
101    public static final String KEY_LOCATION = "Location";
102    public static final String KEY_CONTENT_TYPE = "Content-Type";
103    /** Number of bytes to send and receive on the HTTP connection in
104     * one go. */
105    private static final int BUFFER_SIZE = 4096;
106
107    /** The first element of the String[] value in a headers map is the
108     * unmodified (case-sensitive) key. */
109    public static final int HEADERS_MAP_INDEX_KEY = 0;
110    /** The second element of the String[] value in a headers map is the
111     * associated value. */
112    public static final int HEADERS_MAP_INDEX_VALUE = 1;
113
114    /** Request headers, as key -> value map. */
115    // TODO: replace this design by a simpler one (the C++ side has to
116    // be modified too), where we do not store both the original header
117    // and the lowercase one.
118    private Map<String, String[]> mRequestHeaders =
119        new HashMap<String, String[]>();
120    /** Response headers, as a lowercase key -> value map. */
121    private Map<String, String[]> mResponseHeaders =
122        new HashMap<String, String[]>();
123    /** The URL used for createCacheResult() */
124    private String mCacheResultUrl;
125    /** CacheResult being saved into, if inserting a new cache entry. */
126    private CacheResult mCacheResult;
127    /** Initialized by initChildThread(). Used to target abort(). */
128    private Thread mBridgeThread;
129
130    /** Our HttpClient */
131    private AbstractHttpClient mClient;
132    /** The HttpMethod associated with this request */
133    private HttpRequestBase mMethod;
134    /** The complete response line e.g "HTTP/1.0 200 OK" */
135    private String mResponseLine;
136    /** HTTP body stream, setup after connection. */
137    private InputStream mBodyInputStream;
138
139    /** HTTP Response Entity */
140    private HttpResponse mResponse;
141
142    /** Post Entity, used to stream the request to the server */
143    private StreamEntity mPostEntity = null;
144    /** Content lenght, mandatory when using POST */
145    private long mContentLength;
146
147    /** The request executes in a parallel thread */
148    private Thread mHttpThread = null;
149    /** protect mHttpThread, if interrupt() is called concurrently */
150    private Lock mHttpThreadLock = new ReentrantLock();
151    /** Flag set to true when the request thread is joined */
152    private boolean mConnectionFinished = false;
153    /** Flag set to true by interrupt() and/or connection errors */
154    private boolean mConnectionFailed = false;
155    /** Lock protecting the access to mConnectionFailed */
156    private Lock mConnectionFailedLock = new ReentrantLock();
157
158    /** Lock on the loop in StreamEntity */
159    private Lock mStreamingReadyLock = new ReentrantLock();
160    /** Condition variable used to signal the loop is ready... */
161    private Condition mStreamingReady = mStreamingReadyLock.newCondition();
162
163    /** Used to pass around the block of data POSTed */
164    private Buffer mBuffer = new Buffer();
165    /** Used to signal that the block of data has been written */
166    private SignalConsumed mSignal = new SignalConsumed();
167
168    // inner classes
169
170    /**
171     * Implements the http request
172     */
173    class Connection implements Runnable {
174        public void run() {
175            boolean problem = false;
176            try {
177                if (LOGV_ENABLED) {
178                    Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine());
179                }
180                mResponse = mClient.execute(mMethod);
181                if (mResponse != null) {
182                    if (LOGV_ENABLED) {
183                        Log.i(LOG_TAG, "response (status line): "
184                              + mResponse.getStatusLine());
185                    }
186                    mResponseLine = "" + mResponse.getStatusLine();
187                } else {
188                    if (LOGV_ENABLED) {
189                        Log.i(LOG_TAG, "problem, response == null");
190                    }
191                    problem = true;
192                }
193            } catch (IOException e) {
194                Log.e(LOG_TAG, "Connection IO exception ", e);
195                problem = true;
196            } catch (RuntimeException e) {
197                Log.e(LOG_TAG, "Connection runtime exception ", e);
198                problem = true;
199            }
200
201            if (!problem) {
202                if (LOGV_ENABLED) {
203                    Log.i(LOG_TAG, "Request complete ("
204                          + mMethod.getRequestLine() + ")");
205                }
206            } else {
207                mConnectionFailedLock.lock();
208                mConnectionFailed = true;
209                mConnectionFailedLock.unlock();
210                if (LOGV_ENABLED) {
211                    Log.i(LOG_TAG, "Request FAILED ("
212                          + mMethod.getRequestLine() + ")");
213                }
214                // We abort the execution in order to shutdown and release
215                // the underlying connection
216                mMethod.abort();
217                if (mPostEntity != null) {
218                    // If there is a post entity, we need to wake it up from
219                    // a potential deadlock
220                    mPostEntity.signalOutputStream();
221                }
222            }
223        }
224    }
225
226    /**
227     * simple buffer class implementing a producer/consumer model
228     */
229    class Buffer {
230        private DataPacket mPacket;
231        private boolean mEmpty = true;
232        public synchronized void put(DataPacket packet) {
233            while (!mEmpty) {
234                try {
235                    wait();
236                } catch (InterruptedException e) {
237                    if (LOGV_ENABLED) {
238                        Log.i(LOG_TAG, "InterruptedException while putting " +
239                            "a DataPacket in the Buffer: " + e);
240                    }
241                }
242            }
243            mPacket = packet;
244            mEmpty = false;
245            notify();
246        }
247        public synchronized DataPacket get() {
248            while (mEmpty) {
249                try {
250                    wait();
251                } catch (InterruptedException e) {
252                    if (LOGV_ENABLED) {
253                      Log.i(LOG_TAG, "InterruptedException while getting " +
254                          "a DataPacket in the Buffer: " + e);
255                    }
256                }
257            }
258            mEmpty = true;
259            notify();
260            return mPacket;
261        }
262    }
263
264    /**
265     * utility class used to block until the packet is signaled as being
266     * consumed
267     */
268    class SignalConsumed {
269        private boolean mConsumed = false;
270        public synchronized void waitUntilPacketConsumed() {
271            while (!mConsumed) {
272                try {
273                    wait();
274                } catch (InterruptedException e) {
275                    if (LOGV_ENABLED) {
276                        Log.i(LOG_TAG, "InterruptedException while waiting " +
277                            "until a DataPacket is consumed: " + e);
278                    }
279                }
280            }
281            mConsumed = false;
282            notify();
283        }
284        public synchronized void packetConsumed() {
285            while (mConsumed) {
286                try {
287                    wait();
288                } catch (InterruptedException e) {
289                    if (LOGV_ENABLED) {
290                        Log.i(LOG_TAG, "InterruptedException while indicating "
291                              + "that the DataPacket has been consumed: " + e);
292                    }
293                }
294            }
295            mConsumed = true;
296            notify();
297        }
298    }
299
300    /**
301     * Utility class encapsulating a packet of data
302     */
303    class DataPacket {
304        private byte[] mContent;
305        private int mLength;
306        public DataPacket(byte[] content, int length) {
307            mContent = content;
308            mLength = length;
309        }
310        public byte[] getBytes() {
311            return mContent;
312        }
313        public int getLength() {
314            return mLength;
315        }
316    }
317
318    /**
319     * HttpEntity class to write the bytes received by the C++ thread
320     * on the connection outputstream, in a streaming way.
321     * This entity is executed in the request thread.
322     * The writeTo() method is automatically called by the
323     * HttpPost execution; upon reception, we loop while receiving
324     * the data packets from the main thread, until completion
325     * or error. When done, we flush the outputstream.
326     * The main thread (sendPostData()) also blocks until the
327     * outputstream is made available (or an error happens)
328     */
329    class StreamEntity implements HttpEntity {
330        private OutputStream mOutputStream;
331
332        // HttpEntity interface methods
333
334        public boolean isRepeatable() {
335            return false;
336        }
337
338        public boolean isChunked() {
339            return false;
340        }
341
342        public long getContentLength() {
343            return mContentLength;
344        }
345
346        public Header getContentType() {
347            return null;
348        }
349
350        public Header getContentEncoding() {
351            return null;
352        }
353
354        public InputStream getContent() throws IOException {
355            return null;
356        }
357
358        public void writeTo(final OutputStream out) throws IOException {
359            // We signal that the outputstream is available
360            mStreamingReadyLock.lock();
361            mOutputStream = out;
362            mStreamingReady.signal();
363            mStreamingReadyLock.unlock();
364
365            // We then loop waiting on messages to process.
366            boolean finished = false;
367            while (!finished) {
368                DataPacket packet = mBuffer.get();
369                if (packet == null) {
370                    finished = true;
371                } else {
372                    write(packet);
373                }
374                mSignal.packetConsumed();
375                mConnectionFailedLock.lock();
376                if (mConnectionFailed) {
377                    if (LOGV_ENABLED) {
378                        Log.i(LOG_TAG, "stopping loop on error");
379                    }
380                    finished = true;
381                }
382                mConnectionFailedLock.unlock();
383            }
384            if (LOGV_ENABLED) {
385                Log.i(LOG_TAG, "flushing the outputstream...");
386            }
387            mOutputStream.flush();
388        }
389
390        public boolean isStreaming() {
391            return true;
392        }
393
394        public void consumeContent() throws IOException {
395            // Nothing to release
396        }
397
398        // local methods
399
400        private void write(DataPacket packet) {
401            try {
402                if (mOutputStream == null) {
403                    if (LOGV_ENABLED) {
404                        Log.i(LOG_TAG, "NO OUTPUT STREAM !!!");
405                    }
406                    return;
407                }
408                mOutputStream.write(packet.getBytes(), 0, packet.getLength());
409                mOutputStream.flush();
410            } catch (IOException e) {
411                if (LOGV_ENABLED) {
412                    Log.i(LOG_TAG, "exc: " + e);
413                }
414                mConnectionFailedLock.lock();
415                mConnectionFailed = true;
416                mConnectionFailedLock.unlock();
417            }
418        }
419
420        public boolean isReady() {
421            mStreamingReadyLock.lock();
422            try {
423                if (mOutputStream == null) {
424                    mStreamingReady.await();
425                }
426            } catch (InterruptedException e) {
427                if (LOGV_ENABLED) {
428                    Log.i(LOG_TAG, "InterruptedException in "
429                          + "StreamEntity::isReady() : ", e);
430                }
431            } finally {
432                mStreamingReadyLock.unlock();
433            }
434            if (mOutputStream == null) {
435                return false;
436            }
437            return true;
438        }
439
440        public void signalOutputStream() {
441            mStreamingReadyLock.lock();
442            mStreamingReady.signal();
443            mStreamingReadyLock.unlock();
444        }
445    }
446
447    /**
448     * Initialize mBridgeThread using the TLS value of
449     * Thread.currentThread(). Called on start up of the native child
450     * thread.
451     */
452    public synchronized void initChildThread() {
453        mBridgeThread = Thread.currentThread();
454    }
455
456    public void setContentLength(long length) {
457        mContentLength = length;
458    }
459
460    /**
461     * Analagous to the native-side HttpRequest::open() function. This
462     * initializes an underlying HttpClient method, but does
463     * not go to the wire. On success, this enables a call to send() to
464     * initiate the transaction.
465     *
466     * @param method    The HTTP method, e.g GET or POST.
467     * @param url       The URL to open.
468     * @return          True on success with a complete HTTP response.
469     *                  False on failure.
470     */
471    public synchronized boolean open(String method, String url) {
472        if (LOGV_ENABLED) {
473            Log.i(LOG_TAG, "open " + method + " " + url);
474        }
475        // Create the client
476        if (mConnectionFailed) {
477            // interrupt() could have been called even before open()
478            return false;
479        }
480        mClient = new DefaultHttpClient();
481        mClient.setHttpRequestRetryHandler(
482            new DefaultHttpRequestRetryHandler(0, false));
483        mBodyInputStream = null;
484        mResponseLine = null;
485        mResponseHeaders = null;
486        mPostEntity = null;
487        mHttpThread = null;
488        mConnectionFailed = false;
489        mConnectionFinished = false;
490
491        // Create the method. We support everything that
492        // Apache HttpClient supports, apart from TRACE.
493        if ("GET".equalsIgnoreCase(method)) {
494            mMethod = new HttpGet(url);
495        } else if ("POST".equalsIgnoreCase(method)) {
496            mMethod = new HttpPost(url);
497            mPostEntity = new StreamEntity();
498            ((HttpPost)mMethod).setEntity(mPostEntity);
499        } else if ("HEAD".equalsIgnoreCase(method)) {
500            mMethod = new HttpHead(url);
501        } else if ("PUT".equalsIgnoreCase(method)) {
502            mMethod = new HttpPut(url);
503        } else if ("DELETE".equalsIgnoreCase(method)) {
504            mMethod = new HttpDelete(url);
505        } else {
506            if (LOGV_ENABLED) {
507                Log.i(LOG_TAG, "Method " + method + " not supported");
508            }
509            return false;
510        }
511        HttpParams params = mClient.getParams();
512        // We handle the redirections C++-side
513        HttpClientParams.setRedirecting(params, false);
514        HttpProtocolParams.setUseExpectContinue(params, false);
515        return true;
516    }
517
518    /**
519     * We use this to start the connection thread (doing the method execute).
520     * We usually always return true here, as the connection will run its
521     * course in the thread.
522     * We only return false if interrupted beforehand -- if a connection
523     * problem happens, we will thus fail in either sendPostData() or
524     * parseHeaders().
525     */
526    public synchronized boolean connectToRemote() {
527        boolean ret = false;
528        applyRequestHeaders();
529        mConnectionFailedLock.lock();
530        if (!mConnectionFailed) {
531            mHttpThread = new Thread(new Connection());
532            mHttpThread.start();
533        }
534        ret = mConnectionFailed;
535        mConnectionFailedLock.unlock();
536        return !ret;
537    }
538
539    /**
540     * Get the complete response line of the HTTP request. Only valid on
541     * completion of the transaction.
542     * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK".
543     */
544    public synchronized String getResponseLine() {
545        return mResponseLine;
546    }
547
548    /**
549     * Wait for the request thread completion
550     * (unless already finished)
551     */
552    private void waitUntilConnectionFinished() {
553        if (LOGV_ENABLED) {
554            Log.i(LOG_TAG, "waitUntilConnectionFinished("
555                  + mConnectionFinished + ")");
556        }
557        if (!mConnectionFinished) {
558            if (mHttpThread != null) {
559                try {
560                    mHttpThread.join();
561                    mConnectionFinished = true;
562                    if (LOGV_ENABLED) {
563                        Log.i(LOG_TAG, "http thread joined");
564                    }
565                } catch (InterruptedException e) {
566                    if (LOGV_ENABLED) {
567                        Log.i(LOG_TAG, "interrupted: " + e);
568                    }
569                }
570            } else {
571                Log.e(LOG_TAG, ">>> Trying to join on mHttpThread " +
572                      "when it does not exist!");
573            }
574        }
575    }
576
577    // Headers handling
578
579    /**
580     * Receive all headers from the server and populate
581     * mResponseHeaders.
582     * @return True if headers are successfully received, False on
583     *         connection error.
584     */
585    public synchronized boolean parseHeaders() {
586        mConnectionFailedLock.lock();
587        if (mConnectionFailed) {
588            mConnectionFailedLock.unlock();
589            return false;
590        }
591        mConnectionFailedLock.unlock();
592        waitUntilConnectionFinished();
593        mResponseHeaders = new HashMap<String, String[]>();
594        if (mResponse == null)
595            return false;
596
597        Header[] headers = mResponse.getAllHeaders();
598        for (int i = 0; i < headers.length; i++) {
599            Header header = headers[i];
600            if (LOGV_ENABLED) {
601                Log.i(LOG_TAG, "header " + header.getName()
602                      + " -> " + header.getValue());
603            }
604            setResponseHeader(header.getName(), header.getValue());
605        }
606
607        return true;
608    }
609
610    /**
611     * Set a header to send with the HTTP request. Will not take effect
612     * on a transaction already in progress. The key is associated
613     * case-insensitive, but stored case-sensitive.
614     * @param name  The name of the header, e.g "Set-Cookie".
615     * @param value The value for this header, e.g "text/html".
616     */
617    public synchronized void setRequestHeader(String name, String value) {
618        String[] mapValue = { name, value };
619        if (LOGV_ENABLED) {
620            Log.i(LOG_TAG, "setRequestHeader: " + name + " => " + value);
621        }
622        if (name.equalsIgnoreCase(KEY_CONTENT_LENGTH)) {
623            setContentLength(Long.parseLong(value));
624        } else {
625            mRequestHeaders.put(name.toLowerCase(), mapValue);
626        }
627    }
628
629    /**
630     * Returns the value associated with the given request header.
631     * @param name The name of the request header, non-null, case-insensitive.
632     * @return The value associated with the request header, or null if
633     *         not set, or error.
634     */
635    public synchronized String getRequestHeader(String name) {
636        String[] value = mRequestHeaders.get(name.toLowerCase());
637        if (value != null) {
638            return value[HEADERS_MAP_INDEX_VALUE];
639        } else {
640            return null;
641        }
642    }
643
644    private void applyRequestHeaders() {
645        if (mMethod == null)
646            return;
647        Iterator<String[]> it = mRequestHeaders.values().iterator();
648        while (it.hasNext()) {
649            // Set the key case-sensitive.
650            String[] entry = it.next();
651            if (LOGV_ENABLED) {
652                Log.i(LOG_TAG, "apply header " + entry[HEADERS_MAP_INDEX_KEY] +
653                    " => " + entry[HEADERS_MAP_INDEX_VALUE]);
654            }
655            mMethod.setHeader(entry[HEADERS_MAP_INDEX_KEY],
656                                     entry[HEADERS_MAP_INDEX_VALUE]);
657        }
658    }
659
660    /**
661     * Returns the value associated with the given response header.
662     * @param name The name of the response header, non-null, case-insensitive.
663     * @return The value associated with the response header, or null if
664     *         not set or error.
665     */
666    public synchronized String getResponseHeader(String name) {
667        if (mResponseHeaders != null) {
668            String[] value = mResponseHeaders.get(name.toLowerCase());
669            if (value != null) {
670                return value[HEADERS_MAP_INDEX_VALUE];
671            } else {
672                return null;
673            }
674        } else {
675            if (LOGV_ENABLED) {
676                Log.i(LOG_TAG, "getResponseHeader() called but "
677                      + "response not received");
678            }
679            return null;
680        }
681    }
682
683    /**
684     * Return all response headers, separated by CR-LF line endings, and
685     * ending with a trailing blank line. This mimics the format of the
686     * raw response header up to but not including the body.
687     * @return A string containing the entire response header.
688     */
689    public synchronized String getAllResponseHeaders() {
690        if (mResponseHeaders == null) {
691            if (LOGV_ENABLED) {
692                Log.i(LOG_TAG, "getAllResponseHeaders() called but "
693                      + "response not received");
694            }
695            return null;
696        }
697        StringBuilder result = new StringBuilder();
698        Iterator<String[]> it = mResponseHeaders.values().iterator();
699        while (it.hasNext()) {
700            String[] entry = it.next();
701            // Output the "key: value" lines.
702            result.append(entry[HEADERS_MAP_INDEX_KEY]);
703            result.append(": ");
704            result.append(entry[HEADERS_MAP_INDEX_VALUE]);
705            result.append(HTTP_LINE_ENDING);
706        }
707        result.append(HTTP_LINE_ENDING);
708        return result.toString();
709    }
710
711
712    /**
713     * Set a response header and associated value. The key is associated
714     * case-insensitively, but stored case-sensitively.
715     * @param name  Case sensitive request header key.
716     * @param value The associated value.
717     */
718    private void setResponseHeader(String name, String value) {
719        if (LOGV_ENABLED) {
720            Log.i(LOG_TAG, "Set response header " + name + ": " + value);
721        }
722        String mapValue[] = { name, value };
723        mResponseHeaders.put(name.toLowerCase(), mapValue);
724    }
725
726    // Cookie handling
727
728    /**
729     * Get the cookie for the given URL.
730     * @param url The fully qualified URL.
731     * @return A string containing the cookie for the URL if it exists,
732     *         or null if not.
733     */
734    public static String getCookieForUrl(String url) {
735        // Get the cookie for this URL, set as a header
736        return CookieManager.getInstance().getCookie(url);
737    }
738
739    /**
740     * Set the cookie for the given URL.
741     * @param url    The fully qualified URL.
742     * @param cookie The new cookie value.
743     * @return A string containing the cookie for the URL if it exists,
744     *         or null if not.
745     */
746    public static void setCookieForUrl(String url, String cookie) {
747        // Get the cookie for this URL, set as a header
748        CookieManager.getInstance().setCookie(url, cookie);
749    }
750
751    // Cache handling
752
753    /**
754     * Perform a request using LocalServer if possible. Initializes
755     * class members so that receive() will obtain data from the stream
756     * provided by the response.
757     * @param url The fully qualified URL to try in LocalServer.
758     * @return True if the url was found and is now setup to receive.
759     *         False if not found, with no side-effect.
760     */
761    public synchronized boolean useLocalServerResult(String url) {
762        UrlInterceptHandlerGears handler =
763            UrlInterceptHandlerGears.getInstance();
764        if (handler == null) {
765            return false;
766        }
767        UrlInterceptHandlerGears.ServiceResponse serviceResponse =
768            handler.getServiceResponse(url, mRequestHeaders);
769        if (serviceResponse == null) {
770            if (LOGV_ENABLED) {
771                Log.i(LOG_TAG, "No response in LocalServer");
772            }
773            return false;
774        }
775        // LocalServer will handle this URL. Initialize stream and
776        // response.
777        mBodyInputStream = serviceResponse.getInputStream();
778        mResponseLine = serviceResponse.getStatusLine();
779        mResponseHeaders = serviceResponse.getResponseHeaders();
780        if (LOGV_ENABLED) {
781            Log.i(LOG_TAG, "Got response from LocalServer: " + mResponseLine);
782        }
783        return true;
784    }
785
786    /**
787     * Perform a request using the cache result if present. Initializes
788     * class members so that receive() will obtain data from the cache.
789     * @param url The fully qualified URL to try in the cache.
790     * @return True is the url was found and is now setup to receive
791     *         from cache. False if not found, with no side-effect.
792     */
793    public synchronized boolean useCacheResult(String url) {
794        // Try the browser's cache. CacheManager wants a Map<String, String>.
795        Map<String, String> cacheRequestHeaders = new HashMap<String, String>();
796        Iterator<Map.Entry<String, String[]>> it =
797            mRequestHeaders.entrySet().iterator();
798        while (it.hasNext()) {
799            Map.Entry<String, String[]> entry = it.next();
800            cacheRequestHeaders.put(
801                entry.getKey(),
802                entry.getValue()[HEADERS_MAP_INDEX_VALUE]);
803        }
804        CacheResult mCacheResult =
805            CacheManager.getCacheFile(url, cacheRequestHeaders);
806        if (mCacheResult == null) {
807            if (LOGV_ENABLED) {
808                Log.i(LOG_TAG, "No CacheResult for " + url);
809            }
810            return false;
811        }
812        if (LOGV_ENABLED) {
813            Log.i(LOG_TAG, "Got CacheResult from browser cache");
814        }
815        // Check for expiry. -1 is "never", otherwise milliseconds since 1970.
816        // Can be compared to System.currentTimeMillis().
817        long expires = mCacheResult.getExpires();
818        if (expires >= 0 && System.currentTimeMillis() >= expires) {
819            if (LOGV_ENABLED) {
820                Log.i(LOG_TAG, "CacheResult expired "
821                    + (System.currentTimeMillis() - expires)
822                    + " milliseconds ago");
823            }
824            // Cache hit has expired. Do not return it.
825            return false;
826        }
827        // Setup the mBodyInputStream to come from the cache.
828        mBodyInputStream = mCacheResult.getInputStream();
829        if (mBodyInputStream == null) {
830            // Cache result may have gone away.
831            if (LOGV_ENABLED) {
832                Log.i(LOG_TAG, "No mBodyInputStream for CacheResult " + url);
833            }
834            return false;
835        }
836        // Cache hit. Parse headers.
837        synthesizeHeadersFromCacheResult(mCacheResult);
838        return true;
839    }
840
841    /**
842     * Take the limited set of headers in a CacheResult and synthesize
843     * response headers.
844     * @param cacheResult A CacheResult to populate mResponseHeaders with.
845     */
846    private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) {
847        int statusCode = cacheResult.getHttpStatusCode();
848        // The status message is informal, so we can greatly simplify it.
849        String statusMessage;
850        if (statusCode >= 200 && statusCode < 300) {
851            statusMessage = "OK";
852        } else if (statusCode >= 300 && statusCode < 400) {
853            statusMessage = "MOVED";
854        } else {
855            statusMessage = "UNAVAILABLE";
856        }
857        // Synthesize the response line.
858        mResponseLine = "HTTP/1.1 " + statusCode + " " + statusMessage;
859        if (LOGV_ENABLED) {
860            Log.i(LOG_TAG, "Synthesized " + mResponseLine);
861        }
862        // Synthesize the returned headers from cache.
863        mResponseHeaders = new HashMap<String, String[]>();
864        String contentLength = Long.toString(cacheResult.getContentLength());
865        setResponseHeader(KEY_CONTENT_LENGTH, contentLength);
866        long expires = cacheResult.getExpires();
867        if (expires >= 0) {
868            // "Expires" header is valid and finite. Milliseconds since 1970
869            // epoch, formatted as RFC-1123.
870            String expiresString = DateUtils.formatDate(new Date(expires));
871            setResponseHeader(KEY_EXPIRES, expiresString);
872        }
873        String lastModified = cacheResult.getLastModified();
874        if (lastModified != null) {
875            // Last modification time of the page. Passed end-to-end, but
876            // not used by us.
877            setResponseHeader(KEY_LAST_MODIFIED, lastModified);
878        }
879        String eTag = cacheResult.getETag();
880        if (eTag != null) {
881            // Entity tag. A kind of GUID to identify identical resources.
882            setResponseHeader(KEY_ETAG, eTag);
883        }
884        String location = cacheResult.getLocation();
885        if (location != null) {
886            // If valid, refers to the location of a redirect.
887            setResponseHeader(KEY_LOCATION, location);
888        }
889        String mimeType = cacheResult.getMimeType();
890        if (mimeType == null) {
891            // Use a safe default MIME type when none is
892            // specified. "text/plain" is safe to render in the browser
893            // window (even if large) and won't be intepreted as anything
894            // that would cause execution.
895            mimeType = DEFAULT_MIME_TYPE;
896        }
897        String encoding = cacheResult.getEncoding();
898        // Encoding may not be specified. No default.
899        String contentType = mimeType;
900        if (encoding != null) {
901            if (encoding.length() > 0) {
902                contentType += "; charset=" + encoding;
903            }
904        }
905        setResponseHeader(KEY_CONTENT_TYPE, contentType);
906    }
907
908    /**
909     * Create a CacheResult for this URL. This enables the repsonse body
910     * to be sent in calls to appendCacheResult().
911     * @param url          The fully qualified URL to add to the cache.
912     * @param responseCode The response code returned for the request, e.g 200.
913     * @param mimeType     The MIME type of the body, e.g "text/plain".
914     * @param encoding     The encoding, e.g "utf-8". Use "" for unknown.
915     */
916    public synchronized boolean createCacheResult(
917        String url, int responseCode, String mimeType, String encoding) {
918        if (LOGV_ENABLED) {
919            Log.i(LOG_TAG, "Making cache entry for " + url);
920        }
921        // Take the headers and parse them into a format needed by
922        // CacheManager.
923        Headers cacheHeaders = new Headers();
924        Iterator<Map.Entry<String, String[]>> it =
925            mResponseHeaders.entrySet().iterator();
926        while (it.hasNext()) {
927            Map.Entry<String, String[]> entry = it.next();
928            // Headers.parseHeader() expects lowercase keys.
929            String keyValue = entry.getKey() + ": "
930                + entry.getValue()[HEADERS_MAP_INDEX_VALUE];
931            CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length());
932            buffer.append(keyValue);
933            // Parse it into the header container.
934            cacheHeaders.parseHeader(buffer);
935        }
936        mCacheResult = CacheManager.createCacheFile(
937            url, responseCode, cacheHeaders, mimeType, true);
938        if (mCacheResult != null) {
939            if (LOGV_ENABLED) {
940                Log.i(LOG_TAG, "Saving into cache");
941            }
942            mCacheResult.setEncoding(encoding);
943            mCacheResultUrl = url;
944            return true;
945        } else {
946            if (LOGV_ENABLED) {
947                Log.i(LOG_TAG, "Couldn't create mCacheResult");
948            }
949            return false;
950        }
951    }
952
953    /**
954     * Add data from the response body to the CacheResult created with
955     * createCacheResult().
956     * @param data  A byte array of the next sequential bytes in the
957     *              response body.
958     * @param bytes The number of bytes to write from the start of
959     *              the array.
960     * @return True if all bytes successfully written, false on failure.
961     */
962    public synchronized boolean appendCacheResult(byte[] data, int bytes) {
963        if (mCacheResult == null) {
964            if (LOGV_ENABLED) {
965                Log.i(LOG_TAG, "appendCacheResult() called without a "
966                      + "CacheResult initialized");
967            }
968            return false;
969        }
970        try {
971            mCacheResult.getOutputStream().write(data, 0, bytes);
972        } catch (IOException ex) {
973            if (LOGV_ENABLED) {
974                Log.i(LOG_TAG, "Got IOException writing cache data: " + ex);
975            }
976            return false;
977        }
978        return true;
979    }
980
981    /**
982     * Save the completed CacheResult into the CacheManager. This must
983     * have been created first with createCacheResult().
984     * @return Returns true if the entry has been successfully saved.
985     */
986    public synchronized boolean saveCacheResult() {
987        if (mCacheResult == null || mCacheResultUrl == null) {
988            if (LOGV_ENABLED) {
989                Log.i(LOG_TAG, "Tried to save cache result but "
990                      + "createCacheResult not called");
991            }
992            return false;
993        }
994
995        if (LOGV_ENABLED) {
996            Log.i(LOG_TAG, "Saving cache result");
997        }
998        CacheManager.saveCacheFile(mCacheResultUrl, mCacheResult);
999        mCacheResult = null;
1000        mCacheResultUrl = null;
1001        return true;
1002    }
1003
1004    /**
1005     * Called by the main thread to interrupt the child thread.
1006     * We do not set mConnectionFailed here as we still need the
1007     * ability to receive a null packet for sendPostData().
1008     */
1009    public synchronized void abort() {
1010        if (LOGV_ENABLED) {
1011            Log.i(LOG_TAG, "ABORT CALLED");
1012        }
1013        if (mMethod != null) {
1014            mMethod.abort();
1015        }
1016    }
1017
1018   /**
1019     * Interrupt a blocking IO operation and wait for the
1020     * thread to complete.
1021     */
1022    public synchronized void interrupt() {
1023        if (LOGV_ENABLED) {
1024            Log.i(LOG_TAG, "INTERRUPT CALLED");
1025        }
1026        mConnectionFailedLock.lock();
1027        mConnectionFailed = true;
1028        mConnectionFailedLock.unlock();
1029        if (mMethod != null) {
1030            mMethod.abort();
1031        }
1032        if (mHttpThread != null) {
1033            waitUntilConnectionFinished();
1034        }
1035    }
1036
1037    /**
1038     * Receive the next sequential bytes of the response body after
1039     * successful connection. This will receive up to the size of the
1040     * provided byte array. If there is no body, this will return 0
1041     * bytes on the first call after connection.
1042     * @param  buf A pre-allocated byte array to receive data into.
1043     * @return The number of bytes from the start of the array which
1044     *         have been filled, 0 on EOF, or negative on error.
1045     */
1046    public synchronized int receive(byte[] buf) {
1047        if (mBodyInputStream == null) {
1048            // If this is the first call, setup the InputStream. This may
1049            // fail if there were headers, but no body returned by the
1050            // server.
1051            try {
1052                if (mResponse != null) {
1053                    HttpEntity entity = mResponse.getEntity();
1054                    mBodyInputStream = entity.getContent();
1055                }
1056            } catch (IOException inputException) {
1057                if (LOGV_ENABLED) {
1058                    Log.i(LOG_TAG, "Failed to connect InputStream: "
1059                          + inputException);
1060                }
1061                // Not unexpected. For example, 404 response return headers,
1062                // and sometimes a body with a detailed error.
1063            }
1064            if (mBodyInputStream == null) {
1065                // No error stream either. Treat as a 0 byte response.
1066                if (LOGV_ENABLED) {
1067                    Log.i(LOG_TAG, "No InputStream");
1068                }
1069                return 0; // EOF.
1070            }
1071        }
1072        int ret;
1073        try {
1074            int got = mBodyInputStream.read(buf);
1075            if (got > 0) {
1076                // Got some bytes, not EOF.
1077                ret = got;
1078            } else {
1079                // EOF.
1080                mBodyInputStream.close();
1081                ret = 0;
1082            }
1083        } catch (IOException e) {
1084            // An abort() interrupts us by calling close() on our stream.
1085            if (LOGV_ENABLED) {
1086                Log.i(LOG_TAG, "Got IOException in mBodyInputStream.read(): ", e);
1087            }
1088            ret = -1;
1089        }
1090        return ret;
1091    }
1092
1093    /**
1094     * For POST method requests, send a stream of data provided by the
1095     * native side in repeated callbacks.
1096     * We put the data in mBuffer, and wait until it is consumed
1097     * by the StreamEntity in the request thread.
1098     * @param data  A byte array containing the data to sent, or null
1099     *              if indicating EOF.
1100     * @param bytes The number of bytes from the start of the array to
1101     *              send, or 0 if indicating EOF.
1102     * @return True if all bytes were successfully sent, false on error.
1103     */
1104    public boolean sendPostData(byte[] data, int bytes) {
1105        mConnectionFailedLock.lock();
1106        if (mConnectionFailed) {
1107            mConnectionFailedLock.unlock();
1108            return false;
1109        }
1110        mConnectionFailedLock.unlock();
1111        if (mPostEntity == null) return false;
1112
1113        // We block until the outputstream is available
1114        // (or in case of connection error)
1115        if (!mPostEntity.isReady()) return false;
1116
1117        if (data == null && bytes == 0) {
1118            mBuffer.put(null);
1119        } else {
1120            mBuffer.put(new DataPacket(data, bytes));
1121        }
1122        mSignal.waitUntilPacketConsumed();
1123
1124        mConnectionFailedLock.lock();
1125        if (mConnectionFailed) {
1126            Log.e(LOG_TAG, "failure");
1127            mConnectionFailedLock.unlock();
1128            return false;
1129        }
1130        mConnectionFailedLock.unlock();
1131        return true;
1132    }
1133
1134}
1135