LoadListener.java revision f6a6cff234e164874233de3618ab98b4fb00efb7
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.webkit; 18 19import android.content.Context; 20import android.net.WebAddress; 21import android.net.ParseException; 22import android.net.http.EventHandler; 23import android.net.http.Headers; 24import android.net.http.HttpAuthHeader; 25import android.net.http.RequestHandle; 26import android.net.http.SslCertificate; 27import android.net.http.SslError; 28 29import android.os.Handler; 30import android.os.Message; 31import android.security.CertTool; 32import android.util.Log; 33import android.webkit.CacheManager.CacheResult; 34import android.widget.Toast; 35 36import com.android.internal.R; 37 38import java.io.IOException; 39import java.util.ArrayList; 40import java.util.HashMap; 41import java.util.Map; 42import java.util.Vector; 43import java.util.regex.Pattern; 44import java.util.regex.Matcher; 45 46class LoadListener extends Handler implements EventHandler { 47 48 private static final String LOGTAG = "webkit"; 49 50 // Messages used internally to communicate state between the 51 // Network thread and the WebCore thread. 52 private static final int MSG_CONTENT_HEADERS = 100; 53 private static final int MSG_CONTENT_DATA = 110; 54 private static final int MSG_CONTENT_FINISHED = 120; 55 private static final int MSG_CONTENT_ERROR = 130; 56 private static final int MSG_LOCATION_CHANGED = 140; 57 private static final int MSG_LOCATION_CHANGED_REQUEST = 150; 58 private static final int MSG_STATUS = 160; 59 private static final int MSG_SSL_CERTIFICATE = 170; 60 private static final int MSG_SSL_ERROR = 180; 61 62 // Standard HTTP status codes in a more representative format 63 private static final int HTTP_OK = 200; 64 private static final int HTTP_MOVED_PERMANENTLY = 301; 65 private static final int HTTP_FOUND = 302; 66 private static final int HTTP_SEE_OTHER = 303; 67 private static final int HTTP_NOT_MODIFIED = 304; 68 private static final int HTTP_TEMPORARY_REDIRECT = 307; 69 private static final int HTTP_AUTH = 401; 70 private static final int HTTP_NOT_FOUND = 404; 71 private static final int HTTP_PROXY_AUTH = 407; 72 73 private static final String CERT_MIMETYPE = "application/x-x509-ca-cert"; 74 75 private static int sNativeLoaderCount; 76 77 private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(8192); 78 79 private String mUrl; 80 private WebAddress mUri; 81 private boolean mPermanent; 82 private String mOriginalUrl; 83 private Context mContext; 84 private BrowserFrame mBrowserFrame; 85 private int mNativeLoader; 86 private String mMimeType; 87 private String mEncoding; 88 private String mTransferEncoding; 89 private int mStatusCode; 90 private String mStatusText; 91 public long mContentLength; // Content length of the incoming data 92 private boolean mCancelled; // The request has been cancelled. 93 private boolean mAuthFailed; // indicates that the prev. auth failed 94 private CacheLoader mCacheLoader; 95 private CacheManager.CacheResult mCacheResult; 96 private HttpAuthHeader mAuthHeader; 97 private int mErrorID = OK; 98 private String mErrorDescription; 99 private SslError mSslError; 100 private RequestHandle mRequestHandle; 101 102 // Request data. It is only valid when we are doing a load from the 103 // cache. It is needed if the cache returns a redirect 104 private String mMethod; 105 private Map<String, String> mRequestHeaders; 106 private byte[] mPostData; 107 private boolean mIsHighPriority; 108 // Flag to indicate that this load is synchronous. 109 private boolean mSynchronous; 110 private Vector<Message> mMessageQueue; 111 112 // Does this loader correspond to the main-frame top-level page? 113 private boolean mIsMainPageLoader; 114 115 private Headers mHeaders; 116 117 // ========================================================================= 118 // Public functions 119 // ========================================================================= 120 121 public static LoadListener getLoadListener( 122 Context context, BrowserFrame frame, String url, 123 int nativeLoader, boolean synchronous, boolean isMainPageLoader) { 124 125 sNativeLoaderCount += 1; 126 return new LoadListener( 127 context, frame, url, nativeLoader, synchronous, isMainPageLoader); 128 } 129 130 public static int getNativeLoaderCount() { 131 return sNativeLoaderCount; 132 } 133 134 LoadListener(Context context, BrowserFrame frame, String url, 135 int nativeLoader, boolean synchronous, boolean isMainPageLoader) { 136 if (DebugFlags.LOAD_LISTENER) { 137 Log.v(LOGTAG, "LoadListener constructor url=" + url); 138 } 139 mContext = context; 140 mBrowserFrame = frame; 141 setUrl(url); 142 mNativeLoader = nativeLoader; 143 mSynchronous = synchronous; 144 if (synchronous) { 145 mMessageQueue = new Vector<Message>(); 146 } 147 mIsMainPageLoader = isMainPageLoader; 148 } 149 150 /** 151 * We keep a count of refs to the nativeLoader so we do not create 152 * so many LoadListeners that the GREFs blow up 153 */ 154 private void clearNativeLoader() { 155 sNativeLoaderCount -= 1; 156 mNativeLoader = 0; 157 } 158 159 /* 160 * This message handler is to facilitate communication between the network 161 * thread and the browser thread. 162 */ 163 public void handleMessage(Message msg) { 164 switch (msg.what) { 165 case MSG_CONTENT_HEADERS: 166 /* 167 * This message is sent when the LoadListener has headers 168 * available. The headers are sent onto WebCore to see what we 169 * should do with them. 170 */ 171 handleHeaders((Headers) msg.obj); 172 break; 173 174 case MSG_CONTENT_DATA: 175 /* 176 * This message is sent when the LoadListener has data available 177 * in it's data buffer. This data buffer could be filled from a 178 * file (this thread) or from http (Network thread). 179 */ 180 if (mNativeLoader != 0 && !ignoreCallbacks()) { 181 commitLoad(); 182 } 183 break; 184 185 case MSG_CONTENT_FINISHED: 186 /* 187 * This message is sent when the LoadListener knows that the 188 * load is finished. This message is not sent in the case of an 189 * error. 190 * 191 */ 192 handleEndData(); 193 break; 194 195 case MSG_CONTENT_ERROR: 196 /* 197 * This message is sent when a load error has occured. The 198 * LoadListener will clean itself up. 199 */ 200 handleError(msg.arg1, (String) msg.obj); 201 break; 202 203 case MSG_LOCATION_CHANGED: 204 /* 205 * This message is sent from LoadListener.endData to inform the 206 * browser activity that the location of the top level page 207 * changed. 208 */ 209 doRedirect(); 210 break; 211 212 case MSG_LOCATION_CHANGED_REQUEST: 213 /* 214 * This message is sent from endData on receipt of a 307 215 * Temporary Redirect in response to a POST -- the user must 216 * confirm whether to continue loading. If the user says Yes, 217 * we simply call MSG_LOCATION_CHANGED. If the user says No, 218 * we call MSG_CONTENT_FINISHED. 219 */ 220 Message contMsg = obtainMessage(MSG_LOCATION_CHANGED); 221 Message stopMsg = obtainMessage(MSG_CONTENT_FINISHED); 222 mBrowserFrame.getCallbackProxy().onFormResubmission( 223 stopMsg, contMsg); 224 break; 225 226 case MSG_STATUS: 227 /* 228 * This message is sent from the network thread when the http 229 * stack has received the status response from the server. 230 */ 231 HashMap status = (HashMap) msg.obj; 232 handleStatus(((Integer) status.get("major")).intValue(), 233 ((Integer) status.get("minor")).intValue(), 234 ((Integer) status.get("code")).intValue(), 235 (String) status.get("reason")); 236 break; 237 238 case MSG_SSL_CERTIFICATE: 239 /* 240 * This message is sent when the network thread receives a ssl 241 * certificate. 242 */ 243 handleCertificate((SslCertificate) msg.obj); 244 break; 245 246 case MSG_SSL_ERROR: 247 /* 248 * This message is sent when the network thread encounters a 249 * ssl error. 250 */ 251 handleSslError((SslError) msg.obj); 252 break; 253 } 254 } 255 256 /** 257 * @return The loader's BrowserFrame. 258 */ 259 BrowserFrame getFrame() { 260 return mBrowserFrame; 261 } 262 263 Context getContext() { 264 return mContext; 265 } 266 267 /* package */ boolean isSynchronous() { 268 return mSynchronous; 269 } 270 271 /** 272 * @return True iff the load has been cancelled 273 */ 274 public boolean cancelled() { 275 return mCancelled; 276 } 277 278 /** 279 * Parse the headers sent from the server. 280 * @param headers gives up the HeaderGroup 281 * IMPORTANT: as this is called from network thread, can't call native 282 * directly 283 */ 284 public void headers(Headers headers) { 285 if (DebugFlags.LOAD_LISTENER) Log.v(LOGTAG, "LoadListener.headers"); 286 sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS, headers)); 287 } 288 289 // Does the header parsing work on the WebCore thread. 290 private void handleHeaders(Headers headers) { 291 if (mCancelled) return; 292 mHeaders = headers; 293 294 ArrayList<String> cookies = headers.getSetCookie(); 295 for (int i = 0; i < cookies.size(); ++i) { 296 CookieManager.getInstance().setCookie(mUri, cookies.get(i)); 297 } 298 299 long contentLength = headers.getContentLength(); 300 if (contentLength != Headers.NO_CONTENT_LENGTH) { 301 mContentLength = contentLength; 302 } else { 303 mContentLength = 0; 304 } 305 306 String contentType = headers.getContentType(); 307 if (contentType != null) { 308 parseContentTypeHeader(contentType); 309 310 // If we have one of "generic" MIME types, try to deduce 311 // the right MIME type from the file extension (if any): 312 if (mMimeType.equals("text/plain") || 313 mMimeType.equals("application/octet-stream")) { 314 315 String newMimeType = guessMimeTypeFromExtension(); 316 if (newMimeType != null) { 317 mMimeType = newMimeType; 318 } 319 } else if (mMimeType.equals("text/vnd.wap.wml")) { 320 // As we don't support wml, render it as plain text 321 mMimeType = "text/plain"; 322 } else { 323 // It seems that xhtml+xml and vnd.wap.xhtml+xml mime 324 // subtypes are used interchangeably. So treat them the same. 325 if (mMimeType.equals("application/vnd.wap.xhtml+xml")) { 326 mMimeType = "application/xhtml+xml"; 327 } 328 } 329 } else { 330 /* Often when servers respond with 304 Not Modified or a 331 Redirect, then they don't specify a MIMEType. When this 332 occurs, the function below is called. In the case of 333 304 Not Modified, the cached headers are used rather 334 than the headers that are returned from the server. */ 335 guessMimeType(); 336 } 337 338 // is it an authentication request? 339 boolean mustAuthenticate = (mStatusCode == HTTP_AUTH || 340 mStatusCode == HTTP_PROXY_AUTH); 341 // is it a proxy authentication request? 342 boolean isProxyAuthRequest = (mStatusCode == HTTP_PROXY_AUTH); 343 // is this authentication request due to a failed attempt to 344 // authenticate ealier? 345 mAuthFailed = false; 346 347 // if we tried to authenticate ourselves last time 348 if (mAuthHeader != null) { 349 // we failed, if we must to authenticate again now and 350 // we have a proxy-ness match 351 mAuthFailed = (mustAuthenticate && 352 isProxyAuthRequest == mAuthHeader.isProxy()); 353 354 // if we did NOT fail and last authentication request was a 355 // proxy-authentication request 356 if (!mAuthFailed && mAuthHeader.isProxy()) { 357 Network network = Network.getInstance(mContext); 358 // if we have a valid proxy set 359 if (network.isValidProxySet()) { 360 /* The proxy credentials can be read in the WebCore thread 361 */ 362 synchronized (network) { 363 // save authentication credentials for pre-emptive proxy 364 // authentication 365 network.setProxyUsername(mAuthHeader.getUsername()); 366 network.setProxyPassword(mAuthHeader.getPassword()); 367 } 368 } 369 } 370 } 371 372 // it is only here that we can reset the last mAuthHeader object 373 // (if existed) and start a new one!!! 374 mAuthHeader = null; 375 if (mustAuthenticate) { 376 if (mStatusCode == HTTP_AUTH) { 377 mAuthHeader = parseAuthHeader( 378 headers.getWwwAuthenticate()); 379 } else { 380 mAuthHeader = parseAuthHeader( 381 headers.getProxyAuthenticate()); 382 // if successfully parsed the header 383 if (mAuthHeader != null) { 384 // mark the auth-header object as a proxy 385 mAuthHeader.setProxy(); 386 } 387 } 388 } 389 390 // Only create a cache file if the server has responded positively. 391 if ((mStatusCode == HTTP_OK || 392 mStatusCode == HTTP_FOUND || 393 mStatusCode == HTTP_MOVED_PERMANENTLY || 394 mStatusCode == HTTP_TEMPORARY_REDIRECT) && 395 mNativeLoader != 0) { 396 // Content arriving from a StreamLoader (eg File, Cache or Data) 397 // will not be cached as they have the header: 398 // cache-control: no-store 399 mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, 400 headers, mMimeType, false); 401 if (mCacheResult != null) { 402 mCacheResult.encoding = mEncoding; 403 } 404 } 405 commitHeadersCheckRedirect(); 406 } 407 408 /** 409 * @return True iff this loader is in the proxy-authenticate state. 410 */ 411 boolean proxyAuthenticate() { 412 if (mAuthHeader != null) { 413 return mAuthHeader.isProxy(); 414 } 415 416 return false; 417 } 418 419 /** 420 * Report the status of the response. 421 * TODO: Comments about each parameter. 422 * IMPORTANT: as this is called from network thread, can't call native 423 * directly 424 */ 425 public void status(int majorVersion, int minorVersion, 426 int code, /* Status-Code value */ String reasonPhrase) { 427 if (DebugFlags.LOAD_LISTENER) { 428 Log.v(LOGTAG, "LoadListener: from: " + mUrl 429 + " major: " + majorVersion 430 + " minor: " + minorVersion 431 + " code: " + code 432 + " reason: " + reasonPhrase); 433 } 434 HashMap status = new HashMap(); 435 status.put("major", majorVersion); 436 status.put("minor", minorVersion); 437 status.put("code", code); 438 status.put("reason", reasonPhrase); 439 // New status means new data. Clear the old. 440 mDataBuilder.clear(); 441 mMimeType = ""; 442 mEncoding = ""; 443 mTransferEncoding = ""; 444 sendMessageInternal(obtainMessage(MSG_STATUS, status)); 445 } 446 447 // Handle the status callback on the WebCore thread. 448 private void handleStatus(int major, int minor, int code, String reason) { 449 if (mCancelled) return; 450 451 mStatusCode = code; 452 mStatusText = reason; 453 mPermanent = false; 454 } 455 456 /** 457 * Implementation of certificate handler for EventHandler. 458 * Called every time a resource is loaded via a secure 459 * connection. In this context, can be called multiple 460 * times if we have redirects 461 * @param certificate The SSL certifcate 462 * IMPORTANT: as this is called from network thread, can't call native 463 * directly 464 */ 465 public void certificate(SslCertificate certificate) { 466 sendMessageInternal(obtainMessage(MSG_SSL_CERTIFICATE, certificate)); 467 } 468 469 // Handle the certificate on the WebCore thread. 470 private void handleCertificate(SslCertificate certificate) { 471 // if this is the top-most main-frame page loader 472 if (mIsMainPageLoader) { 473 // update the browser frame (ie, the main frame) 474 mBrowserFrame.certificate(certificate); 475 } 476 } 477 478 /** 479 * Implementation of error handler for EventHandler. 480 * Subclasses should call this method to have error fields set. 481 * @param id The error id described by EventHandler. 482 * @param description A string description of the error. 483 * IMPORTANT: as this is called from network thread, can't call native 484 * directly 485 */ 486 public void error(int id, String description) { 487 if (DebugFlags.LOAD_LISTENER) { 488 Log.v(LOGTAG, "LoadListener.error url:" + 489 url() + " id:" + id + " description:" + description); 490 } 491 sendMessageInternal(obtainMessage(MSG_CONTENT_ERROR, id, 0, description)); 492 } 493 494 // Handle the error on the WebCore thread. 495 private void handleError(int id, String description) { 496 mErrorID = id; 497 mErrorDescription = description; 498 detachRequestHandle(); 499 notifyError(); 500 tearDown(); 501 } 502 503 /** 504 * Add data to the internal collection of data. This function is used by 505 * the data: scheme, about: scheme and http/https schemes. 506 * @param data A byte array containing the content. 507 * @param length The length of data. 508 * IMPORTANT: as this is called from network thread, can't call native 509 * directly 510 * XXX: Unlike the other network thread methods, this method can do the 511 * work of decoding the data and appending it to the data builder because 512 * mDataBuilder is a thread-safe structure. 513 */ 514 public void data(byte[] data, int length) { 515 if (DebugFlags.LOAD_LISTENER) { 516 Log.v(LOGTAG, "LoadListener.data(): url: " + url()); 517 } 518 519 // Synchronize on mData because commitLoad may write mData to WebCore 520 // and we don't want to replace mData or mDataLength at the same time 521 // as a write. 522 boolean sendMessage = false; 523 synchronized (mDataBuilder) { 524 sendMessage = mDataBuilder.isEmpty(); 525 mDataBuilder.append(data, 0, length); 526 } 527 if (sendMessage) { 528 // Send a message whenever data comes in after a write to WebCore 529 sendMessageInternal(obtainMessage(MSG_CONTENT_DATA)); 530 } 531 } 532 533 /** 534 * Event handler's endData call. Send a message to the handler notifying 535 * them that the data has finished. 536 * IMPORTANT: as this is called from network thread, can't call native 537 * directly 538 */ 539 public void endData() { 540 if (DebugFlags.LOAD_LISTENER) { 541 Log.v(LOGTAG, "LoadListener.endData(): url: " + url()); 542 } 543 sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); 544 } 545 546 // Handle the end of data. 547 private void handleEndData() { 548 if (mCancelled) return; 549 550 switch (mStatusCode) { 551 case HTTP_MOVED_PERMANENTLY: 552 // 301 - permanent redirect 553 mPermanent = true; 554 case HTTP_FOUND: 555 case HTTP_SEE_OTHER: 556 case HTTP_TEMPORARY_REDIRECT: 557 // 301, 302, 303, and 307 - redirect 558 if (mStatusCode == HTTP_TEMPORARY_REDIRECT) { 559 if (mRequestHandle != null && 560 mRequestHandle.getMethod().equals("POST")) { 561 sendMessageInternal(obtainMessage( 562 MSG_LOCATION_CHANGED_REQUEST)); 563 } else if (mMethod != null && mMethod.equals("POST")) { 564 sendMessageInternal(obtainMessage( 565 MSG_LOCATION_CHANGED_REQUEST)); 566 } else { 567 sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); 568 } 569 } else { 570 sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); 571 } 572 return; 573 574 case HTTP_AUTH: 575 case HTTP_PROXY_AUTH: 576 // According to rfc2616, the response for HTTP_AUTH must include 577 // WWW-Authenticate header field and the response for 578 // HTTP_PROXY_AUTH must include Proxy-Authenticate header field. 579 if (mAuthHeader != null && 580 (Network.getInstance(mContext).isValidProxySet() || 581 !mAuthHeader.isProxy())) { 582 Network.getInstance(mContext).handleAuthRequest(this); 583 return; 584 } 585 break; // use default 586 587 case HTTP_NOT_MODIFIED: 588 // Server could send back NOT_MODIFIED even if we didn't 589 // ask for it, so make sure we have a valid CacheLoader 590 // before calling it. 591 if (mCacheLoader != null) { 592 mCacheLoader.load(); 593 if (DebugFlags.LOAD_LISTENER) { 594 Log.v(LOGTAG, "LoadListener cache load url=" + url()); 595 } 596 return; 597 } 598 break; // use default 599 600 case HTTP_NOT_FOUND: 601 // Not an error, the server can send back content. 602 default: 603 break; 604 } 605 detachRequestHandle(); 606 tearDown(); 607 } 608 609 /* This method is called from CacheLoader when the initial request is 610 * serviced by the Cache. */ 611 /* package */ void setCacheLoader(CacheLoader c) { 612 mCacheLoader = c; 613 } 614 615 /** 616 * Check the cache for the current URL, and load it if it is valid. 617 * 618 * @param headers for the request 619 * @return true if cached response is used. 620 */ 621 boolean checkCache(Map<String, String> headers) { 622 // Get the cache file name for the current URL 623 CacheResult result = CacheManager.getCacheFile(url(), 624 headers); 625 626 // Go ahead and set the cache loader to null in case the result is 627 // null. 628 mCacheLoader = null; 629 630 if (result != null) { 631 // The contents of the cache may need to be revalidated so just 632 // remember the cache loader in the case that the server responds 633 // positively to the cached content. This is also used to detect if 634 // a redirect came from the cache. 635 mCacheLoader = new CacheLoader(this, result); 636 637 // If I got a cachedUrl and the revalidation header was not 638 // added, then the cached content valid, we should use it. 639 if (!headers.containsKey( 640 CacheManager.HEADER_KEY_IFNONEMATCH) && 641 !headers.containsKey( 642 CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) { 643 if (DebugFlags.LOAD_LISTENER) { 644 Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " + 645 "and usable: " + url()); 646 } 647 // Load the cached file 648 mCacheLoader.load(); 649 return true; 650 } 651 } 652 return false; 653 } 654 655 /** 656 * SSL certificate error callback. Handles SSL error(s) on the way up 657 * to the user. 658 * IMPORTANT: as this is called from network thread, can't call native 659 * directly 660 */ 661 public void handleSslErrorRequest(SslError error) { 662 if (DebugFlags.LOAD_LISTENER) { 663 Log.v(LOGTAG, 664 "LoadListener.handleSslErrorRequest(): url:" + url() + 665 " primary error: " + error.getPrimaryError() + 666 " certificate: " + error.getCertificate()); 667 } 668 sendMessageInternal(obtainMessage(MSG_SSL_ERROR, error)); 669 } 670 671 // Handle the ssl error on the WebCore thread. 672 private void handleSslError(SslError error) { 673 if (!mCancelled) { 674 mSslError = error; 675 Network.getInstance(mContext).handleSslErrorRequest(this); 676 } 677 } 678 679 /** 680 * @return HTTP authentication realm or null if none. 681 */ 682 String realm() { 683 if (mAuthHeader == null) { 684 return null; 685 } else { 686 return mAuthHeader.getRealm(); 687 } 688 } 689 690 /** 691 * Returns true iff an HTTP authentication problem has 692 * occured (credentials invalid). 693 */ 694 boolean authCredentialsInvalid() { 695 // if it is digest and the nonce is stale, we just 696 // resubmit with a new nonce 697 return (mAuthFailed && 698 !(mAuthHeader.isDigest() && mAuthHeader.getStale())); 699 } 700 701 /** 702 * @return The last SSL error or null if there is none 703 */ 704 SslError sslError() { 705 return mSslError; 706 } 707 708 /** 709 * Handles SSL error(s) on the way down from the user 710 * (the user has already provided their feedback). 711 */ 712 void handleSslErrorResponse(boolean proceed) { 713 if (mRequestHandle != null) { 714 mRequestHandle.handleSslErrorResponse(proceed); 715 } 716 if (!proceed) { 717 // Commit whatever data we have and tear down the loader. 718 commitLoad(); 719 tearDown(); 720 } 721 } 722 723 /** 724 * Uses user-supplied credentials to restart a request. If the credentials 725 * are null, cancel the request. 726 */ 727 void handleAuthResponse(String username, String password) { 728 if (DebugFlags.LOAD_LISTENER) { 729 Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl 730 + " username: " + username 731 + " password: " + password); 732 } 733 734 // create and queue an authentication-response 735 if (username != null && password != null) { 736 if (mAuthHeader != null && mRequestHandle != null) { 737 mAuthHeader.setUsername(username); 738 mAuthHeader.setPassword(password); 739 740 int scheme = mAuthHeader.getScheme(); 741 if (scheme == HttpAuthHeader.BASIC) { 742 // create a basic response 743 boolean isProxy = mAuthHeader.isProxy(); 744 745 mRequestHandle.setupBasicAuthResponse(isProxy, 746 username, password); 747 } else { 748 if (scheme == HttpAuthHeader.DIGEST) { 749 // create a digest response 750 boolean isProxy = mAuthHeader.isProxy(); 751 752 String realm = mAuthHeader.getRealm(); 753 String nonce = mAuthHeader.getNonce(); 754 String qop = mAuthHeader.getQop(); 755 String algorithm = mAuthHeader.getAlgorithm(); 756 String opaque = mAuthHeader.getOpaque(); 757 758 mRequestHandle.setupDigestAuthResponse 759 (isProxy, username, password, realm, 760 nonce, qop, algorithm, opaque); 761 } 762 } 763 } 764 } else { 765 // Commit whatever data we have and tear down the loader. 766 commitLoad(); 767 tearDown(); 768 } 769 } 770 771 /** 772 * This is called when a request can be satisfied by the cache, however, 773 * the cache result could be a redirect. In this case we need to issue 774 * the network request. 775 * @param method 776 * @param headers 777 * @param postData 778 * @param isHighPriority 779 */ 780 void setRequestData(String method, Map<String, String> headers, 781 byte[] postData, boolean isHighPriority) { 782 mMethod = method; 783 mRequestHeaders = headers; 784 mPostData = postData; 785 mIsHighPriority = isHighPriority; 786 } 787 788 /** 789 * @return The current URL associated with this load. 790 */ 791 String url() { 792 return mUrl; 793 } 794 795 /** 796 * @return The current WebAddress associated with this load. 797 */ 798 WebAddress getWebAddress() { 799 return mUri; 800 } 801 802 /** 803 * @return URL hostname (current URL). 804 */ 805 String host() { 806 if (mUri != null) { 807 return mUri.mHost; 808 } 809 810 return null; 811 } 812 813 /** 814 * @return The original URL associated with this load. 815 */ 816 String originalUrl() { 817 if (mOriginalUrl != null) { 818 return mOriginalUrl; 819 } else { 820 return mUrl; 821 } 822 } 823 824 void attachRequestHandle(RequestHandle requestHandle) { 825 if (DebugFlags.LOAD_LISTENER) { 826 Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + 827 "requestHandle: " + requestHandle); 828 } 829 mRequestHandle = requestHandle; 830 } 831 832 void detachRequestHandle() { 833 if (DebugFlags.LOAD_LISTENER) { 834 Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " + 835 "requestHandle: " + mRequestHandle); 836 } 837 mRequestHandle = null; 838 } 839 840 /* 841 * This function is called from native WebCore code to 842 * notify this LoadListener that the content it is currently 843 * downloading should be saved to a file and not sent to 844 * WebCore. 845 */ 846 void downloadFile() { 847 // Setting the Cache Result to null ensures that this 848 // content is not added to the cache 849 mCacheResult = null; 850 851 // Inform the client that they should download a file 852 mBrowserFrame.getCallbackProxy().onDownloadStart(url(), 853 mBrowserFrame.getUserAgentString(), 854 mHeaders.getContentDisposition(), 855 mMimeType, mContentLength); 856 857 // Cancel the download. We need to stop the http load. 858 // The native loader object will get cleared by the call to 859 // cancel() but will also be cleared on the WebCore side 860 // when this function returns. 861 cancel(); 862 } 863 864 /* 865 * This function is called from native WebCore code to 866 * find out if the given URL is in the cache, and if it can 867 * be used. This is just for forward/back navigation to a POST 868 * URL. 869 */ 870 static boolean willLoadFromCache(String url) { 871 boolean inCache = CacheManager.getCacheFile(url, null) != null; 872 if (DebugFlags.LOAD_LISTENER) { 873 Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + 874 inCache); 875 } 876 return inCache; 877 } 878 879 /* 880 * Reset the cancel flag. This is used when we are resuming a stopped 881 * download. To suspend a download, we cancel it. It can also be cancelled 882 * when it has run out of disk space. In this situation, the download 883 * can be resumed. 884 */ 885 void resetCancel() { 886 mCancelled = false; 887 } 888 889 String mimeType() { 890 return mMimeType; 891 } 892 893 String transferEncoding() { 894 return mTransferEncoding; 895 } 896 897 /* 898 * Return the size of the content being downloaded. This represents the 899 * full content size, even under the situation where the download has been 900 * resumed after interruption. 901 * 902 * @ return full content size 903 */ 904 long contentLength() { 905 return mContentLength; 906 } 907 908 // Commit the headers if the status code is not a redirect. 909 private void commitHeadersCheckRedirect() { 910 if (mCancelled) return; 911 912 // do not call webcore if it is redirect. According to the code in 913 // InspectorController::willSendRequest(), the response is only updated 914 // when it is not redirect. 915 if ((mStatusCode >= 301 && mStatusCode <= 303) || mStatusCode == 307) { 916 return; 917 } 918 919 commitHeaders(); 920 } 921 922 // This commits the headers without checking the response status code. 923 private void commitHeaders() { 924 if (mIsMainPageLoader && CERT_MIMETYPE.equals(mMimeType)) { 925 // In the case of downloading certificate, we will save it to the 926 // Keystore in commitLoad. Do not call webcore. 927 return; 928 } 929 930 // Commit the headers to WebCore 931 int nativeResponse = createNativeResponse(); 932 // The native code deletes the native response object. 933 nativeReceivedResponse(nativeResponse); 934 } 935 936 /** 937 * Create a WebCore response object so that it can be used by 938 * nativeReceivedResponse or nativeRedirectedToUrl 939 * @return native response pointer 940 */ 941 private int createNativeResponse() { 942 // If WebCore sends if-modified-since, mCacheLoader is null. If 943 // CacheManager sends it, mCacheLoader is not null. In this case, if the 944 // server responds with a 304, then we treat it like it was a 200 code 945 // and proceed with loading the file from the cache. 946 int statusCode = (mStatusCode == HTTP_NOT_MODIFIED && 947 mCacheLoader != null) ? HTTP_OK : mStatusCode; 948 // pass content-type content-length and content-encoding 949 final int nativeResponse = nativeCreateResponse( 950 mUrl, statusCode, mStatusText, 951 mMimeType, mContentLength, mEncoding); 952 if (mHeaders != null) { 953 mHeaders.getHeaders(new Headers.HeaderCallback() { 954 public void header(String name, String value) { 955 nativeSetResponseHeader(nativeResponse, name, value); 956 } 957 }); 958 } 959 return nativeResponse; 960 } 961 962 /** 963 * Commit the load. It should be ok to call repeatedly but only before 964 * tearDown is called. 965 */ 966 private void commitLoad() { 967 if (mCancelled) return; 968 969 if (mIsMainPageLoader && CERT_MIMETYPE.equals(mMimeType)) { 970 // In the case of downloading certificate, we will save it to the 971 // Keystore and stop the current loading so that it will not 972 // generate a new history page 973 byte[] cert = new byte[mDataBuilder.getByteSize()]; 974 int position = 0; 975 ByteArrayBuilder.Chunk c; 976 while (true) { 977 c = mDataBuilder.getFirstChunk(); 978 if (c == null) break; 979 980 if (c.mLength != 0) { 981 System.arraycopy(c.mArray, 0, cert, position, c.mLength); 982 position += c.mLength; 983 } 984 mDataBuilder.releaseChunk(c); 985 } 986 CertTool.getInstance().addCertificate(cert, mContext); 987 Toast.makeText(mContext, R.string.certificateSaved, 988 Toast.LENGTH_SHORT).show(); 989 mBrowserFrame.stopLoading(); 990 return; 991 } 992 993 // Give the data to WebKit now 994 PerfChecker checker = new PerfChecker(); 995 ByteArrayBuilder.Chunk c; 996 while (true) { 997 c = mDataBuilder.getFirstChunk(); 998 if (c == null) break; 999 1000 if (c.mLength != 0) { 1001 if (mCacheResult != null) { 1002 try { 1003 mCacheResult.outStream.write(c.mArray, 0, c.mLength); 1004 } catch (IOException e) { 1005 mCacheResult = null; 1006 } 1007 } 1008 nativeAddData(c.mArray, c.mLength); 1009 } 1010 mDataBuilder.releaseChunk(c); 1011 checker.responseAlert("res nativeAddData"); 1012 } 1013 } 1014 1015 /** 1016 * Tear down the load. Subclasses should clean up any mess because of 1017 * cancellation or errors during the load. 1018 */ 1019 void tearDown() { 1020 if (mCacheResult != null) { 1021 if (getErrorID() == OK) { 1022 CacheManager.saveCacheFile(mUrl, mCacheResult); 1023 } 1024 1025 // we need to reset mCacheResult to be null 1026 // resource loader's tearDown will call into WebCore's 1027 // nativeFinish, which in turn calls loader.cancel(). 1028 // If we don't reset mCacheFile, the file will be deleted. 1029 mCacheResult = null; 1030 } 1031 if (mNativeLoader != 0) { 1032 PerfChecker checker = new PerfChecker(); 1033 nativeFinished(); 1034 checker.responseAlert("res nativeFinished"); 1035 clearNativeLoader(); 1036 } 1037 } 1038 1039 /** 1040 * Helper for getting the error ID. 1041 * @return errorID. 1042 */ 1043 private int getErrorID() { 1044 return mErrorID; 1045 } 1046 1047 /** 1048 * Return the error description. 1049 * @return errorDescription. 1050 */ 1051 private String getErrorDescription() { 1052 return mErrorDescription; 1053 } 1054 1055 /** 1056 * Notify the loader we encountered an error. 1057 */ 1058 void notifyError() { 1059 if (mNativeLoader != 0) { 1060 String description = getErrorDescription(); 1061 if (description == null) description = ""; 1062 nativeError(getErrorID(), description, url()); 1063 clearNativeLoader(); 1064 } 1065 } 1066 1067 /** 1068 * Cancel a request. 1069 * FIXME: This will only work if the request has yet to be handled. This 1070 * is in no way guarenteed if requests are served in a separate thread. 1071 * It also causes major problems if cancel is called during an 1072 * EventHandler's method call. 1073 */ 1074 public void cancel() { 1075 if (DebugFlags.LOAD_LISTENER) { 1076 if (mRequestHandle == null) { 1077 Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle"); 1078 } else { 1079 Log.v(LOGTAG, "LoadListener.cancel()"); 1080 } 1081 } 1082 if (mRequestHandle != null) { 1083 mRequestHandle.cancel(); 1084 mRequestHandle = null; 1085 } 1086 1087 mCacheResult = null; 1088 mCancelled = true; 1089 1090 clearNativeLoader(); 1091 } 1092 1093 // This count is transferred from RequestHandle to LoadListener when 1094 // loading from the cache so that we can detect redirect loops that switch 1095 // between the network and the cache. 1096 private int mCacheRedirectCount; 1097 1098 /* 1099 * Perform the actual redirection. This involves setting up the new URL, 1100 * informing WebCore and then telling the Network to start loading again. 1101 */ 1102 private void doRedirect() { 1103 // as cancel() can cancel the load before doRedirect() is 1104 // called through handleMessage, needs to check to see if we 1105 // are canceled before proceed 1106 if (mCancelled) { 1107 return; 1108 } 1109 1110 // Do the same check for a redirect loop that 1111 // RequestHandle.setupRedirect does. 1112 if (mCacheRedirectCount >= RequestHandle.MAX_REDIRECT_COUNT) { 1113 handleError(EventHandler.ERROR_REDIRECT_LOOP, mContext.getString( 1114 R.string.httpErrorRedirectLoop)); 1115 return; 1116 } 1117 1118 String redirectTo = mHeaders.getLocation(); 1119 if (redirectTo != null) { 1120 int nativeResponse = createNativeResponse(); 1121 redirectTo = 1122 nativeRedirectedToUrl(mUrl, redirectTo, nativeResponse); 1123 // nativeRedirectedToUrl() may call cancel(), e.g. when redirect 1124 // from a https site to a http site, check mCancelled again 1125 if (mCancelled) { 1126 return; 1127 } 1128 if (redirectTo == null) { 1129 Log.d(LOGTAG, "Redirection failed for " 1130 + mHeaders.getLocation()); 1131 cancel(); 1132 return; 1133 } else if (!URLUtil.isNetworkUrl(redirectTo)) { 1134 final String text = mContext 1135 .getString(R.string.open_permission_deny) 1136 + "\n" + redirectTo; 1137 nativeAddData(text.getBytes(), text.length()); 1138 nativeFinished(); 1139 clearNativeLoader(); 1140 return; 1141 } 1142 1143 if (mOriginalUrl == null) { 1144 mOriginalUrl = mUrl; 1145 } 1146 1147 // Cache the redirect response 1148 if (mCacheResult != null) { 1149 if (getErrorID() == OK) { 1150 CacheManager.saveCacheFile(mUrl, mCacheResult); 1151 } 1152 mCacheResult = null; 1153 } 1154 1155 // This will strip the anchor 1156 setUrl(redirectTo); 1157 1158 // Redirect may be in the cache 1159 if (mRequestHeaders == null) { 1160 mRequestHeaders = new HashMap<String, String>(); 1161 } 1162 boolean fromCache = false; 1163 if (mCacheLoader != null) { 1164 // This is a redirect from the cache loader. Increment the 1165 // redirect count to avoid redirect loops. 1166 mCacheRedirectCount++; 1167 fromCache = true; 1168 } 1169 if (!checkCache(mRequestHeaders)) { 1170 // mRequestHandle can be null when the request was satisfied 1171 // by the cache, and the cache returned a redirect 1172 if (mRequestHandle != null) { 1173 mRequestHandle.setupRedirect(mUrl, mStatusCode, 1174 mRequestHeaders); 1175 } else { 1176 // If the original request came from the cache, there is no 1177 // RequestHandle, we have to create a new one through 1178 // Network.requestURL. 1179 Network network = Network.getInstance(getContext()); 1180 if (!network.requestURL(mMethod, mRequestHeaders, 1181 mPostData, this, mIsHighPriority)) { 1182 // Signal a bad url error if we could not load the 1183 // redirection. 1184 handleError(EventHandler.ERROR_BAD_URL, 1185 mContext.getString(R.string.httpErrorBadUrl)); 1186 return; 1187 } 1188 } 1189 if (fromCache) { 1190 // If we are coming from a cache load, we need to transfer 1191 // the redirect count to the new (or old) RequestHandle to 1192 // keep the redirect count in sync. 1193 mRequestHandle.setRedirectCount(mCacheRedirectCount); 1194 } 1195 } else if (!fromCache) { 1196 // Switching from network to cache means we need to grab the 1197 // redirect count from the RequestHandle to keep the count in 1198 // sync. Add 1 to account for the current redirect. 1199 mCacheRedirectCount = mRequestHandle.getRedirectCount() + 1; 1200 } 1201 } else { 1202 commitHeaders(); 1203 commitLoad(); 1204 tearDown(); 1205 } 1206 1207 if (DebugFlags.LOAD_LISTENER) { 1208 Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " + 1209 redirectTo); 1210 } 1211 } 1212 1213 /** 1214 * Parses the content-type header. 1215 * The first part only allows '-' if it follows x or X. 1216 */ 1217 private static final Pattern CONTENT_TYPE_PATTERN = 1218 Pattern.compile("^((?:[xX]-)?[a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$"); 1219 1220 /* package */ void parseContentTypeHeader(String contentType) { 1221 if (DebugFlags.LOAD_LISTENER) { 1222 Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " + 1223 "contentType: " + contentType); 1224 } 1225 1226 if (contentType != null) { 1227 int i = contentType.indexOf(';'); 1228 if (i >= 0) { 1229 mMimeType = contentType.substring(0, i); 1230 1231 int j = contentType.indexOf('=', i); 1232 if (j > 0) { 1233 i = contentType.indexOf(';', j); 1234 if (i < j) { 1235 i = contentType.length(); 1236 } 1237 mEncoding = contentType.substring(j + 1, i); 1238 } else { 1239 mEncoding = contentType.substring(i + 1); 1240 } 1241 // Trim excess whitespace. 1242 mEncoding = mEncoding.trim().toLowerCase(); 1243 1244 if (i < contentType.length() - 1) { 1245 // for data: uri the mimeType and encoding have 1246 // the form image/jpeg;base64 or text/plain;charset=utf-8 1247 // or text/html;charset=utf-8;base64 1248 mTransferEncoding = 1249 contentType.substring(i + 1).trim().toLowerCase(); 1250 } 1251 } else { 1252 mMimeType = contentType; 1253 } 1254 1255 // Trim leading and trailing whitespace 1256 mMimeType = mMimeType.trim(); 1257 1258 try { 1259 Matcher m = CONTENT_TYPE_PATTERN.matcher(mMimeType); 1260 if (m.find()) { 1261 mMimeType = m.group(1); 1262 } else { 1263 guessMimeType(); 1264 } 1265 } catch (IllegalStateException ex) { 1266 guessMimeType(); 1267 } 1268 } 1269 // Ensure mMimeType is lower case. 1270 mMimeType = mMimeType.toLowerCase(); 1271 } 1272 1273 /** 1274 * @return The HTTP-authentication object or null if there 1275 * is no supported scheme in the header. 1276 * If there are several valid schemes present, we pick the 1277 * strongest one. If there are several schemes of the same 1278 * strength, we pick the one that comes first. 1279 */ 1280 private HttpAuthHeader parseAuthHeader(String header) { 1281 if (header != null) { 1282 int posMax = 256; 1283 int posLen = 0; 1284 int[] pos = new int [posMax]; 1285 1286 int headerLen = header.length(); 1287 if (headerLen > 0) { 1288 // first, we find all unquoted instances of 'Basic' and 'Digest' 1289 boolean quoted = false; 1290 for (int i = 0; i < headerLen && posLen < posMax; ++i) { 1291 if (header.charAt(i) == '\"') { 1292 quoted = !quoted; 1293 } else { 1294 if (!quoted) { 1295 if (header.regionMatches(true, i, 1296 HttpAuthHeader.BASIC_TOKEN, 0, 1297 HttpAuthHeader.BASIC_TOKEN.length())) { 1298 pos[posLen++] = i; 1299 continue; 1300 } 1301 1302 if (header.regionMatches(true, i, 1303 HttpAuthHeader.DIGEST_TOKEN, 0, 1304 HttpAuthHeader.DIGEST_TOKEN.length())) { 1305 pos[posLen++] = i; 1306 continue; 1307 } 1308 } 1309 } 1310 } 1311 } 1312 1313 if (posLen > 0) { 1314 // consider all digest schemes first (if any) 1315 for (int i = 0; i < posLen; i++) { 1316 if (header.regionMatches(true, pos[i], 1317 HttpAuthHeader.DIGEST_TOKEN, 0, 1318 HttpAuthHeader.DIGEST_TOKEN.length())) { 1319 String sub = header.substring(pos[i], 1320 (i + 1 < posLen ? pos[i + 1] : headerLen)); 1321 1322 HttpAuthHeader rval = new HttpAuthHeader(sub); 1323 if (rval.isSupportedScheme()) { 1324 // take the first match 1325 return rval; 1326 } 1327 } 1328 } 1329 1330 // ...then consider all basic schemes (if any) 1331 for (int i = 0; i < posLen; i++) { 1332 if (header.regionMatches(true, pos[i], 1333 HttpAuthHeader.BASIC_TOKEN, 0, 1334 HttpAuthHeader.BASIC_TOKEN.length())) { 1335 String sub = header.substring(pos[i], 1336 (i + 1 < posLen ? pos[i + 1] : headerLen)); 1337 1338 HttpAuthHeader rval = new HttpAuthHeader(sub); 1339 if (rval.isSupportedScheme()) { 1340 // take the first match 1341 return rval; 1342 } 1343 } 1344 } 1345 } 1346 } 1347 1348 return null; 1349 } 1350 1351 /** 1352 * If the content is a redirect or not modified we should not send 1353 * any data into WebCore as that will cause it create a document with 1354 * the data, then when we try to provide the real content, it will assert. 1355 * 1356 * @return True iff the callback should be ignored. 1357 */ 1358 private boolean ignoreCallbacks() { 1359 return (mCancelled || mAuthHeader != null || 1360 (mStatusCode > 300 && mStatusCode < 400)); 1361 } 1362 1363 /** 1364 * Sets the current URL associated with this load. 1365 */ 1366 void setUrl(String url) { 1367 if (url != null) { 1368 mUri = null; 1369 if (URLUtil.isNetworkUrl(url)) { 1370 mUrl = URLUtil.stripAnchor(url); 1371 try { 1372 mUri = new WebAddress(mUrl); 1373 } catch (ParseException e) { 1374 e.printStackTrace(); 1375 } 1376 } else { 1377 mUrl = url; 1378 } 1379 } 1380 } 1381 1382 /** 1383 * Guesses MIME type if one was not specified. Defaults to 'text/html'. In 1384 * addition, tries to guess the MIME type based on the extension. 1385 * 1386 */ 1387 private void guessMimeType() { 1388 // Data urls must have a valid mime type or a blank string for the mime 1389 // type (implying text/plain). 1390 if (URLUtil.isDataUrl(mUrl) && mMimeType.length() != 0) { 1391 cancel(); 1392 final String text = mContext.getString(R.string.httpErrorBadUrl); 1393 handleError(EventHandler.ERROR_BAD_URL, text); 1394 } else { 1395 // Note: This is ok because this is used only for the main content 1396 // of frames. If no content-type was specified, it is fine to 1397 // default to text/html. 1398 mMimeType = "text/html"; 1399 String newMimeType = guessMimeTypeFromExtension(); 1400 if (newMimeType != null) { 1401 mMimeType = newMimeType; 1402 } 1403 } 1404 } 1405 1406 /** 1407 * guess MIME type based on the file extension. 1408 */ 1409 private String guessMimeTypeFromExtension() { 1410 // PENDING: need to normalize url 1411 if (DebugFlags.LOAD_LISTENER) { 1412 Log.v(LOGTAG, "guessMimeTypeFromExtension: mURL = " + mUrl); 1413 } 1414 1415 return MimeTypeMap.getSingleton().getMimeTypeFromExtension( 1416 MimeTypeMap.getFileExtensionFromUrl(mUrl)); 1417 } 1418 1419 /** 1420 * Either send a message to ourselves or queue the message if this is a 1421 * synchronous load. 1422 */ 1423 private void sendMessageInternal(Message msg) { 1424 if (mSynchronous) { 1425 mMessageQueue.add(msg); 1426 } else { 1427 sendMessage(msg); 1428 } 1429 } 1430 1431 /** 1432 * Cycle through our messages for synchronous loads. 1433 */ 1434 /* package */ void loadSynchronousMessages() { 1435 if (DebugFlags.LOAD_LISTENER && !mSynchronous) { 1436 throw new AssertionError(); 1437 } 1438 // Note: this can be called twice if it is a synchronous network load, 1439 // and there is a cache, but it needs to go to network to validate. If 1440 // validation succeed, the CacheLoader is used so this is first called 1441 // from http thread. Then it is called again from WebViewCore thread 1442 // after the load is completed. So make sure the queue is cleared but 1443 // don't set it to null. 1444 for (int size = mMessageQueue.size(); size > 0; size--) { 1445 handleMessage(mMessageQueue.remove(0)); 1446 } 1447 } 1448 1449 //========================================================================= 1450 // native functions 1451 //========================================================================= 1452 1453 /** 1454 * Create a new native response object. 1455 * @param url The url of the resource. 1456 * @param statusCode The HTTP status code. 1457 * @param statusText The HTTP status text. 1458 * @param mimeType HTTP content-type. 1459 * @param expectedLength An estimate of the content length or the length 1460 * given by the server. 1461 * @param encoding HTTP encoding. 1462 * @return The native response pointer. 1463 */ 1464 private native int nativeCreateResponse(String url, int statusCode, 1465 String statusText, String mimeType, long expectedLength, 1466 String encoding); 1467 1468 /** 1469 * Add a response header to the native object. 1470 * @param nativeResponse The native pointer. 1471 * @param key String key. 1472 * @param val String value. 1473 */ 1474 private native void nativeSetResponseHeader(int nativeResponse, String key, 1475 String val); 1476 1477 /** 1478 * Dispatch the response. 1479 * @param nativeResponse The native pointer. 1480 */ 1481 private native void nativeReceivedResponse(int nativeResponse); 1482 1483 /** 1484 * Add data to the loader. 1485 * @param data Byte array of data. 1486 * @param length Number of objects in data. 1487 */ 1488 private native void nativeAddData(byte[] data, int length); 1489 1490 /** 1491 * Tell the loader it has finished. 1492 */ 1493 private native void nativeFinished(); 1494 1495 /** 1496 * tell the loader to redirect 1497 * @param baseUrl The base url. 1498 * @param redirectTo The url to redirect to. 1499 * @param nativeResponse The native pointer. 1500 * @return The new url that the resource redirected to. 1501 */ 1502 private native String nativeRedirectedToUrl(String baseUrl, 1503 String redirectTo, int nativeResponse); 1504 1505 /** 1506 * Tell the loader there is error 1507 * @param id 1508 * @param desc 1509 * @param failingUrl The url that failed. 1510 */ 1511 private native void nativeError(int id, String desc, String failingUrl); 1512 1513} 1514