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