blob: 5d17691d8998d60bc27704dd798b3d7bbb0e8990 [file] [log] [blame]
Grace Kloba22ac16e2009-10-07 18:00:23 -07001/*
2 * Copyright (C) 2009 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 com.android.browser;
18
19import java.io.File;
Leon Scroggins58d56c62010-01-28 15:12:40 -050020import java.util.ArrayList;
Leon Scroggins9df94972010-03-08 18:20:35 -050021import java.util.HashMap;
22import java.util.Iterator;
Grace Kloba22ac16e2009-10-07 18:00:23 -070023import java.util.LinkedList;
Leon Scroggins9df94972010-03-08 18:20:35 -050024import java.util.Map;
Grace Kloba22ac16e2009-10-07 18:00:23 -070025import java.util.Vector;
26
27import android.app.AlertDialog;
Leon Scroggins58d56c62010-01-28 15:12:40 -050028import android.app.SearchManager;
Grace Kloba22ac16e2009-10-07 18:00:23 -070029import android.content.ContentResolver;
30import android.content.ContentValues;
31import android.content.DialogInterface;
32import android.content.DialogInterface.OnCancelListener;
Leon Scroggins58d56c62010-01-28 15:12:40 -050033import android.content.Intent;
Grace Kloba22ac16e2009-10-07 18:00:23 -070034import android.database.Cursor;
35import android.database.sqlite.SQLiteDatabase;
36import android.database.sqlite.SQLiteException;
37import android.graphics.Bitmap;
38import android.net.Uri;
39import android.net.http.SslError;
40import android.os.AsyncTask;
41import android.os.Bundle;
42import android.os.Message;
Kristian Monsen4dce3bf2010-02-02 13:37:09 +000043import android.os.SystemClock;
Grace Kloba22ac16e2009-10-07 18:00:23 -070044import android.provider.Browser;
Leon Scrogginsa1cc3fd2010-02-01 16:14:11 -050045import android.speech.RecognizerResultsIntent;
Grace Kloba22ac16e2009-10-07 18:00:23 -070046import android.util.Log;
47import android.view.KeyEvent;
48import android.view.LayoutInflater;
49import android.view.View;
50import android.view.ViewGroup;
51import android.view.View.OnClickListener;
Ben Murdochc42addf2010-01-28 15:19:59 +000052import android.webkit.ConsoleMessage;
Grace Kloba22ac16e2009-10-07 18:00:23 -070053import android.webkit.CookieSyncManager;
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -050054import android.webkit.DownloadListener;
Grace Kloba22ac16e2009-10-07 18:00:23 -070055import android.webkit.GeolocationPermissions;
56import android.webkit.HttpAuthHandler;
57import android.webkit.SslErrorHandler;
58import android.webkit.URLUtil;
59import android.webkit.ValueCallback;
60import android.webkit.WebBackForwardList;
Leon Scroggins0c75a8e2010-03-03 16:40:58 -050061import android.webkit.WebBackForwardListClient;
Grace Kloba22ac16e2009-10-07 18:00:23 -070062import android.webkit.WebChromeClient;
63import android.webkit.WebHistoryItem;
64import android.webkit.WebIconDatabase;
65import android.webkit.WebStorage;
66import android.webkit.WebView;
67import android.webkit.WebViewClient;
68import android.widget.FrameLayout;
69import android.widget.ImageButton;
70import android.widget.LinearLayout;
71import android.widget.TextView;
72
Leon Scroggins1fe13a52010-02-09 15:31:26 -050073import com.android.common.speech.LoggingEvents;
74
Grace Kloba22ac16e2009-10-07 18:00:23 -070075/**
76 * Class for maintaining Tabs with a main WebView and a subwindow.
77 */
78class Tab {
79 // Log Tag
80 private static final String LOGTAG = "Tab";
Ben Murdochc42addf2010-01-28 15:19:59 +000081 // Special case the logtag for messages for the Console to make it easier to
82 // filter them and match the logtag used for these messages in older versions
83 // of the browser.
84 private static final String CONSOLE_LOGTAG = "browser";
85
Grace Kloba22ac16e2009-10-07 18:00:23 -070086 // The Geolocation permissions prompt
87 private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt;
88 // Main WebView wrapper
89 private View mContainer;
90 // Main WebView
91 private WebView mMainView;
92 // Subwindow container
93 private View mSubViewContainer;
94 // Subwindow WebView
95 private WebView mSubView;
96 // Saved bundle for when we are running low on memory. It contains the
97 // information needed to restore the WebView if the user goes back to the
98 // tab.
99 private Bundle mSavedState;
100 // Data used when displaying the tab in the picker.
101 private PickerData mPickerData;
102 // Parent Tab. This is the Tab that created this Tab, or null if the Tab was
103 // created by the UI
104 private Tab mParentTab;
105 // Tab that constructed by this Tab. This is used when this Tab is
106 // destroyed, it clears all mParentTab values in the children.
107 private Vector<Tab> mChildTabs;
108 // If true, the tab will be removed when back out of the first page.
109 private boolean mCloseOnExit;
110 // If true, the tab is in the foreground of the current activity.
111 private boolean mInForeground;
112 // If true, the tab is in loading state.
113 private boolean mInLoad;
Kristian Monsen4dce3bf2010-02-02 13:37:09 +0000114 // The time the load started, used to find load page time
115 private long mLoadStartTime;
Grace Kloba22ac16e2009-10-07 18:00:23 -0700116 // Application identifier used to find tabs that another application wants
117 // to reuse.
118 private String mAppId;
119 // Keep the original url around to avoid killing the old WebView if the url
120 // has not changed.
121 private String mOriginalUrl;
122 // Error console for the tab
123 private ErrorConsoleView mErrorConsole;
124 // the lock icon type and previous lock icon type for the tab
125 private int mLockIconType;
126 private int mPrevLockIconType;
127 // Inflation service for making subwindows.
128 private final LayoutInflater mInflateService;
129 // The BrowserActivity which owners the Tab
130 private final BrowserActivity mActivity;
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -0500131 // The listener that gets invoked when a download is started from the
132 // mMainView
133 private final DownloadListener mDownloadListener;
Leon Scroggins0c75a8e2010-03-03 16:40:58 -0500134 // Listener used to know when we move forward or back in the history list.
135 private final WebBackForwardListClient mWebBackForwardListClient;
Grace Kloba22ac16e2009-10-07 18:00:23 -0700136
137 // AsyncTask for downloading touch icons
138 DownloadTouchIcon mTouchIconLoader;
139
140 // Extra saved information for displaying the tab in the picker.
141 private static class PickerData {
142 String mUrl;
143 String mTitle;
144 Bitmap mFavicon;
145 }
146
147 // Used for saving and restoring each Tab
148 static final String WEBVIEW = "webview";
149 static final String NUMTABS = "numTabs";
150 static final String CURRTAB = "currentTab";
151 static final String CURRURL = "currentUrl";
152 static final String CURRTITLE = "currentTitle";
153 static final String CURRPICTURE = "currentPicture";
154 static final String CLOSEONEXIT = "closeonexit";
155 static final String PARENTTAB = "parentTab";
156 static final String APPID = "appid";
157 static final String ORIGINALURL = "originalUrl";
158
159 // -------------------------------------------------------------------------
160
Leon Scroggins58d56c62010-01-28 15:12:40 -0500161 /**
162 * Private information regarding the latest voice search. If the Tab is not
163 * in voice search mode, this will be null.
164 */
165 private VoiceSearchData mVoiceSearchData;
166 /**
167 * Return whether the tab is in voice search mode.
168 */
169 public boolean isInVoiceSearchMode() {
170 return mVoiceSearchData != null;
171 }
172 /**
Leon Scroggins1fe13a52010-02-09 15:31:26 -0500173 * Return true if the voice search Intent came with a String identifying
174 * that Google provided the Intent.
175 */
176 public boolean voiceSearchSourceIsGoogle() {
177 return mVoiceSearchData != null && mVoiceSearchData.mSourceIsGoogle;
178 }
179 /**
Leon Scroggins58d56c62010-01-28 15:12:40 -0500180 * Get the title to display for the current voice search page. If the Tab
181 * is not in voice search mode, return null.
182 */
183 public String getVoiceDisplayTitle() {
184 if (mVoiceSearchData == null) return null;
185 return mVoiceSearchData.mLastVoiceSearchTitle;
186 }
187 /**
188 * Get the latest array of voice search results, to be passed to the
189 * BrowserProvider. If the Tab is not in voice search mode, return null.
190 */
191 public ArrayList<String> getVoiceSearchResults() {
192 if (mVoiceSearchData == null) return null;
193 return mVoiceSearchData.mVoiceSearchResults;
194 }
195 /**
196 * Activate voice search mode.
197 * @param intent Intent which has the results to use, or an index into the
198 * results when reusing the old results.
199 */
200 /* package */ void activateVoiceSearchMode(Intent intent) {
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500201 int index = 0;
Leon Scroggins58d56c62010-01-28 15:12:40 -0500202 ArrayList<String> results = intent.getStringArrayListExtra(
Leon Scrogginsa1cc3fd2010-02-01 16:14:11 -0500203 RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS);
Leon Scroggins58d56c62010-01-28 15:12:40 -0500204 if (results != null) {
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500205 ArrayList<String> urls = intent.getStringArrayListExtra(
206 RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS);
207 ArrayList<String> htmls = intent.getStringArrayListExtra(
208 RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_HTML);
209 ArrayList<String> baseUrls = intent.getStringArrayListExtra(
210 RecognizerResultsIntent
211 .EXTRA_VOICE_SEARCH_RESULT_HTML_BASE_URLS);
Leon Scroggins58d56c62010-01-28 15:12:40 -0500212 // This tab is now entering voice search mode for the first time, or
213 // a new voice search was done.
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500214 int size = results.size();
215 if (urls == null || size != urls.size()) {
Leon Scroggins58d56c62010-01-28 15:12:40 -0500216 throw new AssertionError("improper extras passed in Intent");
217 }
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500218 if (htmls == null || htmls.size() != size || baseUrls == null ||
219 (baseUrls.size() != size && baseUrls.size() != 1)) {
220 // If either of these arrays are empty/incorrectly sized, ignore
221 // them.
222 htmls = null;
223 baseUrls = null;
224 }
225 mVoiceSearchData = new VoiceSearchData(results, urls, htmls,
226 baseUrls);
Leon Scroggins9df94972010-03-08 18:20:35 -0500227 mVoiceSearchData.mHeaders = intent.getParcelableArrayListExtra(
228 RecognizerResultsIntent
229 .EXTRA_VOICE_SEARCH_RESULT_HTTP_HEADERS);
Leon Scroggins1fe13a52010-02-09 15:31:26 -0500230 mVoiceSearchData.mSourceIsGoogle = intent.getBooleanExtra(
231 VoiceSearchData.SOURCE_IS_GOOGLE, false);
Leon Scrogginse10dde52010-03-08 19:53:03 -0500232 mVoiceSearchData.mVoiceSearchIntent = intent;
233 }
234 String extraData = intent.getStringExtra(
235 SearchManager.EXTRA_DATA_KEY);
236 if (extraData != null) {
237 index = Integer.parseInt(extraData);
238 if (index >= mVoiceSearchData.mVoiceSearchResults.size()) {
239 throw new AssertionError("index must be less than "
240 + "size of mVoiceSearchResults");
241 }
242 if (mVoiceSearchData.mSourceIsGoogle) {
243 Intent logIntent = new Intent(
244 LoggingEvents.ACTION_LOG_EVENT);
245 logIntent.putExtra(LoggingEvents.EXTRA_EVENT,
246 LoggingEvents.VoiceSearch.N_BEST_CHOOSE);
247 logIntent.putExtra(
248 LoggingEvents.VoiceSearch.EXTRA_N_BEST_CHOOSE_INDEX,
249 index);
250 mActivity.sendBroadcast(logIntent);
251 }
252 if (mVoiceSearchData.mVoiceSearchIntent != null) {
253 mVoiceSearchData.mVoiceSearchIntent.putExtra(
254 SearchManager.EXTRA_DATA_KEY, extraData);
Leon Scroggins58d56c62010-01-28 15:12:40 -0500255 }
256 }
257 mVoiceSearchData.mLastVoiceSearchTitle
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500258 = mVoiceSearchData.mVoiceSearchResults.get(index);
Leon Scroggins58d56c62010-01-28 15:12:40 -0500259 if (mInForeground) {
260 mActivity.showVoiceTitleBar(mVoiceSearchData.mLastVoiceSearchTitle);
261 }
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500262 if (mVoiceSearchData.mVoiceSearchHtmls != null) {
263 // When index was found it was already ensured that it was valid
264 String uriString = mVoiceSearchData.mVoiceSearchHtmls.get(index);
265 if (uriString != null) {
266 Uri dataUri = Uri.parse(uriString);
267 if (RecognizerResultsIntent.URI_SCHEME_INLINE.equals(
268 dataUri.getScheme())) {
269 // If there is only one base URL, use it. If there are
270 // more, there will be one for each index, so use the base
271 // URL corresponding to the index.
272 String baseUrl = mVoiceSearchData.mVoiceSearchBaseUrls.get(
273 mVoiceSearchData.mVoiceSearchBaseUrls.size() > 1 ?
274 index : 0);
275 mVoiceSearchData.mLastVoiceSearchUrl = baseUrl;
276 mMainView.loadDataWithBaseURL(baseUrl,
277 uriString.substring(RecognizerResultsIntent
278 .URI_SCHEME_INLINE.length() + 1), "text/html",
279 "utf-8", baseUrl);
280 return;
281 }
282 }
283 }
Leon Scroggins58d56c62010-01-28 15:12:40 -0500284 mVoiceSearchData.mLastVoiceSearchUrl
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500285 = mVoiceSearchData.mVoiceSearchUrls.get(index);
286 if (null == mVoiceSearchData.mLastVoiceSearchUrl) {
287 mVoiceSearchData.mLastVoiceSearchUrl = mActivity.smartUrlFilter(
288 mVoiceSearchData.mLastVoiceSearchTitle);
289 }
Leon Scroggins9df94972010-03-08 18:20:35 -0500290 Map<String, String> headers = null;
291 if (mVoiceSearchData.mHeaders != null) {
292 int bundleIndex = mVoiceSearchData.mHeaders.size() == 1 ? 0
293 : index;
294 Bundle bundle = mVoiceSearchData.mHeaders.get(bundleIndex);
295 if (bundle != null && !bundle.isEmpty()) {
296 Iterator<String> iter = bundle.keySet().iterator();
297 headers = new HashMap<String, String>();
298 while (iter.hasNext()) {
299 String key = iter.next();
300 headers.put(key, bundle.getString(key));
301 }
302 }
303 }
304 mMainView.loadUrl(mVoiceSearchData.mLastVoiceSearchUrl, headers);
Leon Scroggins58d56c62010-01-28 15:12:40 -0500305 }
306 /* package */ static class VoiceSearchData {
Leon Scroggins58d56c62010-01-28 15:12:40 -0500307 public VoiceSearchData(ArrayList<String> results,
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500308 ArrayList<String> urls, ArrayList<String> htmls,
309 ArrayList<String> baseUrls) {
Leon Scroggins58d56c62010-01-28 15:12:40 -0500310 mVoiceSearchResults = results;
311 mVoiceSearchUrls = urls;
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500312 mVoiceSearchHtmls = htmls;
313 mVoiceSearchBaseUrls = baseUrls;
Leon Scroggins58d56c62010-01-28 15:12:40 -0500314 }
315 /*
316 * ArrayList of suggestions to be displayed when opening the
317 * SearchManager
318 */
319 public ArrayList<String> mVoiceSearchResults;
320 /*
321 * ArrayList of urls, associated with the suggestions in
322 * mVoiceSearchResults.
323 */
324 public ArrayList<String> mVoiceSearchUrls;
325 /*
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500326 * ArrayList holding content to load for each item in
327 * mVoiceSearchResults.
328 */
329 public ArrayList<String> mVoiceSearchHtmls;
330 /*
331 * ArrayList holding base urls for the items in mVoiceSearchResults.
332 * If non null, this will either have the same size as
333 * mVoiceSearchResults or have a size of 1, in which case all will use
334 * the same base url
335 */
336 public ArrayList<String> mVoiceSearchBaseUrls;
337 /*
Leon Scroggins58d56c62010-01-28 15:12:40 -0500338 * The last url provided by voice search. Used for comparison to see if
Leon Scroggins82c1baa2010-02-02 16:10:57 -0500339 * we are going to a page by some method besides voice search.
Leon Scroggins58d56c62010-01-28 15:12:40 -0500340 */
341 public String mLastVoiceSearchUrl;
342 /**
343 * The last title used for voice search. Needed to update the title bar
344 * when switching tabs.
345 */
346 public String mLastVoiceSearchTitle;
Leon Scroggins1fe13a52010-02-09 15:31:26 -0500347 /**
348 * Whether the Intent which turned on voice search mode contained the
349 * String signifying that Google was the source.
350 */
351 public boolean mSourceIsGoogle;
352 /**
Leon Scroggins9df94972010-03-08 18:20:35 -0500353 * List of headers to be passed into the WebView containing location
354 * information
355 */
356 public ArrayList<Bundle> mHeaders;
357 /**
Leon Scroggins0c75a8e2010-03-03 16:40:58 -0500358 * The Intent used to invoke voice search. Placed on the
359 * WebHistoryItem so that when coming back to a previous voice search
360 * page we can again activate voice search.
361 */
Leon Scrogginse10dde52010-03-08 19:53:03 -0500362 public Intent mVoiceSearchIntent;
Leon Scroggins0c75a8e2010-03-03 16:40:58 -0500363 /**
Leon Scroggins1fe13a52010-02-09 15:31:26 -0500364 * String used to identify Google as the source of voice search.
365 */
366 public static String SOURCE_IS_GOOGLE
367 = "android.speech.extras.SOURCE_IS_GOOGLE";
Leon Scroggins58d56c62010-01-28 15:12:40 -0500368 }
369
Grace Kloba22ac16e2009-10-07 18:00:23 -0700370 // Container class for the next error dialog that needs to be displayed
371 private class ErrorDialog {
372 public final int mTitle;
373 public final String mDescription;
374 public final int mError;
375 ErrorDialog(int title, String desc, int error) {
376 mTitle = title;
377 mDescription = desc;
378 mError = error;
379 }
380 };
381
382 private void processNextError() {
383 if (mQueuedErrors == null) {
384 return;
385 }
386 // The first one is currently displayed so just remove it.
387 mQueuedErrors.removeFirst();
388 if (mQueuedErrors.size() == 0) {
389 mQueuedErrors = null;
390 return;
391 }
392 showError(mQueuedErrors.getFirst());
393 }
394
395 private DialogInterface.OnDismissListener mDialogListener =
396 new DialogInterface.OnDismissListener() {
397 public void onDismiss(DialogInterface d) {
398 processNextError();
399 }
400 };
401 private LinkedList<ErrorDialog> mQueuedErrors;
402
403 private void queueError(int err, String desc) {
404 if (mQueuedErrors == null) {
405 mQueuedErrors = new LinkedList<ErrorDialog>();
406 }
407 for (ErrorDialog d : mQueuedErrors) {
408 if (d.mError == err) {
409 // Already saw a similar error, ignore the new one.
410 return;
411 }
412 }
413 ErrorDialog errDialog = new ErrorDialog(
414 err == WebViewClient.ERROR_FILE_NOT_FOUND ?
415 R.string.browserFrameFileErrorLabel :
416 R.string.browserFrameNetworkErrorLabel,
417 desc, err);
418 mQueuedErrors.addLast(errDialog);
419
420 // Show the dialog now if the queue was empty and it is in foreground
421 if (mQueuedErrors.size() == 1 && mInForeground) {
422 showError(errDialog);
423 }
424 }
425
426 private void showError(ErrorDialog errDialog) {
427 if (mInForeground) {
428 AlertDialog d = new AlertDialog.Builder(mActivity)
429 .setTitle(errDialog.mTitle)
430 .setMessage(errDialog.mDescription)
431 .setPositiveButton(R.string.ok, null)
432 .create();
433 d.setOnDismissListener(mDialogListener);
434 d.show();
435 }
436 }
437
438 // -------------------------------------------------------------------------
439 // WebViewClient implementation for the main WebView
440 // -------------------------------------------------------------------------
441
442 private final WebViewClient mWebViewClient = new WebViewClient() {
Leon Scroggins4a64a8a2010-03-02 17:57:40 -0500443 private Message mDontResend;
444 private Message mResend;
Grace Kloba22ac16e2009-10-07 18:00:23 -0700445 @Override
446 public void onPageStarted(WebView view, String url, Bitmap favicon) {
447 mInLoad = true;
Kristian Monsen4dce3bf2010-02-02 13:37:09 +0000448 mLoadStartTime = SystemClock.uptimeMillis();
Leon Scroggins58d56c62010-01-28 15:12:40 -0500449 if (mVoiceSearchData != null
450 && !url.equals(mVoiceSearchData.mLastVoiceSearchUrl)) {
Leon Scroggins1fe13a52010-02-09 15:31:26 -0500451 if (mVoiceSearchData.mSourceIsGoogle) {
452 Intent i = new Intent(LoggingEvents.ACTION_LOG_EVENT);
453 i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
454 mActivity.sendBroadcast(i);
455 }
Leon Scroggins58d56c62010-01-28 15:12:40 -0500456 mVoiceSearchData = null;
457 if (mInForeground) {
458 mActivity.revertVoiceTitleBar();
459 }
460 }
Grace Kloba22ac16e2009-10-07 18:00:23 -0700461
462 // We've started to load a new page. If there was a pending message
463 // to save a screenshot then we will now take the new page and save
464 // an incorrect screenshot. Therefore, remove any pending thumbnail
465 // messages from the queue.
466 mActivity.removeMessages(BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL,
467 view);
468
469 // If we start a touch icon load and then load a new page, we don't
470 // want to cancel the current touch icon loader. But, we do want to
471 // create a new one when the touch icon url is known.
472 if (mTouchIconLoader != null) {
473 mTouchIconLoader.mTab = null;
474 mTouchIconLoader = null;
475 }
476
477 // reset the error console
478 if (mErrorConsole != null) {
479 mErrorConsole.clearErrorMessages();
480 if (mActivity.shouldShowErrorConsole()) {
481 mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
482 }
483 }
484
485 // update the bookmark database for favicon
486 if (favicon != null) {
487 BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity
488 .getContentResolver(), view.getOriginalUrl(), view
489 .getUrl(), favicon);
490 }
491
492 // reset sync timer to avoid sync starts during loading a page
493 CookieSyncManager.getInstance().resetSync();
494
495 if (!mActivity.isNetworkUp()) {
496 view.setNetworkAvailable(false);
497 }
498
499 // finally update the UI in the activity if it is in the foreground
500 if (mInForeground) {
501 mActivity.onPageStarted(view, url, favicon);
502 }
503 }
504
505 @Override
506 public void onPageFinished(WebView view, String url) {
Kristian Monsen4dce3bf2010-02-02 13:37:09 +0000507 LogTag.logPageFinishedLoading(
508 url, SystemClock.uptimeMillis() - mLoadStartTime);
Grace Kloba22ac16e2009-10-07 18:00:23 -0700509 mInLoad = false;
510
511 if (mInForeground && !mActivity.didUserStopLoading()
512 || !mInForeground) {
513 // Only update the bookmark screenshot if the user did not
514 // cancel the load early.
515 mActivity.postMessage(
516 BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, 0, 0, view,
517 500);
518 }
519
520 // finally update the UI in the activity if it is in the foreground
521 if (mInForeground) {
522 mActivity.onPageFinished(view, url);
523 }
524 }
525
526 // return true if want to hijack the url to let another app to handle it
527 @Override
528 public boolean shouldOverrideUrlLoading(WebView view, String url) {
529 if (mInForeground) {
530 return mActivity.shouldOverrideUrlLoading(view, url);
531 } else {
532 return false;
533 }
534 }
535
536 /**
537 * Updates the lock icon. This method is called when we discover another
538 * resource to be loaded for this page (for example, javascript). While
539 * we update the icon type, we do not update the lock icon itself until
540 * we are done loading, it is slightly more secure this way.
541 */
542 @Override
543 public void onLoadResource(WebView view, String url) {
544 if (url != null && url.length() > 0) {
545 // It is only if the page claims to be secure that we may have
546 // to update the lock:
547 if (mLockIconType == BrowserActivity.LOCK_ICON_SECURE) {
548 // If NOT a 'safe' url, change the lock to mixed content!
549 if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url)
550 || URLUtil.isAboutUrl(url))) {
551 mLockIconType = BrowserActivity.LOCK_ICON_MIXED;
552 }
553 }
554 }
555 }
556
557 /**
Grace Kloba22ac16e2009-10-07 18:00:23 -0700558 * Show a dialog informing the user of the network error reported by
559 * WebCore if it is in the foreground.
560 */
561 @Override
562 public void onReceivedError(WebView view, int errorCode,
563 String description, String failingUrl) {
564 if (errorCode != WebViewClient.ERROR_HOST_LOOKUP &&
565 errorCode != WebViewClient.ERROR_CONNECT &&
566 errorCode != WebViewClient.ERROR_BAD_URL &&
567 errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME &&
568 errorCode != WebViewClient.ERROR_FILE) {
569 queueError(errorCode, description);
570 }
571 Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl
572 + " " + description);
573
574 // We need to reset the title after an error if it is in foreground.
575 if (mInForeground) {
576 mActivity.resetTitleAndRevertLockIcon();
577 }
578 }
579
580 /**
581 * Check with the user if it is ok to resend POST data as the page they
582 * are trying to navigate to is the result of a POST.
583 */
584 @Override
585 public void onFormResubmission(WebView view, final Message dontResend,
586 final Message resend) {
587 if (!mInForeground) {
588 dontResend.sendToTarget();
589 return;
590 }
Leon Scroggins4a64a8a2010-03-02 17:57:40 -0500591 if (mDontResend != null) {
592 Log.w(LOGTAG, "onFormResubmission should not be called again "
593 + "while dialog is still up");
594 dontResend.sendToTarget();
595 return;
596 }
597 mDontResend = dontResend;
598 mResend = resend;
Grace Kloba22ac16e2009-10-07 18:00:23 -0700599 new AlertDialog.Builder(mActivity).setTitle(
600 R.string.browserFrameFormResubmitLabel).setMessage(
601 R.string.browserFrameFormResubmitMessage)
602 .setPositiveButton(R.string.ok,
603 new DialogInterface.OnClickListener() {
604 public void onClick(DialogInterface dialog,
605 int which) {
Leon Scroggins4a64a8a2010-03-02 17:57:40 -0500606 if (mResend != null) {
607 mResend.sendToTarget();
608 mResend = null;
609 mDontResend = null;
610 }
Grace Kloba22ac16e2009-10-07 18:00:23 -0700611 }
612 }).setNegativeButton(R.string.cancel,
613 new DialogInterface.OnClickListener() {
614 public void onClick(DialogInterface dialog,
615 int which) {
Leon Scroggins4a64a8a2010-03-02 17:57:40 -0500616 if (mDontResend != null) {
617 mDontResend.sendToTarget();
618 mResend = null;
619 mDontResend = null;
620 }
Grace Kloba22ac16e2009-10-07 18:00:23 -0700621 }
622 }).setOnCancelListener(new OnCancelListener() {
623 public void onCancel(DialogInterface dialog) {
Leon Scroggins4a64a8a2010-03-02 17:57:40 -0500624 if (mDontResend != null) {
625 mDontResend.sendToTarget();
626 mResend = null;
627 mDontResend = null;
628 }
Grace Kloba22ac16e2009-10-07 18:00:23 -0700629 }
630 }).show();
631 }
632
633 /**
634 * Insert the url into the visited history database.
635 * @param url The url to be inserted.
636 * @param isReload True if this url is being reloaded.
637 * FIXME: Not sure what to do when reloading the page.
638 */
639 @Override
640 public void doUpdateVisitedHistory(WebView view, String url,
641 boolean isReload) {
642 if (url.regionMatches(true, 0, "about:", 0, 6)) {
643 return;
644 }
645 // remove "client" before updating it to the history so that it wont
646 // show up in the auto-complete list.
647 int index = url.indexOf("client=ms-");
648 if (index > 0 && url.contains(".google.")) {
649 int end = url.indexOf('&', index);
650 if (end > 0) {
651 url = url.substring(0, index)
652 .concat(url.substring(end + 1));
653 } else {
654 // the url.charAt(index-1) should be either '?' or '&'
655 url = url.substring(0, index-1);
656 }
657 }
658 Browser.updateVisitedHistory(mActivity.getContentResolver(), url,
659 true);
660 WebIconDatabase.getInstance().retainIconForPageUrl(url);
661 }
662
663 /**
664 * Displays SSL error(s) dialog to the user.
665 */
666 @Override
667 public void onReceivedSslError(final WebView view,
668 final SslErrorHandler handler, final SslError error) {
669 if (!mInForeground) {
670 handler.cancel();
671 return;
672 }
673 if (BrowserSettings.getInstance().showSecurityWarnings()) {
674 final LayoutInflater factory =
675 LayoutInflater.from(mActivity);
676 final View warningsView =
677 factory.inflate(R.layout.ssl_warnings, null);
678 final LinearLayout placeholder =
679 (LinearLayout)warningsView.findViewById(R.id.placeholder);
680
681 if (error.hasError(SslError.SSL_UNTRUSTED)) {
682 LinearLayout ll = (LinearLayout)factory
683 .inflate(R.layout.ssl_warning, null);
684 ((TextView)ll.findViewById(R.id.warning))
685 .setText(R.string.ssl_untrusted);
686 placeholder.addView(ll);
687 }
688
689 if (error.hasError(SslError.SSL_IDMISMATCH)) {
690 LinearLayout ll = (LinearLayout)factory
691 .inflate(R.layout.ssl_warning, null);
692 ((TextView)ll.findViewById(R.id.warning))
693 .setText(R.string.ssl_mismatch);
694 placeholder.addView(ll);
695 }
696
697 if (error.hasError(SslError.SSL_EXPIRED)) {
698 LinearLayout ll = (LinearLayout)factory
699 .inflate(R.layout.ssl_warning, null);
700 ((TextView)ll.findViewById(R.id.warning))
701 .setText(R.string.ssl_expired);
702 placeholder.addView(ll);
703 }
704
705 if (error.hasError(SslError.SSL_NOTYETVALID)) {
706 LinearLayout ll = (LinearLayout)factory
707 .inflate(R.layout.ssl_warning, null);
708 ((TextView)ll.findViewById(R.id.warning))
709 .setText(R.string.ssl_not_yet_valid);
710 placeholder.addView(ll);
711 }
712
713 new AlertDialog.Builder(mActivity).setTitle(
714 R.string.security_warning).setIcon(
715 android.R.drawable.ic_dialog_alert).setView(
716 warningsView).setPositiveButton(R.string.ssl_continue,
717 new DialogInterface.OnClickListener() {
718 public void onClick(DialogInterface dialog,
719 int whichButton) {
720 handler.proceed();
721 }
722 }).setNeutralButton(R.string.view_certificate,
723 new DialogInterface.OnClickListener() {
724 public void onClick(DialogInterface dialog,
725 int whichButton) {
726 mActivity.showSSLCertificateOnError(view,
727 handler, error);
728 }
729 }).setNegativeButton(R.string.cancel,
730 new DialogInterface.OnClickListener() {
731 public void onClick(DialogInterface dialog,
732 int whichButton) {
733 handler.cancel();
734 mActivity.resetTitleAndRevertLockIcon();
735 }
736 }).setOnCancelListener(
737 new DialogInterface.OnCancelListener() {
738 public void onCancel(DialogInterface dialog) {
739 handler.cancel();
740 mActivity.resetTitleAndRevertLockIcon();
741 }
742 }).show();
743 } else {
744 handler.proceed();
745 }
746 }
747
748 /**
749 * Handles an HTTP authentication request.
750 *
751 * @param handler The authentication handler
752 * @param host The host
753 * @param realm The realm
754 */
755 @Override
756 public void onReceivedHttpAuthRequest(WebView view,
757 final HttpAuthHandler handler, final String host,
758 final String realm) {
759 String username = null;
760 String password = null;
761
762 boolean reuseHttpAuthUsernamePassword = handler
763 .useHttpAuthUsernamePassword();
764
765 if (reuseHttpAuthUsernamePassword && mMainView != null) {
766 String[] credentials = mMainView.getHttpAuthUsernamePassword(
767 host, realm);
768 if (credentials != null && credentials.length == 2) {
769 username = credentials[0];
770 password = credentials[1];
771 }
772 }
773
774 if (username != null && password != null) {
775 handler.proceed(username, password);
776 } else {
777 if (mInForeground) {
778 mActivity.showHttpAuthentication(handler, host, realm,
779 null, null, null, 0);
780 } else {
781 handler.cancel();
782 }
783 }
784 }
785
786 @Override
787 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
788 if (!mInForeground) {
789 return false;
790 }
791 if (mActivity.isMenuDown()) {
792 // only check shortcut key when MENU is held
793 return mActivity.getWindow().isShortcutKey(event.getKeyCode(),
794 event);
795 } else {
796 return false;
797 }
798 }
799
800 @Override
801 public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
802 if (!mInForeground) {
803 return;
804 }
805 if (event.isDown()) {
806 mActivity.onKeyDown(event.getKeyCode(), event);
807 } else {
808 mActivity.onKeyUp(event.getKeyCode(), event);
809 }
810 }
811 };
812
813 // -------------------------------------------------------------------------
814 // WebChromeClient implementation for the main WebView
815 // -------------------------------------------------------------------------
816
817 private final WebChromeClient mWebChromeClient = new WebChromeClient() {
818 // Helper method to create a new tab or sub window.
819 private void createWindow(final boolean dialog, final Message msg) {
820 WebView.WebViewTransport transport =
821 (WebView.WebViewTransport) msg.obj;
822 if (dialog) {
823 createSubWindow();
824 mActivity.attachSubWindow(Tab.this);
825 transport.setWebView(mSubView);
826 } else {
827 final Tab newTab = mActivity.openTabAndShow(
828 BrowserActivity.EMPTY_URL_DATA, false, null);
829 if (newTab != Tab.this) {
830 Tab.this.addChildTab(newTab);
831 }
832 transport.setWebView(newTab.getWebView());
833 }
834 msg.sendToTarget();
835 }
836
837 @Override
838 public boolean onCreateWindow(WebView view, final boolean dialog,
839 final boolean userGesture, final Message resultMsg) {
840 // only allow new window or sub window for the foreground case
841 if (!mInForeground) {
842 return false;
843 }
844 // Short-circuit if we can't create any more tabs or sub windows.
845 if (dialog && mSubView != null) {
846 new AlertDialog.Builder(mActivity)
847 .setTitle(R.string.too_many_subwindows_dialog_title)
848 .setIcon(android.R.drawable.ic_dialog_alert)
849 .setMessage(R.string.too_many_subwindows_dialog_message)
850 .setPositiveButton(R.string.ok, null)
851 .show();
852 return false;
853 } else if (!mActivity.getTabControl().canCreateNewTab()) {
854 new AlertDialog.Builder(mActivity)
855 .setTitle(R.string.too_many_windows_dialog_title)
856 .setIcon(android.R.drawable.ic_dialog_alert)
857 .setMessage(R.string.too_many_windows_dialog_message)
858 .setPositiveButton(R.string.ok, null)
859 .show();
860 return false;
861 }
862
863 // Short-circuit if this was a user gesture.
864 if (userGesture) {
865 createWindow(dialog, resultMsg);
866 return true;
867 }
868
869 // Allow the popup and create the appropriate window.
870 final AlertDialog.OnClickListener allowListener =
871 new AlertDialog.OnClickListener() {
872 public void onClick(DialogInterface d,
873 int which) {
874 createWindow(dialog, resultMsg);
875 }
876 };
877
878 // Block the popup by returning a null WebView.
879 final AlertDialog.OnClickListener blockListener =
880 new AlertDialog.OnClickListener() {
881 public void onClick(DialogInterface d, int which) {
882 resultMsg.sendToTarget();
883 }
884 };
885
886 // Build a confirmation dialog to display to the user.
887 final AlertDialog d =
888 new AlertDialog.Builder(mActivity)
889 .setTitle(R.string.attention)
890 .setIcon(android.R.drawable.ic_dialog_alert)
891 .setMessage(R.string.popup_window_attempt)
892 .setPositiveButton(R.string.allow, allowListener)
893 .setNegativeButton(R.string.block, blockListener)
894 .setCancelable(false)
895 .create();
896
897 // Show the confirmation dialog.
898 d.show();
899 return true;
900 }
901
902 @Override
Patrick Scotteb5061b2009-11-18 15:00:30 -0500903 public void onRequestFocus(WebView view) {
904 if (!mInForeground) {
905 mActivity.switchToTab(mActivity.getTabControl().getTabIndex(
906 Tab.this));
907 }
908 }
909
910 @Override
Grace Kloba22ac16e2009-10-07 18:00:23 -0700911 public void onCloseWindow(WebView window) {
912 if (mParentTab != null) {
913 // JavaScript can only close popup window.
914 if (mInForeground) {
915 mActivity.switchToTab(mActivity.getTabControl()
916 .getTabIndex(mParentTab));
917 }
918 mActivity.closeTab(Tab.this);
919 }
920 }
921
922 @Override
923 public void onProgressChanged(WebView view, int newProgress) {
924 if (newProgress == 100) {
925 // sync cookies and cache promptly here.
926 CookieSyncManager.getInstance().sync();
927 }
928 if (mInForeground) {
929 mActivity.onProgressChanged(view, newProgress);
930 }
931 }
932
933 @Override
934 public void onReceivedTitle(WebView view, String title) {
935 String url = view.getUrl();
936 if (mInForeground) {
937 // here, if url is null, we want to reset the title
938 mActivity.setUrlTitle(url, title);
939 }
940 if (url == null ||
941 url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
942 return;
943 }
944 // See if we can find the current url in our history database and
945 // add the new title to it.
946 if (url.startsWith("http://www.")) {
947 url = url.substring(11);
948 } else if (url.startsWith("http://")) {
949 url = url.substring(4);
950 }
951 try {
952 final ContentResolver cr = mActivity.getContentResolver();
953 url = "%" + url;
954 String [] selArgs = new String[] { url };
955 String where = Browser.BookmarkColumns.URL + " LIKE ? AND "
956 + Browser.BookmarkColumns.BOOKMARK + " = 0";
957 Cursor c = cr.query(Browser.BOOKMARKS_URI,
958 Browser.HISTORY_PROJECTION, where, selArgs, null);
959 if (c.moveToFirst()) {
960 // Current implementation of database only has one entry per
961 // url.
962 ContentValues map = new ContentValues();
963 map.put(Browser.BookmarkColumns.TITLE, title);
964 cr.update(Browser.BOOKMARKS_URI, map, "_id = "
965 + c.getInt(0), null);
966 }
967 c.close();
968 } catch (IllegalStateException e) {
969 Log.e(LOGTAG, "Tab onReceived title", e);
970 } catch (SQLiteException ex) {
971 Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex);
972 }
973 }
974
975 @Override
976 public void onReceivedIcon(WebView view, Bitmap icon) {
977 if (icon != null) {
978 BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity
979 .getContentResolver(), view.getOriginalUrl(), view
980 .getUrl(), icon);
981 }
982 if (mInForeground) {
983 mActivity.setFavicon(icon);
984 }
985 }
986
987 @Override
988 public void onReceivedTouchIconUrl(WebView view, String url,
989 boolean precomposed) {
990 final ContentResolver cr = mActivity.getContentResolver();
991 final Cursor c = BrowserBookmarksAdapter.queryBookmarksForUrl(cr,
992 view.getOriginalUrl(), view.getUrl(), true);
993 if (c != null) {
994 if (c.getCount() > 0) {
995 // Let precomposed icons take precedence over non-composed
996 // icons.
997 if (precomposed && mTouchIconLoader != null) {
998 mTouchIconLoader.cancel(false);
999 mTouchIconLoader = null;
1000 }
1001 // Have only one async task at a time.
1002 if (mTouchIconLoader == null) {
1003 mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr,
1004 c, view);
1005 mTouchIconLoader.execute(url);
Patrick Scott8e9fe322010-03-04 14:29:31 -05001006 } else {
1007 c.close();
Grace Kloba22ac16e2009-10-07 18:00:23 -07001008 }
1009 } else {
1010 c.close();
1011 }
1012 }
1013 }
1014
1015 @Override
1016 public void onShowCustomView(View view,
1017 WebChromeClient.CustomViewCallback callback) {
1018 if (mInForeground) mActivity.onShowCustomView(view, callback);
1019 }
1020
1021 @Override
1022 public void onHideCustomView() {
1023 if (mInForeground) mActivity.onHideCustomView();
1024 }
1025
1026 /**
1027 * The origin has exceeded its database quota.
1028 * @param url the URL that exceeded the quota
1029 * @param databaseIdentifier the identifier of the database on which the
1030 * transaction that caused the quota overflow was run
1031 * @param currentQuota the current quota for the origin.
1032 * @param estimatedSize the estimated size of the database.
1033 * @param totalUsedQuota is the sum of all origins' quota.
1034 * @param quotaUpdater The callback to run when a decision to allow or
1035 * deny quota has been made. Don't forget to call this!
1036 */
1037 @Override
1038 public void onExceededDatabaseQuota(String url,
1039 String databaseIdentifier, long currentQuota, long estimatedSize,
1040 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
1041 BrowserSettings.getInstance().getWebStorageSizeManager()
1042 .onExceededDatabaseQuota(url, databaseIdentifier,
1043 currentQuota, estimatedSize, totalUsedQuota,
1044 quotaUpdater);
1045 }
1046
1047 /**
1048 * The Application Cache has exceeded its max size.
1049 * @param spaceNeeded is the amount of disk space that would be needed
1050 * in order for the last appcache operation to succeed.
1051 * @param totalUsedQuota is the sum of all origins' quota.
1052 * @param quotaUpdater A callback to inform the WebCore thread that a
1053 * new app cache size is available. This callback must always
1054 * be executed at some point to ensure that the sleeping
1055 * WebCore thread is woken up.
1056 */
1057 @Override
1058 public void onReachedMaxAppCacheSize(long spaceNeeded,
1059 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
1060 BrowserSettings.getInstance().getWebStorageSizeManager()
1061 .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota,
1062 quotaUpdater);
1063 }
1064
1065 /**
1066 * Instructs the browser to show a prompt to ask the user to set the
1067 * Geolocation permission state for the specified origin.
1068 * @param origin The origin for which Geolocation permissions are
1069 * requested.
1070 * @param callback The callback to call once the user has set the
1071 * Geolocation permission state.
1072 */
1073 @Override
1074 public void onGeolocationPermissionsShowPrompt(String origin,
1075 GeolocationPermissions.Callback callback) {
1076 if (mInForeground) {
1077 mGeolocationPermissionsPrompt.show(origin, callback);
1078 }
1079 }
1080
1081 /**
1082 * Instructs the browser to hide the Geolocation permissions prompt.
1083 */
1084 @Override
1085 public void onGeolocationPermissionsHidePrompt() {
1086 if (mInForeground) {
1087 mGeolocationPermissionsPrompt.hide();
1088 }
1089 }
1090
Ben Murdoch65acc352009-11-19 18:16:04 +00001091 /* Adds a JavaScript error message to the system log and if the JS
1092 * console is enabled in the about:debug options, to that console
1093 * also.
Ben Murdochc42addf2010-01-28 15:19:59 +00001094 * @param consoleMessage the message object.
Grace Kloba22ac16e2009-10-07 18:00:23 -07001095 */
1096 @Override
Ben Murdochc42addf2010-01-28 15:19:59 +00001097 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Grace Kloba22ac16e2009-10-07 18:00:23 -07001098 if (mInForeground) {
1099 // call getErrorConsole(true) so it will create one if needed
1100 ErrorConsoleView errorConsole = getErrorConsole(true);
Ben Murdochc42addf2010-01-28 15:19:59 +00001101 errorConsole.addErrorMessage(consoleMessage);
Grace Kloba22ac16e2009-10-07 18:00:23 -07001102 if (mActivity.shouldShowErrorConsole()
1103 && errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) {
1104 errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
1105 }
1106 }
Ben Murdochc42addf2010-01-28 15:19:59 +00001107
1108 String message = "Console: " + consoleMessage.message() + " "
1109 + consoleMessage.sourceId() + ":"
1110 + consoleMessage.lineNumber();
1111
1112 switch (consoleMessage.messageLevel()) {
1113 case TIP:
1114 Log.v(CONSOLE_LOGTAG, message);
1115 break;
1116 case LOG:
1117 Log.i(CONSOLE_LOGTAG, message);
1118 break;
1119 case WARNING:
1120 Log.w(CONSOLE_LOGTAG, message);
1121 break;
1122 case ERROR:
1123 Log.e(CONSOLE_LOGTAG, message);
1124 break;
1125 case DEBUG:
1126 Log.d(CONSOLE_LOGTAG, message);
1127 break;
1128 }
1129
1130 return true;
Grace Kloba22ac16e2009-10-07 18:00:23 -07001131 }
1132
1133 /**
1134 * Ask the browser for an icon to represent a <video> element.
1135 * This icon will be used if the Web page did not specify a poster attribute.
1136 * @return Bitmap The icon or null if no such icon is available.
1137 */
1138 @Override
1139 public Bitmap getDefaultVideoPoster() {
1140 if (mInForeground) {
1141 return mActivity.getDefaultVideoPoster();
1142 }
1143 return null;
1144 }
1145
1146 /**
1147 * Ask the host application for a custom progress view to show while
1148 * a <video> is loading.
1149 * @return View The progress view.
1150 */
1151 @Override
1152 public View getVideoLoadingProgressView() {
1153 if (mInForeground) {
1154 return mActivity.getVideoLoadingProgressView();
1155 }
1156 return null;
1157 }
1158
1159 @Override
1160 public void openFileChooser(ValueCallback<Uri> uploadMsg) {
1161 if (mInForeground) {
1162 mActivity.openFileChooser(uploadMsg);
1163 } else {
1164 uploadMsg.onReceiveValue(null);
1165 }
1166 }
1167
1168 /**
1169 * Deliver a list of already-visited URLs
1170 */
1171 @Override
1172 public void getVisitedHistory(final ValueCallback<String[]> callback) {
1173 AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() {
1174 public String[] doInBackground(Void... unused) {
1175 return Browser.getVisitedHistory(mActivity
1176 .getContentResolver());
1177 }
1178 public void onPostExecute(String[] result) {
1179 callback.onReceiveValue(result);
1180 };
1181 };
1182 task.execute();
1183 };
1184 };
1185
1186 // -------------------------------------------------------------------------
1187 // WebViewClient implementation for the sub window
1188 // -------------------------------------------------------------------------
1189
1190 // Subclass of WebViewClient used in subwindows to notify the main
1191 // WebViewClient of certain WebView activities.
1192 private static class SubWindowClient extends WebViewClient {
1193 // The main WebViewClient.
1194 private final WebViewClient mClient;
1195
1196 SubWindowClient(WebViewClient client) {
1197 mClient = client;
1198 }
1199 @Override
1200 public void doUpdateVisitedHistory(WebView view, String url,
1201 boolean isReload) {
1202 mClient.doUpdateVisitedHistory(view, url, isReload);
1203 }
1204 @Override
1205 public boolean shouldOverrideUrlLoading(WebView view, String url) {
1206 return mClient.shouldOverrideUrlLoading(view, url);
1207 }
1208 @Override
1209 public void onReceivedSslError(WebView view, SslErrorHandler handler,
1210 SslError error) {
1211 mClient.onReceivedSslError(view, handler, error);
1212 }
1213 @Override
1214 public void onReceivedHttpAuthRequest(WebView view,
1215 HttpAuthHandler handler, String host, String realm) {
1216 mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
1217 }
1218 @Override
1219 public void onFormResubmission(WebView view, Message dontResend,
1220 Message resend) {
1221 mClient.onFormResubmission(view, dontResend, resend);
1222 }
1223 @Override
1224 public void onReceivedError(WebView view, int errorCode,
1225 String description, String failingUrl) {
1226 mClient.onReceivedError(view, errorCode, description, failingUrl);
1227 }
1228 @Override
1229 public boolean shouldOverrideKeyEvent(WebView view,
1230 android.view.KeyEvent event) {
1231 return mClient.shouldOverrideKeyEvent(view, event);
1232 }
1233 @Override
1234 public void onUnhandledKeyEvent(WebView view,
1235 android.view.KeyEvent event) {
1236 mClient.onUnhandledKeyEvent(view, event);
1237 }
1238 }
1239
1240 // -------------------------------------------------------------------------
1241 // WebChromeClient implementation for the sub window
1242 // -------------------------------------------------------------------------
1243
1244 private class SubWindowChromeClient extends WebChromeClient {
1245 // The main WebChromeClient.
1246 private final WebChromeClient mClient;
1247
1248 SubWindowChromeClient(WebChromeClient client) {
1249 mClient = client;
1250 }
1251 @Override
1252 public void onProgressChanged(WebView view, int newProgress) {
1253 mClient.onProgressChanged(view, newProgress);
1254 }
1255 @Override
1256 public boolean onCreateWindow(WebView view, boolean dialog,
1257 boolean userGesture, android.os.Message resultMsg) {
1258 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg);
1259 }
1260 @Override
1261 public void onCloseWindow(WebView window) {
1262 if (window != mSubView) {
1263 Log.e(LOGTAG, "Can't close the window");
1264 }
1265 mActivity.dismissSubWindow(Tab.this);
1266 }
1267 }
1268
1269 // -------------------------------------------------------------------------
1270
1271 // Construct a new tab
1272 Tab(BrowserActivity activity, WebView w, boolean closeOnExit, String appId,
1273 String url) {
1274 mActivity = activity;
1275 mCloseOnExit = closeOnExit;
1276 mAppId = appId;
1277 mOriginalUrl = url;
1278 mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
1279 mPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
1280 mInLoad = false;
1281 mInForeground = false;
1282
1283 mInflateService = LayoutInflater.from(activity);
1284
1285 // The tab consists of a container view, which contains the main
1286 // WebView, as well as any other UI elements associated with the tab.
1287 mContainer = mInflateService.inflate(R.layout.tab, null);
1288
1289 mGeolocationPermissionsPrompt =
1290 (GeolocationPermissionsPrompt) mContainer.findViewById(
1291 R.id.geolocation_permissions_prompt);
1292
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -05001293 mDownloadListener = new DownloadListener() {
1294 public void onDownloadStart(String url, String userAgent,
1295 String contentDisposition, String mimetype,
1296 long contentLength) {
1297 mActivity.onDownloadStart(url, userAgent, contentDisposition,
1298 mimetype, contentLength);
1299 if (mMainView.copyBackForwardList().getSize() == 0) {
1300 // This Tab was opened for the sole purpose of downloading a
1301 // file. Remove it.
1302 if (mActivity.getTabControl().getCurrentWebView()
1303 == mMainView) {
1304 // In this case, the Tab is still on top.
1305 mActivity.goBackOnePageOrQuit();
1306 } else {
1307 // In this case, it is not.
1308 mActivity.closeTab(Tab.this);
1309 }
1310 }
1311 }
1312 };
Leon Scroggins0c75a8e2010-03-03 16:40:58 -05001313 mWebBackForwardListClient = new WebBackForwardListClient() {
1314 @Override
1315 public void onNewHistoryItem(WebHistoryItem item) {
1316 if (isInVoiceSearchMode()) {
1317 item.setCustomData(mVoiceSearchData.mVoiceSearchIntent);
1318 }
1319 }
1320 @Override
1321 public void onIndexChanged(WebHistoryItem item, int index) {
1322 Object data = item.getCustomData();
1323 if (data != null && data instanceof Intent) {
1324 activateVoiceSearchMode((Intent) data);
1325 }
1326 }
1327 };
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -05001328
Grace Kloba22ac16e2009-10-07 18:00:23 -07001329 setWebView(w);
1330 }
1331
1332 /**
1333 * Sets the WebView for this tab, correctly removing the old WebView from
1334 * the container view.
1335 */
1336 void setWebView(WebView w) {
1337 if (mMainView == w) {
1338 return;
1339 }
1340 // If the WebView is changing, the page will be reloaded, so any ongoing
1341 // Geolocation permission requests are void.
1342 mGeolocationPermissionsPrompt.hide();
1343
1344 // Just remove the old one.
1345 FrameLayout wrapper =
1346 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
1347 wrapper.removeView(mMainView);
1348
1349 // set the new one
1350 mMainView = w;
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -05001351 // attach the WebViewClient, WebChromeClient and DownloadListener
Grace Kloba22ac16e2009-10-07 18:00:23 -07001352 if (mMainView != null) {
1353 mMainView.setWebViewClient(mWebViewClient);
1354 mMainView.setWebChromeClient(mWebChromeClient);
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -05001355 // Attach DownloadManager so that downloads can start in an active
1356 // or a non-active window. This can happen when going to a site that
1357 // does a redirect after a period of time. The user could have
1358 // switched to another tab while waiting for the download to start.
1359 mMainView.setDownloadListener(mDownloadListener);
Leon Scroggins0c75a8e2010-03-03 16:40:58 -05001360 mMainView.setWebBackForwardListClient(mWebBackForwardListClient);
Grace Kloba22ac16e2009-10-07 18:00:23 -07001361 }
1362 }
1363
1364 /**
1365 * Destroy the tab's main WebView and subWindow if any
1366 */
1367 void destroy() {
1368 if (mMainView != null) {
1369 dismissSubWindow();
1370 BrowserSettings.getInstance().deleteObserver(mMainView.getSettings());
1371 // save the WebView to call destroy() after detach it from the tab
1372 WebView webView = mMainView;
1373 setWebView(null);
1374 webView.destroy();
1375 }
1376 }
1377
1378 /**
1379 * Remove the tab from the parent
1380 */
1381 void removeFromTree() {
1382 // detach the children
1383 if (mChildTabs != null) {
1384 for(Tab t : mChildTabs) {
1385 t.setParentTab(null);
1386 }
1387 }
1388 // remove itself from the parent list
1389 if (mParentTab != null) {
1390 mParentTab.mChildTabs.remove(this);
1391 }
1392 }
1393
1394 /**
1395 * Create a new subwindow unless a subwindow already exists.
1396 * @return True if a new subwindow was created. False if one already exists.
1397 */
1398 boolean createSubWindow() {
1399 if (mSubView == null) {
1400 mSubViewContainer = mInflateService.inflate(
1401 R.layout.browser_subwindow, null);
1402 mSubView = (WebView) mSubViewContainer.findViewById(R.id.webview);
1403 // use trackball directly
1404 mSubView.setMapTrackballToArrowKeys(false);
1405 mSubView.setWebViewClient(new SubWindowClient(mWebViewClient));
1406 mSubView.setWebChromeClient(new SubWindowChromeClient(
1407 mWebChromeClient));
Leon Scrogginsdcc5eeb2010-02-23 17:26:37 -05001408 // Set a different DownloadListener for the mSubView, since it will
1409 // just need to dismiss the mSubView, rather than close the Tab
1410 mSubView.setDownloadListener(new DownloadListener() {
1411 public void onDownloadStart(String url, String userAgent,
1412 String contentDisposition, String mimetype,
1413 long contentLength) {
1414 mActivity.onDownloadStart(url, userAgent,
1415 contentDisposition, mimetype, contentLength);
1416 if (mSubView.copyBackForwardList().getSize() == 0) {
1417 // This subwindow was opened for the sole purpose of
1418 // downloading a file. Remove it.
1419 dismissSubWindow();
1420 }
1421 }
1422 });
Grace Kloba22ac16e2009-10-07 18:00:23 -07001423 mSubView.setOnCreateContextMenuListener(mActivity);
1424 final BrowserSettings s = BrowserSettings.getInstance();
1425 s.addObserver(mSubView.getSettings()).update(s, null);
1426 final ImageButton cancel = (ImageButton) mSubViewContainer
1427 .findViewById(R.id.subwindow_close);
1428 cancel.setOnClickListener(new OnClickListener() {
1429 public void onClick(View v) {
1430 mSubView.getWebChromeClient().onCloseWindow(mSubView);
1431 }
1432 });
1433 return true;
1434 }
1435 return false;
1436 }
1437
1438 /**
1439 * Dismiss the subWindow for the tab.
1440 */
1441 void dismissSubWindow() {
1442 if (mSubView != null) {
1443 BrowserSettings.getInstance().deleteObserver(
1444 mSubView.getSettings());
1445 mSubView.destroy();
1446 mSubView = null;
1447 mSubViewContainer = null;
1448 }
1449 }
1450
1451 /**
1452 * Attach the sub window to the content view.
1453 */
1454 void attachSubWindow(ViewGroup content) {
1455 if (mSubView != null) {
1456 content.addView(mSubViewContainer,
1457 BrowserActivity.COVER_SCREEN_PARAMS);
1458 }
1459 }
1460
1461 /**
1462 * Remove the sub window from the content view.
1463 */
1464 void removeSubWindow(ViewGroup content) {
1465 if (mSubView != null) {
1466 content.removeView(mSubViewContainer);
1467 }
1468 }
1469
1470 /**
1471 * This method attaches both the WebView and any sub window to the
1472 * given content view.
1473 */
1474 void attachTabToContentView(ViewGroup content) {
1475 if (mMainView == null) {
1476 return;
1477 }
1478
1479 // Attach the WebView to the container and then attach the
1480 // container to the content view.
1481 FrameLayout wrapper =
1482 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
1483 wrapper.addView(mMainView);
1484 content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS);
1485 attachSubWindow(content);
1486 }
1487
1488 /**
1489 * Remove the WebView and any sub window from the given content view.
1490 */
1491 void removeTabFromContentView(ViewGroup content) {
1492 if (mMainView == null) {
1493 return;
1494 }
1495
1496 // Remove the container from the content and then remove the
1497 // WebView from the container. This will trigger a focus change
1498 // needed by WebView.
1499 FrameLayout wrapper =
1500 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
1501 wrapper.removeView(mMainView);
1502 content.removeView(mContainer);
1503 removeSubWindow(content);
1504 }
1505
1506 /**
1507 * Set the parent tab of this tab.
1508 */
1509 void setParentTab(Tab parent) {
1510 mParentTab = parent;
1511 // This tab may have been freed due to low memory. If that is the case,
1512 // the parent tab index is already saved. If we are changing that index
1513 // (most likely due to removing the parent tab) we must update the
1514 // parent tab index in the saved Bundle.
1515 if (mSavedState != null) {
1516 if (parent == null) {
1517 mSavedState.remove(PARENTTAB);
1518 } else {
1519 mSavedState.putInt(PARENTTAB, mActivity.getTabControl()
1520 .getTabIndex(parent));
1521 }
1522 }
1523 }
1524
1525 /**
1526 * When a Tab is created through the content of another Tab, then we
1527 * associate the Tabs.
1528 * @param child the Tab that was created from this Tab
1529 */
1530 void addChildTab(Tab child) {
1531 if (mChildTabs == null) {
1532 mChildTabs = new Vector<Tab>();
1533 }
1534 mChildTabs.add(child);
1535 child.setParentTab(this);
1536 }
1537
1538 Vector<Tab> getChildTabs() {
1539 return mChildTabs;
1540 }
1541
1542 void resume() {
1543 if (mMainView != null) {
1544 mMainView.onResume();
1545 if (mSubView != null) {
1546 mSubView.onResume();
1547 }
1548 }
1549 }
1550
1551 void pause() {
1552 if (mMainView != null) {
1553 mMainView.onPause();
1554 if (mSubView != null) {
1555 mSubView.onPause();
1556 }
1557 }
1558 }
1559
1560 void putInForeground() {
1561 mInForeground = true;
1562 resume();
1563 mMainView.setOnCreateContextMenuListener(mActivity);
1564 if (mSubView != null) {
1565 mSubView.setOnCreateContextMenuListener(mActivity);
1566 }
1567 // Show the pending error dialog if the queue is not empty
1568 if (mQueuedErrors != null && mQueuedErrors.size() > 0) {
1569 showError(mQueuedErrors.getFirst());
1570 }
1571 }
1572
1573 void putInBackground() {
1574 mInForeground = false;
1575 pause();
1576 mMainView.setOnCreateContextMenuListener(null);
1577 if (mSubView != null) {
1578 mSubView.setOnCreateContextMenuListener(null);
1579 }
1580 }
1581
1582 /**
1583 * Return the top window of this tab; either the subwindow if it is not
1584 * null or the main window.
1585 * @return The top window of this tab.
1586 */
1587 WebView getTopWindow() {
1588 if (mSubView != null) {
1589 return mSubView;
1590 }
1591 return mMainView;
1592 }
1593
1594 /**
1595 * Return the main window of this tab. Note: if a tab is freed in the
1596 * background, this can return null. It is only guaranteed to be
1597 * non-null for the current tab.
1598 * @return The main WebView of this tab.
1599 */
1600 WebView getWebView() {
1601 return mMainView;
1602 }
1603
1604 /**
1605 * Return the subwindow of this tab or null if there is no subwindow.
1606 * @return The subwindow of this tab or null.
1607 */
1608 WebView getSubWebView() {
1609 return mSubView;
1610 }
1611
1612 /**
1613 * @return The geolocation permissions prompt for this tab.
1614 */
1615 GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() {
1616 return mGeolocationPermissionsPrompt;
1617 }
1618
1619 /**
1620 * @return The application id string
1621 */
1622 String getAppId() {
1623 return mAppId;
1624 }
1625
1626 /**
1627 * Set the application id string
1628 * @param id
1629 */
1630 void setAppId(String id) {
1631 mAppId = id;
1632 }
1633
1634 /**
1635 * @return The original url associated with this Tab
1636 */
1637 String getOriginalUrl() {
1638 return mOriginalUrl;
1639 }
1640
1641 /**
1642 * Set the original url associated with this tab
1643 */
1644 void setOriginalUrl(String url) {
1645 mOriginalUrl = url;
1646 }
1647
1648 /**
1649 * Get the url of this tab. Valid after calling populatePickerData, but
1650 * before calling wipePickerData, or if the webview has been destroyed.
1651 * @return The WebView's url or null.
1652 */
1653 String getUrl() {
1654 if (mPickerData != null) {
1655 return mPickerData.mUrl;
1656 }
1657 return null;
1658 }
1659
1660 /**
1661 * Get the title of this tab. Valid after calling populatePickerData, but
1662 * before calling wipePickerData, or if the webview has been destroyed. If
1663 * the url has no title, use the url instead.
1664 * @return The WebView's title (or url) or null.
1665 */
1666 String getTitle() {
1667 if (mPickerData != null) {
1668 return mPickerData.mTitle;
1669 }
1670 return null;
1671 }
1672
1673 /**
1674 * Get the favicon of this tab. Valid after calling populatePickerData, but
1675 * before calling wipePickerData, or if the webview has been destroyed.
1676 * @return The WebView's favicon or null.
1677 */
1678 Bitmap getFavicon() {
1679 if (mPickerData != null) {
1680 return mPickerData.mFavicon;
1681 }
1682 return null;
1683 }
1684
1685 /**
1686 * Return the tab's error console. Creates the console if createIfNEcessary
1687 * is true and we haven't already created the console.
1688 * @param createIfNecessary Flag to indicate if the console should be
1689 * created if it has not been already.
1690 * @return The tab's error console, or null if one has not been created and
1691 * createIfNecessary is false.
1692 */
1693 ErrorConsoleView getErrorConsole(boolean createIfNecessary) {
1694 if (createIfNecessary && mErrorConsole == null) {
1695 mErrorConsole = new ErrorConsoleView(mActivity);
1696 mErrorConsole.setWebView(mMainView);
1697 }
1698 return mErrorConsole;
1699 }
1700
1701 /**
1702 * If this Tab was created through another Tab, then this method returns
1703 * that Tab.
1704 * @return the Tab parent or null
1705 */
1706 public Tab getParentTab() {
1707 return mParentTab;
1708 }
1709
1710 /**
1711 * Return whether this tab should be closed when it is backing out of the
1712 * first page.
1713 * @return TRUE if this tab should be closed when exit.
1714 */
1715 boolean closeOnExit() {
1716 return mCloseOnExit;
1717 }
1718
1719 /**
1720 * Saves the current lock-icon state before resetting the lock icon. If we
1721 * have an error, we may need to roll back to the previous state.
1722 */
1723 void resetLockIcon(String url) {
1724 mPrevLockIconType = mLockIconType;
1725 mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
1726 if (URLUtil.isHttpsUrl(url)) {
1727 mLockIconType = BrowserActivity.LOCK_ICON_SECURE;
1728 }
1729 }
1730
1731 /**
1732 * Reverts the lock-icon state to the last saved state, for example, if we
1733 * had an error, and need to cancel the load.
1734 */
1735 void revertLockIcon() {
1736 mLockIconType = mPrevLockIconType;
1737 }
1738
1739 /**
1740 * @return The tab's lock icon type.
1741 */
1742 int getLockIconType() {
1743 return mLockIconType;
1744 }
1745
1746 /**
1747 * @return TRUE if onPageStarted is called while onPageFinished is not
1748 * called yet.
1749 */
1750 boolean inLoad() {
1751 return mInLoad;
1752 }
1753
1754 // force mInLoad to be false. This should only be called before closing the
1755 // tab to ensure BrowserActivity's pauseWebViewTimers() is called correctly.
1756 void clearInLoad() {
1757 mInLoad = false;
1758 }
1759
1760 void populatePickerData() {
1761 if (mMainView == null) {
1762 populatePickerDataFromSavedState();
1763 return;
1764 }
1765
1766 // FIXME: The only place we cared about subwindow was for
1767 // bookmarking (i.e. not when saving state). Was this deliberate?
1768 final WebBackForwardList list = mMainView.copyBackForwardList();
1769 final WebHistoryItem item = list != null ? list.getCurrentItem() : null;
1770 populatePickerData(item);
1771 }
1772
1773 // Populate the picker data using the given history item and the current top
1774 // WebView.
1775 private void populatePickerData(WebHistoryItem item) {
1776 mPickerData = new PickerData();
1777 if (item != null) {
1778 mPickerData.mUrl = item.getUrl();
1779 mPickerData.mTitle = item.getTitle();
1780 mPickerData.mFavicon = item.getFavicon();
1781 if (mPickerData.mTitle == null) {
1782 mPickerData.mTitle = mPickerData.mUrl;
1783 }
1784 }
1785 }
1786
1787 // Create the PickerData and populate it using the saved state of the tab.
1788 void populatePickerDataFromSavedState() {
1789 if (mSavedState == null) {
1790 return;
1791 }
1792 mPickerData = new PickerData();
1793 mPickerData.mUrl = mSavedState.getString(CURRURL);
1794 mPickerData.mTitle = mSavedState.getString(CURRTITLE);
1795 }
1796
1797 void clearPickerData() {
1798 mPickerData = null;
1799 }
1800
1801 /**
1802 * Get the saved state bundle.
1803 * @return
1804 */
1805 Bundle getSavedState() {
1806 return mSavedState;
1807 }
1808
1809 /**
1810 * Set the saved state.
1811 */
1812 void setSavedState(Bundle state) {
1813 mSavedState = state;
1814 }
1815
1816 /**
1817 * @return TRUE if succeed in saving the state.
1818 */
1819 boolean saveState() {
1820 // If the WebView is null it means we ran low on memory and we already
1821 // stored the saved state in mSavedState.
1822 if (mMainView == null) {
1823 return mSavedState != null;
1824 }
1825
1826 mSavedState = new Bundle();
1827 final WebBackForwardList list = mMainView.saveState(mSavedState);
1828 if (list != null) {
1829 final File f = new File(mActivity.getTabControl().getThumbnailDir(),
1830 mMainView.hashCode() + "_pic.save");
1831 if (mMainView.savePicture(mSavedState, f)) {
1832 mSavedState.putString(CURRPICTURE, f.getPath());
Mike Reedd5eee692010-03-05 10:13:34 -05001833 } else {
1834 // if savePicture returned false, we can't trust the contents,
1835 // and it may be large, so we delete it right away
1836 f.delete();
Grace Kloba22ac16e2009-10-07 18:00:23 -07001837 }
1838 }
1839
1840 // Store some extra info for displaying the tab in the picker.
1841 final WebHistoryItem item = list != null ? list.getCurrentItem() : null;
1842 populatePickerData(item);
1843
1844 if (mPickerData.mUrl != null) {
1845 mSavedState.putString(CURRURL, mPickerData.mUrl);
1846 }
1847 if (mPickerData.mTitle != null) {
1848 mSavedState.putString(CURRTITLE, mPickerData.mTitle);
1849 }
1850 mSavedState.putBoolean(CLOSEONEXIT, mCloseOnExit);
1851 if (mAppId != null) {
1852 mSavedState.putString(APPID, mAppId);
1853 }
1854 if (mOriginalUrl != null) {
1855 mSavedState.putString(ORIGINALURL, mOriginalUrl);
1856 }
1857 // Remember the parent tab so the relationship can be restored.
1858 if (mParentTab != null) {
1859 mSavedState.putInt(PARENTTAB, mActivity.getTabControl().getTabIndex(
1860 mParentTab));
1861 }
1862 return true;
1863 }
1864
1865 /*
1866 * Restore the state of the tab.
1867 */
1868 boolean restoreState(Bundle b) {
1869 if (b == null) {
1870 return false;
1871 }
1872 // Restore the internal state even if the WebView fails to restore.
1873 // This will maintain the app id, original url and close-on-exit values.
1874 mSavedState = null;
1875 mPickerData = null;
1876 mCloseOnExit = b.getBoolean(CLOSEONEXIT);
1877 mAppId = b.getString(APPID);
1878 mOriginalUrl = b.getString(ORIGINALURL);
1879
1880 final WebBackForwardList list = mMainView.restoreState(b);
1881 if (list == null) {
1882 return false;
1883 }
1884 if (b.containsKey(CURRPICTURE)) {
1885 final File f = new File(b.getString(CURRPICTURE));
1886 mMainView.restorePicture(b, f);
1887 f.delete();
1888 }
1889 return true;
1890 }
1891}