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