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