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