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