LoadListener.java revision c39a6e0c51e182338deb8b63d07933b585134929
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 380 // if there is buffered data, commit them in the end 381 boolean needToCommit = mAuthHeader != null && !mustAuthenticate 382 && mNativeLoader != 0 && !mDataBuilder.isEmpty(); 383 384 // it is only here that we can reset the last mAuthHeader object 385 // (if existed) and start a new one!!! 386 mAuthHeader = null; 387 if (mustAuthenticate) { 388 if (mStatusCode == HTTP_AUTH) { 389 mAuthHeader = parseAuthHeader( 390 headers.getWwwAuthenticate()); 391 } else { 392 mAuthHeader = parseAuthHeader( 393 headers.getProxyAuthenticate()); 394 // if successfully parsed the header 395 if (mAuthHeader != null) { 396 // mark the auth-header object as a proxy 397 mAuthHeader.setProxy(); 398 } 399 } 400 } 401 402 // Only create a cache file if the server has responded positively. 403 if ((mStatusCode == HTTP_OK || 404 mStatusCode == HTTP_FOUND || 405 mStatusCode == HTTP_MOVED_PERMANENTLY || 406 mStatusCode == HTTP_TEMPORARY_REDIRECT) && 407 mNativeLoader != 0) { 408 // Content arriving from a StreamLoader (eg File, Cache or Data) 409 // will not be cached as they have the header: 410 // cache-control: no-store 411 mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, 412 headers, mMimeType, false); 413 if (mCacheResult != null) { 414 mCacheResult.encoding = mEncoding; 415 } 416 } 417 commitHeadersCheckRedirect(); 418 419 if (needToCommit) { 420 commitLoad(); 421 } 422 } 423 424 /** 425 * @return True iff this loader is in the proxy-authenticate state. 426 */ 427 boolean proxyAuthenticate() { 428 if (mAuthHeader != null) { 429 return mAuthHeader.isProxy(); 430 } 431 432 return false; 433 } 434 435 /** 436 * Report the status of the response. 437 * TODO: Comments about each parameter. 438 * IMPORTANT: as this is called from network thread, can't call native 439 * directly 440 */ 441 public void status(int majorVersion, int minorVersion, 442 int code, /* Status-Code value */ String reasonPhrase) { 443 if (Config.LOGV) { 444 Log.v(LOGTAG, "LoadListener: from: " + mUrl 445 + " major: " + majorVersion 446 + " minor: " + minorVersion 447 + " code: " + code 448 + " reason: " + reasonPhrase); 449 } 450 HashMap status = new HashMap(); 451 status.put("major", majorVersion); 452 status.put("minor", minorVersion); 453 status.put("code", code); 454 status.put("reason", reasonPhrase); 455 sendMessageInternal(obtainMessage(MSG_STATUS, status)); 456 } 457 458 // Handle the status callback on the WebCore thread. 459 private void handleStatus(int major, int minor, int code, String reason) { 460 if (mCancelled) return; 461 462 mStatusCode = code; 463 mStatusText = reason; 464 mPermanent = false; 465 } 466 467 /** 468 * Implementation of certificate handler for EventHandler. 469 * Called every time a resource is loaded via a secure 470 * connection. In this context, can be called multiple 471 * times if we have redirects 472 * @param certificate The SSL certifcate 473 * IMPORTANT: as this is called from network thread, can't call native 474 * directly 475 */ 476 public void certificate(SslCertificate certificate) { 477 sendMessageInternal(obtainMessage(MSG_SSL_CERTIFICATE, certificate)); 478 } 479 480 // Handle the certificate on the WebCore thread. 481 private void handleCertificate(SslCertificate certificate) { 482 // if this is the top-most main-frame page loader 483 if (mIsMainPageLoader) { 484 // update the browser frame (ie, the main frame) 485 mBrowserFrame.certificate(certificate); 486 } 487 } 488 489 /** 490 * Implementation of error handler for EventHandler. 491 * Subclasses should call this method to have error fields set. 492 * @param id The error id described by EventHandler. 493 * @param description A string description of the error. 494 * IMPORTANT: as this is called from network thread, can't call native 495 * directly 496 */ 497 public void error(int id, String description) { 498 if (Config.LOGV) { 499 Log.v(LOGTAG, "LoadListener.error url:" + 500 url() + " id:" + id + " description:" + description); 501 } 502 sendMessageInternal(obtainMessage(MSG_CONTENT_ERROR, id, 0, description)); 503 } 504 505 // Handle the error on the WebCore thread. 506 private void handleError(int id, String description) { 507 mErrorID = id; 508 mErrorDescription = description; 509 detachRequestHandle(); 510 notifyError(); 511 tearDown(); 512 } 513 514 /** 515 * Add data to the internal collection of data. This function is used by 516 * the data: scheme, about: scheme and http/https schemes. 517 * @param data A byte array containing the content. 518 * @param length The length of data. 519 * IMPORTANT: as this is called from network thread, can't call native 520 * directly 521 * XXX: Unlike the other network thread methods, this method can do the 522 * work of decoding the data and appending it to the data builder because 523 * mDataBuilder is a thread-safe structure. 524 */ 525 public void data(byte[] data, int length) { 526 if (Config.LOGV) { 527 Log.v(LOGTAG, "LoadListener.data(): url: " + url()); 528 } 529 530 // Decode base64 data 531 // Note: It's fine that we only decode base64 here and not in the other 532 // data call because the only caller of the stream version is not 533 // base64 encoded. 534 if ("base64".equalsIgnoreCase(mTransferEncoding)) { 535 if (length < data.length) { 536 byte[] trimmedData = new byte[length]; 537 System.arraycopy(data, 0, trimmedData, 0, length); 538 data = trimmedData; 539 } 540 data = Base64.decodeBase64(data); 541 length = data.length; 542 } 543 // Synchronize on mData because commitLoad may write mData to WebCore 544 // and we don't want to replace mData or mDataLength at the same time 545 // as a write. 546 boolean sendMessage = false; 547 synchronized (mDataBuilder) { 548 sendMessage = mDataBuilder.isEmpty(); 549 mDataBuilder.append(data, 0, length); 550 } 551 if (sendMessage) { 552 // Send a message whenever data comes in after a write to WebCore 553 sendMessageInternal(obtainMessage(MSG_CONTENT_DATA)); 554 } 555 } 556 557 /** 558 * Event handler's endData call. Send a message to the handler notifying 559 * them that the data has finished. 560 * IMPORTANT: as this is called from network thread, can't call native 561 * directly 562 */ 563 public void endData() { 564 if (Config.LOGV) { 565 Log.v(LOGTAG, "LoadListener.endData(): url: " + url()); 566 } 567 sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); 568 } 569 570 // Handle the end of data. 571 private void handleEndData() { 572 if (mCancelled) return; 573 574 switch (mStatusCode) { 575 case HTTP_MOVED_PERMANENTLY: 576 // 301 - permanent redirect 577 mPermanent = true; 578 case HTTP_FOUND: 579 case HTTP_SEE_OTHER: 580 case HTTP_TEMPORARY_REDIRECT: 581 // 301, 302, 303, and 307 - redirect 582 if (mStatusCode == HTTP_TEMPORARY_REDIRECT) { 583 if (mRequestHandle != null && 584 mRequestHandle.getMethod().equals("POST")) { 585 sendMessageInternal(obtainMessage( 586 MSG_LOCATION_CHANGED_REQUEST)); 587 } else if (mMethod != null && mMethod.equals("POST")) { 588 sendMessageInternal(obtainMessage( 589 MSG_LOCATION_CHANGED_REQUEST)); 590 } else { 591 sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); 592 } 593 } else { 594 sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); 595 } 596 return; 597 598 case HTTP_AUTH: 599 case HTTP_PROXY_AUTH: 600 // According to rfc2616, the response for HTTP_AUTH must include 601 // WWW-Authenticate header field and the response for 602 // HTTP_PROXY_AUTH must include Proxy-Authenticate header field. 603 if (mAuthHeader != null && 604 (Network.getInstance(mContext).isValidProxySet() || 605 !mAuthHeader.isProxy())) { 606 Network.getInstance(mContext).handleAuthRequest(this); 607 return; 608 } 609 break; // use default 610 611 case HTTP_NOT_MODIFIED: 612 // Server could send back NOT_MODIFIED even if we didn't 613 // ask for it, so make sure we have a valid CacheLoader 614 // before calling it. 615 if (mCacheLoader != null) { 616 detachRequestHandle(); 617 mCacheLoader.load(); 618 if (Config.LOGV) { 619 Log.v(LOGTAG, "LoadListener cache load url=" + url()); 620 } 621 return; 622 } 623 break; // use default 624 625 case HTTP_NOT_FOUND: 626 // Not an error, the server can send back content. 627 default: 628 break; 629 } 630 detachRequestHandle(); 631 tearDown(); 632 } 633 634 /* This method is called from CacheLoader when the initial request is 635 * serviced by the Cache. */ 636 /* package */ void setCacheLoader(CacheLoader c) { 637 mCacheLoader = c; 638 } 639 640 /** 641 * Check the cache for the current URL, and load it if it is valid. 642 * 643 * @param headers for the request 644 * @return true if cached response is used. 645 */ 646 boolean checkCache(Map<String, String> headers) { 647 // Get the cache file name for the current URL 648 CacheResult result = CacheManager.getCacheFile(url(), 649 headers); 650 651 // Go ahead and set the cache loader to null in case the result is 652 // null. 653 mCacheLoader = null; 654 655 if (result != null) { 656 // The contents of the cache may need to be revalidated so just 657 // remember the cache loader in the case that the server responds 658 // positively to the cached content. This is also used to detect if 659 // a redirect came from the cache. 660 mCacheLoader = new CacheLoader(this, result); 661 662 // If I got a cachedUrl and the revalidation header was not 663 // added, then the cached content valid, we should use it. 664 if (!headers.containsKey( 665 CacheManager.HEADER_KEY_IFNONEMATCH) && 666 !headers.containsKey( 667 CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) { 668 if (Config.LOGV) { 669 Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " + 670 "and usable: " + url()); 671 } 672 // Load the cached file 673 mCacheLoader.load(); 674 return true; 675 } 676 } 677 return false; 678 } 679 680 /** 681 * SSL certificate error callback. Handles SSL error(s) on the way up 682 * to the user. 683 * IMPORTANT: as this is called from network thread, can't call native 684 * directly 685 */ 686 public void handleSslErrorRequest(SslError error) { 687 if (Config.LOGV) { 688 Log.v(LOGTAG, 689 "LoadListener.handleSslErrorRequest(): url:" + url() + 690 " primary error: " + error.getPrimaryError() + 691 " certificate: " + error.getCertificate()); 692 } 693 sendMessageInternal(obtainMessage(MSG_SSL_ERROR, error)); 694 } 695 696 // Handle the ssl error on the WebCore thread. 697 private void handleSslError(SslError error) { 698 if (!mCancelled) { 699 mSslError = error; 700 Network.getInstance(mContext).handleSslErrorRequest(this); 701 } 702 } 703 704 /** 705 * @return HTTP authentication realm or null if none. 706 */ 707 String realm() { 708 if (mAuthHeader == null) { 709 return null; 710 } else { 711 return mAuthHeader.getRealm(); 712 } 713 } 714 715 /** 716 * Returns true iff an HTTP authentication problem has 717 * occured (credentials invalid). 718 */ 719 boolean authCredentialsInvalid() { 720 // if it is digest and the nonce is stale, we just 721 // resubmit with a new nonce 722 return (mAuthFailed && 723 !(mAuthHeader.isDigest() && mAuthHeader.getStale())); 724 } 725 726 /** 727 * @return The last SSL error or null if there is none 728 */ 729 SslError sslError() { 730 return mSslError; 731 } 732 733 /** 734 * Handles SSL error(s) on the way down from the user 735 * (the user has already provided their feedback). 736 */ 737 void handleSslErrorResponse(boolean proceed) { 738 if (mRequestHandle != null) { 739 mRequestHandle.handleSslErrorResponse(proceed); 740 } 741 } 742 743 /** 744 * Uses user-supplied credentials to restar a request. 745 */ 746 void handleAuthResponse(String username, String password) { 747 if (Config.LOGV) { 748 Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl 749 + " username: " + username 750 + " password: " + password); 751 } 752 753 // create and queue an authentication-response 754 if (username != null && password != null) { 755 if (mAuthHeader != null && mRequestHandle != null) { 756 mAuthHeader.setUsername(username); 757 mAuthHeader.setPassword(password); 758 759 int scheme = mAuthHeader.getScheme(); 760 if (scheme == HttpAuthHeader.BASIC) { 761 // create a basic response 762 boolean isProxy = mAuthHeader.isProxy(); 763 764 mRequestHandle.setupBasicAuthResponse(isProxy, 765 username, password); 766 } else { 767 if (scheme == HttpAuthHeader.DIGEST) { 768 // create a digest response 769 boolean isProxy = mAuthHeader.isProxy(); 770 771 String realm = mAuthHeader.getRealm(); 772 String nonce = mAuthHeader.getNonce(); 773 String qop = mAuthHeader.getQop(); 774 String algorithm = mAuthHeader.getAlgorithm(); 775 String opaque = mAuthHeader.getOpaque(); 776 777 mRequestHandle.setupDigestAuthResponse 778 (isProxy, username, password, realm, 779 nonce, qop, algorithm, opaque); 780 } 781 } 782 } 783 } 784 } 785 786 /** 787 * This is called when a request can be satisfied by the cache, however, 788 * the cache result could be a redirect. In this case we need to issue 789 * the network request. 790 * @param method 791 * @param headers 792 * @param postData 793 * @param isHighPriority 794 */ 795 void setRequestData(String method, Map<String, String> headers, 796 byte[] postData, boolean isHighPriority) { 797 mMethod = method; 798 mRequestHeaders = headers; 799 mPostData = postData; 800 mIsHighPriority = isHighPriority; 801 } 802 803 /** 804 * @return The current URL associated with this load. 805 */ 806 String url() { 807 return mUrl; 808 } 809 810 /** 811 * @return The current WebAddress associated with this load. 812 */ 813 WebAddress getWebAddress() { 814 return mUri; 815 } 816 817 /** 818 * @return URL hostname (current URL). 819 */ 820 String host() { 821 if (mUri != null) { 822 return mUri.mHost; 823 } 824 825 return null; 826 } 827 828 /** 829 * @return The original URL associated with this load. 830 */ 831 String originalUrl() { 832 if (mOriginalUrl != null) { 833 return mOriginalUrl; 834 } else { 835 return mUrl; 836 } 837 } 838 839 void attachRequestHandle(RequestHandle requestHandle) { 840 if (Config.LOGV) { 841 Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + 842 "requestHandle: " + requestHandle); 843 } 844 mRequestHandle = requestHandle; 845 } 846 847 void detachRequestHandle() { 848 if (Config.LOGV) { 849 Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " + 850 "requestHandle: " + mRequestHandle); 851 } 852 mRequestHandle = null; 853 } 854 855 /* 856 * This function is called from native WebCore code to 857 * notify this LoadListener that the content it is currently 858 * downloading should be saved to a file and not sent to 859 * WebCore. 860 */ 861 void downloadFile() { 862 // Setting the Cache Result to null ensures that this 863 // content is not added to the cache 864 mCacheResult = null; 865 866 // Inform the client that they should download a file 867 mBrowserFrame.getCallbackProxy().onDownloadStart(url(), 868 mBrowserFrame.getUserAgentString(), 869 mHeaders.getContentDisposition(), 870 mMimeType, mContentLength); 871 872 // Cancel the download. We need to stop the http load. 873 // The native loader object will get cleared by the call to 874 // cancel() but will also be cleared on the WebCore side 875 // when this function returns. 876 cancel(); 877 } 878 879 /* 880 * This function is called from native WebCore code to 881 * find out if the given URL is in the cache, and if it can 882 * be used. This is just for forward/back navigation to a POST 883 * URL. 884 */ 885 static boolean willLoadFromCache(String url) { 886 boolean inCache = CacheManager.getCacheFile(url, null) != null; 887 if (Config.LOGV) { 888 Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + 889 inCache); 890 } 891 return inCache; 892 } 893 894 /* 895 * Reset the cancel flag. This is used when we are resuming a stopped 896 * download. To suspend a download, we cancel it. It can also be cancelled 897 * when it has run out of disk space. In this situation, the download 898 * can be resumed. 899 */ 900 void resetCancel() { 901 mCancelled = false; 902 } 903 904 String mimeType() { 905 return mMimeType; 906 } 907 908 /* 909 * Return the size of the content being downloaded. This represents the 910 * full content size, even under the situation where the download has been 911 * resumed after interruption. 912 * 913 * @ return full content size 914 */ 915 long contentLength() { 916 return mContentLength; 917 } 918 919 // Commit the headers if the status code is not a redirect. 920 private void commitHeadersCheckRedirect() { 921 if (mCancelled) return; 922 923 // do not call webcore if it is redirect. According to the code in 924 // InspectorController::willSendRequest(), the response is only updated 925 // when it is not redirect. 926 if ((mStatusCode >= 301 && mStatusCode <= 303) || mStatusCode == 307) { 927 return; 928 } 929 930 commitHeaders(); 931 } 932 933 // This commits the headers without checking the response status code. 934 private void commitHeaders() { 935 // Commit the headers to WebCore 936 int nativeResponse = createNativeResponse(); 937 // The native code deletes the native response object. 938 nativeReceivedResponse(nativeResponse); 939 } 940 941 /** 942 * Create a WebCore response object so that it can be used by 943 * nativeReceivedResponse or nativeRedirectedToUrl 944 * @return native response pointer 945 */ 946 private int createNativeResponse() { 947 // The reason we change HTTP_NOT_MODIFIED to HTTP_OK is because we know 948 // that WebCore never sends the if-modified-since header. Our 949 // CacheManager does it for us. If the server responds with a 304, then 950 // we treat it like it was a 200 code and proceed with loading the file 951 // from the cache. 952 int statusCode = mStatusCode == HTTP_NOT_MODIFIED 953 ? HTTP_OK : mStatusCode; 954 // pass content-type content-length and content-encoding 955 final int nativeResponse = nativeCreateResponse( 956 mUrl, statusCode, mStatusText, 957 mMimeType, mContentLength, mEncoding, 958 mCacheResult == null ? 0 : mCacheResult.expires / 1000); 959 if (mHeaders != null) { 960 mHeaders.getHeaders(new Headers.HeaderCallback() { 961 public void header(String name, String value) { 962 nativeSetResponseHeader(nativeResponse, name, value); 963 } 964 }); 965 } 966 return nativeResponse; 967 } 968 969 /** 970 * Commit the load. It should be ok to call repeatedly but only before 971 * tearDown is called. 972 */ 973 private void commitLoad() { 974 if (mCancelled) return; 975 976 // Give the data to WebKit now 977 PerfChecker checker = new PerfChecker(); 978 ByteArrayBuilder.Chunk c; 979 while (true) { 980 c = mDataBuilder.getFirstChunk(); 981 if (c == null) break; 982 983 if (c.mLength != 0) { 984 if (mCacheResult != null) { 985 try { 986 mCacheResult.outStream.write(c.mArray, 0, c.mLength); 987 } catch (IOException e) { 988 mCacheResult = null; 989 } 990 } 991 nativeAddData(c.mArray, c.mLength); 992 } 993 mDataBuilder.releaseChunk(c); 994 checker.responseAlert("res nativeAddData"); 995 } 996 } 997 998 /** 999 * Tear down the load. Subclasses should clean up any mess because of 1000 * cancellation or errors during the load. 1001 */ 1002 void tearDown() { 1003 if (mCacheResult != null) { 1004 if (getErrorID() == OK) { 1005 CacheManager.saveCacheFile(mUrl, mCacheResult); 1006 } 1007 1008 // we need to reset mCacheResult to be null 1009 // resource loader's tearDown will call into WebCore's 1010 // nativeFinish, which in turn calls loader.cancel(). 1011 // If we don't reset mCacheFile, the file will be deleted. 1012 mCacheResult = null; 1013 } 1014 if (mNativeLoader != 0) { 1015 PerfChecker checker = new PerfChecker(); 1016 nativeFinished(); 1017 checker.responseAlert("res nativeFinished"); 1018 clearNativeLoader(); 1019 } 1020 } 1021 1022 /** 1023 * Helper for getting the error ID. 1024 * @return errorID. 1025 */ 1026 private int getErrorID() { 1027 return mErrorID; 1028 } 1029 1030 /** 1031 * Return the error description. 1032 * @return errorDescription. 1033 */ 1034 private String getErrorDescription() { 1035 return mErrorDescription; 1036 } 1037 1038 /** 1039 * Notify the loader we encountered an error. 1040 */ 1041 void notifyError() { 1042 if (mNativeLoader != 0) { 1043 String description = getErrorDescription(); 1044 if (description == null) description = ""; 1045 nativeError(getErrorID(), description, url()); 1046 clearNativeLoader(); 1047 } 1048 } 1049 1050 /** 1051 * Cancel a request. 1052 * FIXME: This will only work if the request has yet to be handled. This 1053 * is in no way guarenteed if requests are served in a separate thread. 1054 * It also causes major problems if cancel is called during an 1055 * EventHandler's method call. 1056 */ 1057 public void cancel() { 1058 if (Config.LOGV) { 1059 if (mRequestHandle == null) { 1060 Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle"); 1061 } else { 1062 Log.v(LOGTAG, "LoadListener.cancel()"); 1063 } 1064 } 1065 if (mRequestHandle != null) { 1066 mRequestHandle.cancel(); 1067 mRequestHandle = null; 1068 } 1069 1070 mCacheResult = null; 1071 mCancelled = true; 1072 1073 clearNativeLoader(); 1074 } 1075 1076 // This count is transferred from RequestHandle to LoadListener when 1077 // loading from the cache so that we can detect redirect loops that switch 1078 // between the network and the cache. 1079 private int mCacheRedirectCount; 1080 1081 /* 1082 * Perform the actual redirection. This involves setting up the new URL, 1083 * informing WebCore and then telling the Network to start loading again. 1084 */ 1085 private void doRedirect() { 1086 // as cancel() can cancel the load before doRedirect() is 1087 // called through handleMessage, needs to check to see if we 1088 // are canceled before proceed 1089 if (mCancelled) { 1090 return; 1091 } 1092 1093 // Do the same check for a redirect loop that 1094 // RequestHandle.setupRedirect does. 1095 if (mCacheRedirectCount >= RequestHandle.MAX_REDIRECT_COUNT) { 1096 handleError(EventHandler.ERROR_REDIRECT_LOOP, mContext.getString( 1097 R.string.httpErrorRedirectLoop)); 1098 return; 1099 } 1100 1101 String redirectTo = mHeaders.getLocation(); 1102 if (redirectTo != null) { 1103 int nativeResponse = createNativeResponse(); 1104 redirectTo = 1105 nativeRedirectedToUrl(mUrl, redirectTo, nativeResponse); 1106 // nativeRedirectedToUrl() may call cancel(), e.g. when redirect 1107 // from a https site to a http site, check mCancelled again 1108 if (mCancelled) { 1109 return; 1110 } 1111 if (redirectTo == null) { 1112 Log.d(LOGTAG, "Redirection failed for " 1113 + mHeaders.getLocation()); 1114 cancel(); 1115 return; 1116 } else if (!URLUtil.isNetworkUrl(redirectTo)) { 1117 final String text = mContext 1118 .getString(R.string.open_permission_deny) 1119 + "\n" + redirectTo; 1120 nativeAddData(text.getBytes(), text.length()); 1121 nativeFinished(); 1122 clearNativeLoader(); 1123 return; 1124 } 1125 1126 if (mOriginalUrl == null) { 1127 mOriginalUrl = mUrl; 1128 } 1129 1130 // Cache the redirect response 1131 if (mCacheResult != null) { 1132 if (getErrorID() == OK) { 1133 CacheManager.saveCacheFile(mUrl, mCacheResult); 1134 } 1135 mCacheResult = null; 1136 } 1137 1138 // This will strip the anchor 1139 setUrl(redirectTo); 1140 1141 // Redirect may be in the cache 1142 if (mRequestHeaders == null) { 1143 mRequestHeaders = new HashMap<String, String>(); 1144 } 1145 boolean fromCache = false; 1146 if (mCacheLoader != null) { 1147 // This is a redirect from the cache loader. Increment the 1148 // redirect count to avoid redirect loops. 1149 mCacheRedirectCount++; 1150 fromCache = true; 1151 } 1152 if (!checkCache(mRequestHeaders)) { 1153 // mRequestHandle can be null when the request was satisfied 1154 // by the cache, and the cache returned a redirect 1155 if (mRequestHandle != null) { 1156 mRequestHandle.setupRedirect(mUrl, mStatusCode, 1157 mRequestHeaders); 1158 } else { 1159 // If the original request came from the cache, there is no 1160 // RequestHandle, we have to create a new one through 1161 // Network.requestURL. 1162 Network network = Network.getInstance(getContext()); 1163 if (!network.requestURL(mMethod, mRequestHeaders, 1164 mPostData, this, mIsHighPriority)) { 1165 // Signal a bad url error if we could not load the 1166 // redirection. 1167 handleError(EventHandler.ERROR_BAD_URL, 1168 mContext.getString(R.string.httpErrorBadUrl)); 1169 return; 1170 } 1171 } 1172 if (fromCache) { 1173 // If we are coming from a cache load, we need to transfer 1174 // the redirect count to the new (or old) RequestHandle to 1175 // keep the redirect count in sync. 1176 mRequestHandle.setRedirectCount(mCacheRedirectCount); 1177 } 1178 } else if (!fromCache) { 1179 // Switching from network to cache means we need to grab the 1180 // redirect count from the RequestHandle to keep the count in 1181 // sync. Add 1 to account for the current redirect. 1182 mCacheRedirectCount = mRequestHandle.getRedirectCount() + 1; 1183 } 1184 // Clear the buffered data since the redirect is valid. 1185 mDataBuilder.clear(); 1186 } else { 1187 commitHeaders(); 1188 commitLoad(); 1189 tearDown(); 1190 } 1191 1192 if (Config.LOGV) { 1193 Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " + 1194 redirectTo); 1195 } 1196 } 1197 1198 /** 1199 * Parses the content-type header. 1200 */ 1201 private static final Pattern CONTENT_TYPE_PATTERN = 1202 Pattern.compile("^([a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$"); 1203 1204 private void parseContentTypeHeader(String contentType) { 1205 if (Config.LOGV) { 1206 Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " + 1207 "contentType: " + contentType); 1208 } 1209 1210 if (contentType != null) { 1211 int i = contentType.indexOf(';'); 1212 if (i >= 0) { 1213 mMimeType = contentType.substring(0, i); 1214 1215 int j = contentType.indexOf('=', i); 1216 if (j > 0) { 1217 i = contentType.indexOf(';', j); 1218 if (i < j) { 1219 i = contentType.length(); 1220 } 1221 mEncoding = contentType.substring(j + 1, i); 1222 } else { 1223 mEncoding = contentType.substring(i + 1); 1224 } 1225 // Trim excess whitespace. 1226 mEncoding = mEncoding.trim(); 1227 1228 if (i < contentType.length() - 1) { 1229 // for data: uri the mimeType and encoding have 1230 // the form image/jpeg;base64 or text/plain;charset=utf-8 1231 // or text/html;charset=utf-8;base64 1232 mTransferEncoding = contentType.substring(i + 1).trim(); 1233 } 1234 } else { 1235 mMimeType = contentType; 1236 } 1237 1238 // Trim leading and trailing whitespace 1239 mMimeType = mMimeType.trim(); 1240 1241 try { 1242 Matcher m = CONTENT_TYPE_PATTERN.matcher(mMimeType); 1243 if (m.find()) { 1244 mMimeType = m.group(1); 1245 } else { 1246 guessMimeType(); 1247 } 1248 } catch (IllegalStateException ex) { 1249 guessMimeType(); 1250 } 1251 } 1252 } 1253 1254 /** 1255 * @return The HTTP-authentication object or null if there 1256 * is no supported scheme in the header. 1257 * If there are several valid schemes present, we pick the 1258 * strongest one. If there are several schemes of the same 1259 * strength, we pick the one that comes first. 1260 */ 1261 private HttpAuthHeader parseAuthHeader(String header) { 1262 if (header != null) { 1263 int posMax = 256; 1264 int posLen = 0; 1265 int[] pos = new int [posMax]; 1266 1267 int headerLen = header.length(); 1268 if (headerLen > 0) { 1269 // first, we find all unquoted instances of 'Basic' and 'Digest' 1270 boolean quoted = false; 1271 for (int i = 0; i < headerLen && posLen < posMax; ++i) { 1272 if (header.charAt(i) == '\"') { 1273 quoted = !quoted; 1274 } else { 1275 if (!quoted) { 1276 if (header.regionMatches(true, i, 1277 HttpAuthHeader.BASIC_TOKEN, 0, 1278 HttpAuthHeader.BASIC_TOKEN.length())) { 1279 pos[posLen++] = i; 1280 continue; 1281 } 1282 1283 if (header.regionMatches(true, i, 1284 HttpAuthHeader.DIGEST_TOKEN, 0, 1285 HttpAuthHeader.DIGEST_TOKEN.length())) { 1286 pos[posLen++] = i; 1287 continue; 1288 } 1289 } 1290 } 1291 } 1292 } 1293 1294 if (posLen > 0) { 1295 // consider all digest schemes first (if any) 1296 for (int i = 0; i < posLen; i++) { 1297 if (header.regionMatches(true, pos[i], 1298 HttpAuthHeader.DIGEST_TOKEN, 0, 1299 HttpAuthHeader.DIGEST_TOKEN.length())) { 1300 String sub = header.substring(pos[i], 1301 (i + 1 < posLen ? pos[i + 1] : headerLen)); 1302 1303 HttpAuthHeader rval = new HttpAuthHeader(sub); 1304 if (rval.isSupportedScheme()) { 1305 // take the first match 1306 return rval; 1307 } 1308 } 1309 } 1310 1311 // ...then consider all basic schemes (if any) 1312 for (int i = 0; i < posLen; i++) { 1313 if (header.regionMatches(true, pos[i], 1314 HttpAuthHeader.BASIC_TOKEN, 0, 1315 HttpAuthHeader.BASIC_TOKEN.length())) { 1316 String sub = header.substring(pos[i], 1317 (i + 1 < posLen ? pos[i + 1] : headerLen)); 1318 1319 HttpAuthHeader rval = new HttpAuthHeader(sub); 1320 if (rval.isSupportedScheme()) { 1321 // take the first match 1322 return rval; 1323 } 1324 } 1325 } 1326 } 1327 } 1328 1329 return null; 1330 } 1331 1332 /** 1333 * If the content is a redirect or not modified we should not send 1334 * any data into WebCore as that will cause it create a document with 1335 * the data, then when we try to provide the real content, it will assert. 1336 * 1337 * @return True iff the callback should be ignored. 1338 */ 1339 private boolean ignoreCallbacks() { 1340 return (mCancelled || mAuthHeader != null || 1341 (mStatusCode > 300 && mStatusCode < 400)); 1342 } 1343 1344 /** 1345 * Sets the current URL associated with this load. 1346 */ 1347 void setUrl(String url) { 1348 if (url != null) { 1349 mUri = null; 1350 if (URLUtil.isNetworkUrl(url)) { 1351 mUrl = URLUtil.stripAnchor(url); 1352 try { 1353 mUri = new WebAddress(mUrl); 1354 } catch (ParseException e) { 1355 e.printStackTrace(); 1356 } 1357 } else { 1358 mUrl = url; 1359 } 1360 } 1361 } 1362 1363 /** 1364 * Guesses MIME type if one was not specified. Defaults to 'text/html'. In 1365 * addition, tries to guess the MIME type based on the extension. 1366 * 1367 */ 1368 private void guessMimeType() { 1369 // Data urls must have a valid mime type or a blank string for the mime 1370 // type (implying text/plain). 1371 if (URLUtil.isDataUrl(mUrl) && mMimeType.length() != 0) { 1372 cancel(); 1373 final String text = mContext.getString(R.string.httpErrorBadUrl); 1374 handleError(EventHandler.ERROR_BAD_URL, text); 1375 } else { 1376 // Note: This is ok because this is used only for the main content 1377 // of frames. If no content-type was specified, it is fine to 1378 // default to text/html. 1379 mMimeType = "text/html"; 1380 String newMimeType = guessMimeTypeFromExtension(); 1381 if (newMimeType != null) { 1382 mMimeType = newMimeType; 1383 } 1384 } 1385 } 1386 1387 /** 1388 * guess MIME type based on the file extension. 1389 */ 1390 private String guessMimeTypeFromExtension() { 1391 // PENDING: need to normalize url 1392 if (Config.LOGV) { 1393 Log.v(LOGTAG, "guessMimeTypeFromExtension: mURL = " + mUrl); 1394 } 1395 1396 String mimeType = 1397 MimeTypeMap.getSingleton().getMimeTypeFromExtension( 1398 MimeTypeMap.getFileExtensionFromUrl(mUrl)); 1399 1400 if (mimeType != null) { 1401 // XXX: Until the servers send us either correct xhtml or 1402 // text/html, treat application/xhtml+xml as text/html. 1403 if (mimeType.equals("application/xhtml+xml")) { 1404 mimeType = "text/html"; 1405 } 1406 } 1407 1408 return mimeType; 1409 } 1410 1411 /** 1412 * Either send a message to ourselves or queue the message if this is a 1413 * synchronous load. 1414 */ 1415 private void sendMessageInternal(Message msg) { 1416 if (mSynchronous) { 1417 mMessageQueue.add(msg); 1418 } else { 1419 sendMessage(msg); 1420 } 1421 } 1422 1423 /** 1424 * Cycle through our messages for synchronous loads. 1425 */ 1426 /* package */ void loadSynchronousMessages() { 1427 if (Config.DEBUG && !mSynchronous) { 1428 throw new AssertionError(); 1429 } 1430 // Note: this can be called twice if it is a synchronous network load, 1431 // and there is a cache, but it needs to go to network to validate. If 1432 // validation succeed, the CacheLoader is used so this is first called 1433 // from http thread. Then it is called again from WebViewCore thread 1434 // after the load is completed. So make sure the queue is cleared but 1435 // don't set it to null. 1436 for (int size = mMessageQueue.size(); size > 0; size--) { 1437 handleMessage(mMessageQueue.remove(0)); 1438 } 1439 } 1440 1441 //========================================================================= 1442 // native functions 1443 //========================================================================= 1444 1445 /** 1446 * Create a new native response object. 1447 * @param url The url of the resource. 1448 * @param statusCode The HTTP status code. 1449 * @param statusText The HTTP status text. 1450 * @param mimeType HTTP content-type. 1451 * @param expectedLength An estimate of the content length or the length 1452 * given by the server. 1453 * @param encoding HTTP encoding. 1454 * @param expireTime HTTP expires converted to seconds since the epoch. 1455 * @return The native response pointer. 1456 */ 1457 private native int nativeCreateResponse(String url, int statusCode, 1458 String statusText, String mimeType, long expectedLength, 1459 String encoding, long expireTime); 1460 1461 /** 1462 * Add a response header to the native object. 1463 * @param nativeResponse The native pointer. 1464 * @param key String key. 1465 * @param val String value. 1466 */ 1467 private native void nativeSetResponseHeader(int nativeResponse, String key, 1468 String val); 1469 1470 /** 1471 * Dispatch the response. 1472 * @param nativeResponse The native pointer. 1473 */ 1474 private native void nativeReceivedResponse(int nativeResponse); 1475 1476 /** 1477 * Add data to the loader. 1478 * @param data Byte array of data. 1479 * @param length Number of objects in data. 1480 */ 1481 private native void nativeAddData(byte[] data, int length); 1482 1483 /** 1484 * Tell the loader it has finished. 1485 */ 1486 private native void nativeFinished(); 1487 1488 /** 1489 * tell the loader to redirect 1490 * @param baseUrl The base url. 1491 * @param redirectTo The url to redirect to. 1492 * @param nativeResponse The native pointer. 1493 * @return The new url that the resource redirected to. 1494 */ 1495 private native String nativeRedirectedToUrl(String baseUrl, 1496 String redirectTo, int nativeResponse); 1497 1498 /** 1499 * Tell the loader there is error 1500 * @param id 1501 * @param desc 1502 * @param failingUrl The url that failed. 1503 */ 1504 private native void nativeError(int id, String desc, String failingUrl); 1505 1506} 1507