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