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