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