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