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