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