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