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