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