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