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