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