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 android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.SearchManager; 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.DialogInterface.OnCancelListener; 27 import android.content.Intent; 28 import android.graphics.Bitmap; 29 import android.graphics.Bitmap.CompressFormat; 30 import android.graphics.BitmapFactory; 31 import android.graphics.Canvas; 32 import android.graphics.Color; 33 import android.graphics.Paint; 34 import android.graphics.Picture; 35 import android.graphics.PorterDuff; 36 import android.graphics.PorterDuffXfermode; 37 import android.net.Uri; 38 import android.net.http.SslError; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.Message; 42 import android.os.SystemClock; 43 import android.security.KeyChain; 44 import android.security.KeyChainAliasCallback; 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.ViewStub; 52 import android.webkit.ClientCertRequestHandler; 53 import android.webkit.ConsoleMessage; 54 import android.webkit.DownloadListener; 55 import android.webkit.GeolocationPermissions; 56 import android.webkit.HttpAuthHandler; 57 import android.webkit.SslErrorHandler; 58 import android.webkit.URLUtil; 59 import android.webkit.ValueCallback; 60 import android.webkit.WebBackForwardList; 61 import android.webkit.WebBackForwardListClient; 62 import android.webkit.WebChromeClient; 63 import android.webkit.WebHistoryItem; 64 import android.webkit.WebResourceResponse; 65 import android.webkit.WebStorage; 66 import android.webkit.WebView; 67 import android.webkit.WebView.PictureListener; 68 import android.webkit.WebViewClient; 69 import android.widget.CheckBox; 70 import android.widget.Toast; 71 72 import com.android.browser.TabControl.OnThumbnailUpdatedListener; 73 import com.android.browser.homepages.HomeProvider; 74 import com.android.browser.provider.SnapshotProvider.Snapshots; 75 import com.android.common.speech.LoggingEvents; 76 77 import java.io.ByteArrayOutputStream; 78 import java.nio.ByteBuffer; 79 import java.util.ArrayList; 80 import java.util.HashMap; 81 import java.util.Iterator; 82 import java.util.LinkedList; 83 import java.util.Map; 84 import java.util.Vector; 85 import java.util.regex.Pattern; 86 import java.util.zip.GZIPOutputStream; 87 88 /** 89 * Class for maintaining Tabs with a main WebView and a subwindow. 90 */ 91 class Tab implements PictureListener { 92 93 // Log Tag 94 private static final String LOGTAG = "Tab"; 95 private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; 96 // Special case the logtag for messages for the Console to make it easier to 97 // filter them and match the logtag used for these messages in older versions 98 // of the browser. 99 private static final String CONSOLE_LOGTAG = "browser"; 100 101 private static final int MSG_CAPTURE = 42; 102 private static final int CAPTURE_DELAY = 100; 103 private static final int INITIAL_PROGRESS = 5; 104 105 private static Bitmap sDefaultFavicon; 106 107 private static Paint sAlphaPaint = new Paint(); 108 static { 109 sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 110 sAlphaPaint.setColor(Color.TRANSPARENT); 111 } 112 113 public enum SecurityState { 114 // The page's main resource does not use SSL. Note that we use this 115 // state irrespective of the SSL authentication state of sub-resources. 116 SECURITY_STATE_NOT_SECURE, 117 // The page's main resource uses SSL and the certificate is good. The 118 // same is true of all sub-resources. 119 SECURITY_STATE_SECURE, 120 // The page's main resource uses SSL and the certificate is good, but 121 // some sub-resources either do not use SSL or have problems with their 122 // certificates. 123 SECURITY_STATE_MIXED, 124 // The page's main resource uses SSL but there is a problem with its 125 // certificate. 126 SECURITY_STATE_BAD_CERTIFICATE, 127 } 128 129 Context mContext; 130 protected WebViewController mWebViewController; 131 132 // The tab ID 133 private long mId = -1; 134 135 // The Geolocation permissions prompt 136 private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; 137 // Main WebView wrapper 138 private View mContainer; 139 // Main WebView 140 private WebView mMainView; 141 // Subwindow container 142 private View mSubViewContainer; 143 // Subwindow WebView 144 private WebView mSubView; 145 // Saved bundle for when we are running low on memory. It contains the 146 // information needed to restore the WebView if the user goes back to the 147 // tab. 148 private Bundle mSavedState; 149 // Parent Tab. This is the Tab that created this Tab, or null if the Tab was 150 // created by the UI 151 private Tab mParent; 152 // Tab that constructed by this Tab. This is used when this Tab is 153 // destroyed, it clears all mParentTab values in the children. 154 private Vector<Tab> mChildren; 155 // If true, the tab is in the foreground of the current activity. 156 private boolean mInForeground; 157 // If true, the tab is in page loading state (after onPageStarted, 158 // before onPageFinsihed) 159 private boolean mInPageLoad; 160 // The last reported progress of the current page 161 private int mPageLoadProgress; 162 // The time the load started, used to find load page time 163 private long mLoadStartTime; 164 // Application identifier used to find tabs that another application wants 165 // to reuse. 166 private String mAppId; 167 // flag to indicate if tab should be closed on back 168 private boolean mCloseOnBack; 169 // Keep the original url around to avoid killing the old WebView if the url 170 // has not changed. 171 // Error console for the tab 172 private ErrorConsoleView mErrorConsole; 173 // The listener that gets invoked when a download is started from the 174 // mMainView 175 private final DownloadListener mDownloadListener; 176 // Listener used to know when we move forward or back in the history list. 177 private final WebBackForwardListClient mWebBackForwardListClient; 178 private DataController mDataController; 179 // State of the auto-login request. 180 private DeviceAccountLogin mDeviceAccountLogin; 181 182 // AsyncTask for downloading touch icons 183 DownloadTouchIcon mTouchIconLoader; 184 185 private BrowserSettings mSettings; 186 private int mCaptureWidth; 187 private int mCaptureHeight; 188 private Bitmap mCapture; 189 private Handler mHandler; 190 191 /** 192 * See {@link #clearBackStackWhenItemAdded(String)}. 193 */ 194 private Pattern mClearHistoryUrlPattern; 195 196 private static synchronized Bitmap getDefaultFavicon(Context context) { 197 if (sDefaultFavicon == null) { 198 sDefaultFavicon = BitmapFactory.decodeResource( 199 context.getResources(), R.drawable.app_web_browser_sm); 200 } 201 return sDefaultFavicon; 202 } 203 204 // All the state needed for a page 205 protected static class PageState { 206 String mUrl; 207 String mOriginalUrl; 208 String mTitle; 209 SecurityState mSecurityState; 210 // This is non-null only when mSecurityState is SECURITY_STATE_BAD_CERTIFICATE. 211 SslError mSslCertificateError; 212 Bitmap mFavicon; 213 boolean mIsBookmarkedSite; 214 boolean mIncognito; 215 216 PageState(Context c, boolean incognito) { 217 mIncognito = incognito; 218 if (mIncognito) { 219 mOriginalUrl = mUrl = "browser:incognito"; 220 mTitle = c.getString(R.string.new_incognito_tab); 221 } else { 222 mOriginalUrl = mUrl = ""; 223 mTitle = c.getString(R.string.new_tab); 224 } 225 mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; 226 } 227 228 PageState(Context c, boolean incognito, String url, Bitmap favicon) { 229 mIncognito = incognito; 230 mOriginalUrl = mUrl = url; 231 if (URLUtil.isHttpsUrl(url)) { 232 mSecurityState = SecurityState.SECURITY_STATE_SECURE; 233 } else { 234 mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; 235 } 236 mFavicon = favicon; 237 } 238 239 } 240 241 // The current/loading page's state 242 protected PageState mCurrentState; 243 244 // Used for saving and restoring each Tab 245 static final String ID = "ID"; 246 static final String CURRURL = "currentUrl"; 247 static final String CURRTITLE = "currentTitle"; 248 static final String PARENTTAB = "parentTab"; 249 static final String APPID = "appid"; 250 static final String INCOGNITO = "privateBrowsingEnabled"; 251 static final String USERAGENT = "useragent"; 252 static final String CLOSEFLAG = "closeOnBack"; 253 254 // ------------------------------------------------------------------------- 255 256 /** 257 * Private information regarding the latest voice search. If the Tab is not 258 * in voice search mode, this will be null. 259 */ 260 private VoiceSearchData mVoiceSearchData; 261 /** 262 * Remove voice search mode from this tab. 263 */ 264 public void revertVoiceSearchMode() { 265 if (mVoiceSearchData != null) { 266 mVoiceSearchData = null; 267 if (mInForeground) { 268 mWebViewController.revertVoiceSearchMode(this); 269 } 270 } 271 } 272 273 /** 274 * Return whether the tab is in voice search mode. 275 */ 276 public boolean isInVoiceSearchMode() { 277 return mVoiceSearchData != null; 278 } 279 /** 280 * Return true if the Tab is in voice search mode and the voice search 281 * Intent came with a String identifying that Google provided the Intent. 282 */ 283 public boolean voiceSearchSourceIsGoogle() { 284 return mVoiceSearchData != null && mVoiceSearchData.mSourceIsGoogle; 285 } 286 /** 287 * Get the title to display for the current voice search page. If the Tab 288 * is not in voice search mode, return null. 289 */ 290 public String getVoiceDisplayTitle() { 291 if (mVoiceSearchData == null) return null; 292 return mVoiceSearchData.mLastVoiceSearchTitle; 293 } 294 /** 295 * Get the latest array of voice search results, to be passed to the 296 * BrowserProvider. If the Tab is not in voice search mode, return null. 297 */ 298 public ArrayList<String> getVoiceSearchResults() { 299 if (mVoiceSearchData == null) return null; 300 return mVoiceSearchData.mVoiceSearchResults; 301 } 302 /** 303 * Activate voice search mode. 304 * @param intent Intent which has the results to use, or an index into the 305 * results when reusing the old results. 306 */ 307 /* package */ void activateVoiceSearchMode(Intent intent) { 308 int index = 0; 309 ArrayList<String> results = intent.getStringArrayListExtra( 310 RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS); 311 if (results != null) { 312 ArrayList<String> urls = intent.getStringArrayListExtra( 313 RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS); 314 ArrayList<String> htmls = intent.getStringArrayListExtra( 315 RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_HTML); 316 ArrayList<String> baseUrls = intent.getStringArrayListExtra( 317 RecognizerResultsIntent 318 .EXTRA_VOICE_SEARCH_RESULT_HTML_BASE_URLS); 319 // This tab is now entering voice search mode for the first time, or 320 // a new voice search was done. 321 int size = results.size(); 322 if (urls == null || size != urls.size()) { 323 throw new AssertionError("improper extras passed in Intent"); 324 } 325 if (htmls == null || htmls.size() != size || baseUrls == null || 326 (baseUrls.size() != size && baseUrls.size() != 1)) { 327 // If either of these arrays are empty/incorrectly sized, ignore 328 // them. 329 htmls = null; 330 baseUrls = null; 331 } 332 mVoiceSearchData = new VoiceSearchData(results, urls, htmls, 333 baseUrls); 334 mVoiceSearchData.mHeaders = intent.getParcelableArrayListExtra( 335 RecognizerResultsIntent 336 .EXTRA_VOICE_SEARCH_RESULT_HTTP_HEADERS); 337 mVoiceSearchData.mSourceIsGoogle = intent.getBooleanExtra( 338 VoiceSearchData.SOURCE_IS_GOOGLE, false); 339 mVoiceSearchData.mVoiceSearchIntent = new Intent(intent); 340 } 341 String extraData = intent.getStringExtra( 342 SearchManager.EXTRA_DATA_KEY); 343 if (extraData != null) { 344 index = Integer.parseInt(extraData); 345 if (index >= mVoiceSearchData.mVoiceSearchResults.size()) { 346 throw new AssertionError("index must be less than " 347 + "size of mVoiceSearchResults"); 348 } 349 if (mVoiceSearchData.mSourceIsGoogle) { 350 Intent logIntent = new Intent( 351 LoggingEvents.ACTION_LOG_EVENT); 352 logIntent.putExtra(LoggingEvents.EXTRA_EVENT, 353 LoggingEvents.VoiceSearch.N_BEST_CHOOSE); 354 logIntent.putExtra( 355 LoggingEvents.VoiceSearch.EXTRA_N_BEST_CHOOSE_INDEX, 356 index); 357 mContext.sendBroadcast(logIntent); 358 } 359 if (mVoiceSearchData.mVoiceSearchIntent != null) { 360 // Copy the Intent, so that each history item will have its own 361 // Intent, with different (or none) extra data. 362 Intent latest = new Intent(mVoiceSearchData.mVoiceSearchIntent); 363 latest.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 364 mVoiceSearchData.mVoiceSearchIntent = latest; 365 } 366 } 367 mVoiceSearchData.mLastVoiceSearchTitle 368 = mVoiceSearchData.mVoiceSearchResults.get(index); 369 if (mInForeground) { 370 mWebViewController.activateVoiceSearchMode( 371 mVoiceSearchData.mLastVoiceSearchTitle, 372 mVoiceSearchData.mVoiceSearchResults); 373 } 374 if (mVoiceSearchData.mVoiceSearchHtmls != null) { 375 // When index was found it was already ensured that it was valid 376 String uriString = mVoiceSearchData.mVoiceSearchHtmls.get(index); 377 if (uriString != null) { 378 Uri dataUri = Uri.parse(uriString); 379 if (RecognizerResultsIntent.URI_SCHEME_INLINE.equals( 380 dataUri.getScheme())) { 381 // If there is only one base URL, use it. If there are 382 // more, there will be one for each index, so use the base 383 // URL corresponding to the index. 384 String baseUrl = mVoiceSearchData.mVoiceSearchBaseUrls.get( 385 mVoiceSearchData.mVoiceSearchBaseUrls.size() > 1 ? 386 index : 0); 387 mVoiceSearchData.mLastVoiceSearchUrl = baseUrl; 388 mMainView.loadDataWithBaseURL(baseUrl, 389 uriString.substring(RecognizerResultsIntent 390 .URI_SCHEME_INLINE.length() + 1), "text/html", 391 "utf-8", baseUrl); 392 return; 393 } 394 } 395 } 396 mVoiceSearchData.mLastVoiceSearchUrl 397 = mVoiceSearchData.mVoiceSearchUrls.get(index); 398 if (null == mVoiceSearchData.mLastVoiceSearchUrl) { 399 mVoiceSearchData.mLastVoiceSearchUrl = UrlUtils.smartUrlFilter( 400 mVoiceSearchData.mLastVoiceSearchTitle); 401 } 402 Map<String, String> headers = null; 403 if (mVoiceSearchData.mHeaders != null) { 404 int bundleIndex = mVoiceSearchData.mHeaders.size() == 1 ? 0 405 : index; 406 Bundle bundle = mVoiceSearchData.mHeaders.get(bundleIndex); 407 if (bundle != null && !bundle.isEmpty()) { 408 Iterator<String> iter = bundle.keySet().iterator(); 409 headers = new HashMap<String, String>(); 410 while (iter.hasNext()) { 411 String key = iter.next(); 412 headers.put(key, bundle.getString(key)); 413 } 414 } 415 } 416 mMainView.loadUrl(mVoiceSearchData.mLastVoiceSearchUrl, headers); 417 } 418 /* package */ static class VoiceSearchData { 419 public VoiceSearchData(ArrayList<String> results, 420 ArrayList<String> urls, ArrayList<String> htmls, 421 ArrayList<String> baseUrls) { 422 mVoiceSearchResults = results; 423 mVoiceSearchUrls = urls; 424 mVoiceSearchHtmls = htmls; 425 mVoiceSearchBaseUrls = baseUrls; 426 } 427 /* 428 * ArrayList of suggestions to be displayed when opening the 429 * SearchManager 430 */ 431 public ArrayList<String> mVoiceSearchResults; 432 /* 433 * ArrayList of urls, associated with the suggestions in 434 * mVoiceSearchResults. 435 */ 436 public ArrayList<String> mVoiceSearchUrls; 437 /* 438 * ArrayList holding content to load for each item in 439 * mVoiceSearchResults. 440 */ 441 public ArrayList<String> mVoiceSearchHtmls; 442 /* 443 * ArrayList holding base urls for the items in mVoiceSearchResults. 444 * If non null, this will either have the same size as 445 * mVoiceSearchResults or have a size of 1, in which case all will use 446 * the same base url 447 */ 448 public ArrayList<String> mVoiceSearchBaseUrls; 449 /* 450 * The last url provided by voice search. Used for comparison to see if 451 * we are going to a page by some method besides voice search. 452 */ 453 public String mLastVoiceSearchUrl; 454 /** 455 * The last title used for voice search. Needed to update the title bar 456 * when switching tabs. 457 */ 458 public String mLastVoiceSearchTitle; 459 /** 460 * Whether the Intent which turned on voice search mode contained the 461 * String signifying that Google was the source. 462 */ 463 public boolean mSourceIsGoogle; 464 /** 465 * List of headers to be passed into the WebView containing location 466 * information 467 */ 468 public ArrayList<Bundle> mHeaders; 469 /** 470 * The Intent used to invoke voice search. Placed on the 471 * WebHistoryItem so that when coming back to a previous voice search 472 * page we can again activate voice search. 473 */ 474 public Intent mVoiceSearchIntent; 475 /** 476 * String used to identify Google as the source of voice search. 477 */ 478 public static String SOURCE_IS_GOOGLE 479 = "android.speech.extras.SOURCE_IS_GOOGLE"; 480 } 481 482 // Container class for the next error dialog that needs to be displayed 483 private class ErrorDialog { 484 public final int mTitle; 485 public final String mDescription; 486 public final int mError; 487 ErrorDialog(int title, String desc, int error) { 488 mTitle = title; 489 mDescription = desc; 490 mError = error; 491 } 492 } 493 494 private void processNextError() { 495 if (mQueuedErrors == null) { 496 return; 497 } 498 // The first one is currently displayed so just remove it. 499 mQueuedErrors.removeFirst(); 500 if (mQueuedErrors.size() == 0) { 501 mQueuedErrors = null; 502 return; 503 } 504 showError(mQueuedErrors.getFirst()); 505 } 506 507 private DialogInterface.OnDismissListener mDialogListener = 508 new DialogInterface.OnDismissListener() { 509 public void onDismiss(DialogInterface d) { 510 processNextError(); 511 } 512 }; 513 private LinkedList<ErrorDialog> mQueuedErrors; 514 515 private void queueError(int err, String desc) { 516 if (mQueuedErrors == null) { 517 mQueuedErrors = new LinkedList<ErrorDialog>(); 518 } 519 for (ErrorDialog d : mQueuedErrors) { 520 if (d.mError == err) { 521 // Already saw a similar error, ignore the new one. 522 return; 523 } 524 } 525 ErrorDialog errDialog = new ErrorDialog( 526 err == WebViewClient.ERROR_FILE_NOT_FOUND ? 527 R.string.browserFrameFileErrorLabel : 528 R.string.browserFrameNetworkErrorLabel, 529 desc, err); 530 mQueuedErrors.addLast(errDialog); 531 532 // Show the dialog now if the queue was empty and it is in foreground 533 if (mQueuedErrors.size() == 1 && mInForeground) { 534 showError(errDialog); 535 } 536 } 537 538 private void showError(ErrorDialog errDialog) { 539 if (mInForeground) { 540 AlertDialog d = new AlertDialog.Builder(mContext) 541 .setTitle(errDialog.mTitle) 542 .setMessage(errDialog.mDescription) 543 .setPositiveButton(R.string.ok, null) 544 .create(); 545 d.setOnDismissListener(mDialogListener); 546 d.show(); 547 } 548 } 549 550 // ------------------------------------------------------------------------- 551 // WebViewClient implementation for the main WebView 552 // ------------------------------------------------------------------------- 553 554 private final WebViewClient mWebViewClient = new WebViewClient() { 555 private Message mDontResend; 556 private Message mResend; 557 558 private boolean providersDiffer(String url, String otherUrl) { 559 Uri uri1 = Uri.parse(url); 560 Uri uri2 = Uri.parse(otherUrl); 561 return !uri1.getEncodedAuthority().equals(uri2.getEncodedAuthority()); 562 } 563 564 @Override 565 public void onPageStarted(WebView view, String url, Bitmap favicon) { 566 mInPageLoad = true; 567 mPageLoadProgress = INITIAL_PROGRESS; 568 mCurrentState = new PageState(mContext, 569 view.isPrivateBrowsingEnabled(), url, favicon); 570 mLoadStartTime = SystemClock.uptimeMillis(); 571 if (mVoiceSearchData != null 572 && providersDiffer(url, mVoiceSearchData.mLastVoiceSearchUrl)) { 573 if (mVoiceSearchData.mSourceIsGoogle) { 574 Intent i = new Intent(LoggingEvents.ACTION_LOG_EVENT); 575 i.putExtra(LoggingEvents.EXTRA_FLUSH, true); 576 mContext.sendBroadcast(i); 577 } 578 revertVoiceSearchMode(); 579 } 580 581 582 // If we start a touch icon load and then load a new page, we don't 583 // want to cancel the current touch icon loader. But, we do want to 584 // create a new one when the touch icon url is known. 585 if (mTouchIconLoader != null) { 586 mTouchIconLoader.mTab = null; 587 mTouchIconLoader = null; 588 } 589 590 // reset the error console 591 if (mErrorConsole != null) { 592 mErrorConsole.clearErrorMessages(); 593 if (mWebViewController.shouldShowErrorConsole()) { 594 mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE); 595 } 596 } 597 598 // Cancel the auto-login process. 599 if (mDeviceAccountLogin != null) { 600 mDeviceAccountLogin.cancel(); 601 mDeviceAccountLogin = null; 602 mWebViewController.hideAutoLogin(Tab.this); 603 } 604 605 // finally update the UI in the activity if it is in the foreground 606 mWebViewController.onPageStarted(Tab.this, view, favicon); 607 608 updateBookmarkedStatus(); 609 } 610 611 @Override 612 public void onPageFinished(WebView view, String url) { 613 if (!mInPageLoad) { 614 // In page navigation links (www.something.com#footer) will 615 // trigger an onPageFinished which we don't care about. 616 return; 617 } 618 if (!isPrivateBrowsingEnabled()) { 619 LogTag.logPageFinishedLoading( 620 url, SystemClock.uptimeMillis() - mLoadStartTime); 621 } 622 syncCurrentState(view, url); 623 mWebViewController.onPageFinished(Tab.this); 624 } 625 626 // return true if want to hijack the url to let another app to handle it 627 @Override 628 public boolean shouldOverrideUrlLoading(WebView view, String url) { 629 if (voiceSearchSourceIsGoogle()) { 630 // This method is called when the user clicks on a link. 631 // VoiceSearchMode is turned off when the user leaves the 632 // Google results page, so at this point the user must be on 633 // that page. If the user clicked a link on that page, assume 634 // that the voice search was effective, and broadcast an Intent 635 // so a receiver can take note of that fact. 636 Intent logIntent = new Intent(LoggingEvents.ACTION_LOG_EVENT); 637 logIntent.putExtra(LoggingEvents.EXTRA_EVENT, 638 LoggingEvents.VoiceSearch.RESULT_CLICKED); 639 mContext.sendBroadcast(logIntent); 640 } 641 if (mInForeground) { 642 return mWebViewController.shouldOverrideUrlLoading(Tab.this, 643 view, url); 644 } else { 645 return false; 646 } 647 } 648 649 /** 650 * Updates the security state. This method is called when we discover 651 * another resource to be loaded for this page (for example, 652 * javascript). While we update the security state, we do not update 653 * the lock icon until we are done loading, as it is slightly more 654 * secure this way. 655 */ 656 @Override 657 public void onLoadResource(WebView view, String url) { 658 if (url != null && url.length() > 0) { 659 // It is only if the page claims to be secure that we may have 660 // to update the security state: 661 if (mCurrentState.mSecurityState == SecurityState.SECURITY_STATE_SECURE) { 662 // If NOT a 'safe' url, change the state to mixed content! 663 if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) 664 || URLUtil.isAboutUrl(url))) { 665 mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_MIXED; 666 } 667 } 668 } 669 } 670 671 /** 672 * Show a dialog informing the user of the network error reported by 673 * WebCore if it is in the foreground. 674 */ 675 @Override 676 public void onReceivedError(WebView view, int errorCode, 677 String description, String failingUrl) { 678 if (errorCode != WebViewClient.ERROR_HOST_LOOKUP && 679 errorCode != WebViewClient.ERROR_CONNECT && 680 errorCode != WebViewClient.ERROR_BAD_URL && 681 errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME && 682 errorCode != WebViewClient.ERROR_FILE) { 683 queueError(errorCode, description); 684 685 // Don't log URLs when in private browsing mode 686 if (!isPrivateBrowsingEnabled()) { 687 Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl 688 + " " + description); 689 } 690 } 691 } 692 693 /** 694 * Check with the user if it is ok to resend POST data as the page they 695 * are trying to navigate to is the result of a POST. 696 */ 697 @Override 698 public void onFormResubmission(WebView view, final Message dontResend, 699 final Message resend) { 700 if (!mInForeground) { 701 dontResend.sendToTarget(); 702 return; 703 } 704 if (mDontResend != null) { 705 Log.w(LOGTAG, "onFormResubmission should not be called again " 706 + "while dialog is still up"); 707 dontResend.sendToTarget(); 708 return; 709 } 710 mDontResend = dontResend; 711 mResend = resend; 712 new AlertDialog.Builder(mContext).setTitle( 713 R.string.browserFrameFormResubmitLabel).setMessage( 714 R.string.browserFrameFormResubmitMessage) 715 .setPositiveButton(R.string.ok, 716 new DialogInterface.OnClickListener() { 717 public void onClick(DialogInterface dialog, 718 int which) { 719 if (mResend != null) { 720 mResend.sendToTarget(); 721 mResend = null; 722 mDontResend = null; 723 } 724 } 725 }).setNegativeButton(R.string.cancel, 726 new DialogInterface.OnClickListener() { 727 public void onClick(DialogInterface dialog, 728 int which) { 729 if (mDontResend != null) { 730 mDontResend.sendToTarget(); 731 mResend = null; 732 mDontResend = null; 733 } 734 } 735 }).setOnCancelListener(new OnCancelListener() { 736 public void onCancel(DialogInterface dialog) { 737 if (mDontResend != null) { 738 mDontResend.sendToTarget(); 739 mResend = null; 740 mDontResend = null; 741 } 742 } 743 }).show(); 744 } 745 746 /** 747 * Insert the url into the visited history database. 748 * @param url The url to be inserted. 749 * @param isReload True if this url is being reloaded. 750 * FIXME: Not sure what to do when reloading the page. 751 */ 752 @Override 753 public void doUpdateVisitedHistory(WebView view, String url, 754 boolean isReload) { 755 mWebViewController.doUpdateVisitedHistory(Tab.this, isReload); 756 } 757 758 /** 759 * Displays SSL error(s) dialog to the user. 760 */ 761 @Override 762 public void onReceivedSslError(final WebView view, 763 final SslErrorHandler handler, final SslError error) { 764 if (!mInForeground) { 765 handler.cancel(); 766 setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE); 767 return; 768 } 769 if (mSettings.showSecurityWarnings()) { 770 new AlertDialog.Builder(mContext) 771 .setTitle(R.string.security_warning) 772 .setMessage(R.string.ssl_warnings_header) 773 .setIcon(android.R.drawable.ic_dialog_alert) 774 .setPositiveButton(R.string.ssl_continue, 775 new DialogInterface.OnClickListener() { 776 @Override 777 public void onClick(DialogInterface dialog, 778 int whichButton) { 779 handler.proceed(); 780 handleProceededAfterSslError(error); 781 } 782 }) 783 .setNeutralButton(R.string.view_certificate, 784 new DialogInterface.OnClickListener() { 785 @Override 786 public void onClick(DialogInterface dialog, 787 int whichButton) { 788 mWebViewController.showSslCertificateOnError( 789 view, handler, error); 790 } 791 }) 792 .setNegativeButton(R.string.ssl_go_back, 793 new DialogInterface.OnClickListener() { 794 @Override 795 public void onClick(DialogInterface dialog, 796 int whichButton) { 797 dialog.cancel(); 798 } 799 }) 800 .setOnCancelListener( 801 new DialogInterface.OnCancelListener() { 802 @Override 803 public void onCancel(DialogInterface dialog) { 804 handler.cancel(); 805 setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE); 806 mWebViewController.onUserCanceledSsl(Tab.this); 807 } 808 }) 809 .show(); 810 } else { 811 handler.proceed(); 812 } 813 } 814 815 /** 816 * Called when an SSL error occurred while loading a resource, but the 817 * WebView but chose to proceed anyway based on a decision retained 818 * from a previous response to onReceivedSslError(). We update our 819 * security state to reflect this. 820 */ 821 @Override 822 public void onProceededAfterSslError(WebView view, SslError error) { 823 handleProceededAfterSslError(error); 824 } 825 826 /** 827 * Displays client certificate request to the user. 828 */ 829 @Override 830 public void onReceivedClientCertRequest(final WebView view, 831 final ClientCertRequestHandler handler, final String host_and_port) { 832 if (!mInForeground) { 833 handler.ignore(); 834 return; 835 } 836 int colon = host_and_port.lastIndexOf(':'); 837 String host; 838 int port; 839 if (colon == -1) { 840 host = host_and_port; 841 port = -1; 842 } else { 843 String portString = host_and_port.substring(colon + 1); 844 try { 845 port = Integer.parseInt(portString); 846 host = host_and_port.substring(0, colon); 847 } catch (NumberFormatException e) { 848 host = host_and_port; 849 port = -1; 850 } 851 } 852 KeyChain.choosePrivateKeyAlias( 853 mWebViewController.getActivity(), new KeyChainAliasCallback() { 854 @Override public void alias(String alias) { 855 if (alias == null) { 856 handler.cancel(); 857 return; 858 } 859 new KeyChainLookup(mContext, handler, alias).execute(); 860 } 861 }, null, null, host, port, null); 862 } 863 864 /** 865 * Handles an HTTP authentication request. 866 * 867 * @param handler The authentication handler 868 * @param host The host 869 * @param realm The realm 870 */ 871 @Override 872 public void onReceivedHttpAuthRequest(WebView view, 873 final HttpAuthHandler handler, final String host, 874 final String realm) { 875 mWebViewController.onReceivedHttpAuthRequest(Tab.this, view, handler, host, realm); 876 } 877 878 @Override 879 public WebResourceResponse shouldInterceptRequest(WebView view, 880 String url) { 881 WebResourceResponse res = HomeProvider.shouldInterceptRequest( 882 mContext, url); 883 return res; 884 } 885 886 @Override 887 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 888 if (!mInForeground) { 889 return false; 890 } 891 return mWebViewController.shouldOverrideKeyEvent(event); 892 } 893 894 @Override 895 public void onUnhandledKeyEvent(WebView view, KeyEvent event) { 896 if (!mInForeground) { 897 return; 898 } 899 mWebViewController.onUnhandledKeyEvent(event); 900 } 901 902 @Override 903 public void onReceivedLoginRequest(WebView view, String realm, 904 String account, String args) { 905 new DeviceAccountLogin(mWebViewController.getActivity(), view, Tab.this, mWebViewController) 906 .handleLogin(realm, account, args); 907 } 908 909 }; 910 911 private void syncCurrentState(WebView view, String url) { 912 // Sync state (in case of stop/timeout) 913 mCurrentState.mUrl = view.getUrl(); 914 if (mCurrentState.mUrl == null) { 915 mCurrentState.mUrl = ""; 916 } 917 mCurrentState.mOriginalUrl = view.getOriginalUrl(); 918 mCurrentState.mTitle = view.getTitle(); 919 mCurrentState.mFavicon = view.getFavicon(); 920 if (!URLUtil.isHttpsUrl(mCurrentState.mUrl)) { 921 // In case we stop when loading an HTTPS page from an HTTP page 922 // but before a provisional load occurred 923 mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; 924 mCurrentState.mSslCertificateError = null; 925 } 926 mCurrentState.mIncognito = view.isPrivateBrowsingEnabled(); 927 } 928 929 // Called by DeviceAccountLogin when the Tab needs to have the auto-login UI 930 // displayed. 931 void setDeviceAccountLogin(DeviceAccountLogin login) { 932 mDeviceAccountLogin = login; 933 } 934 935 // Returns non-null if the title bar should display the auto-login UI. 936 DeviceAccountLogin getDeviceAccountLogin() { 937 return mDeviceAccountLogin; 938 } 939 940 // ------------------------------------------------------------------------- 941 // WebChromeClient implementation for the main WebView 942 // ------------------------------------------------------------------------- 943 944 private final WebChromeClient mWebChromeClient = new WebChromeClient() { 945 // Helper method to create a new tab or sub window. 946 private void createWindow(final boolean dialog, final Message msg) { 947 WebView.WebViewTransport transport = 948 (WebView.WebViewTransport) msg.obj; 949 if (dialog) { 950 createSubWindow(); 951 mWebViewController.attachSubWindow(Tab.this); 952 transport.setWebView(mSubView); 953 } else { 954 final Tab newTab = mWebViewController.openTab(null, 955 Tab.this, true, true); 956 transport.setWebView(newTab.getWebView()); 957 } 958 msg.sendToTarget(); 959 } 960 961 @Override 962 public boolean onCreateWindow(WebView view, final boolean dialog, 963 final boolean userGesture, final Message resultMsg) { 964 // only allow new window or sub window for the foreground case 965 if (!mInForeground) { 966 return false; 967 } 968 // Short-circuit if we can't create any more tabs or sub windows. 969 if (dialog && mSubView != null) { 970 new AlertDialog.Builder(mContext) 971 .setTitle(R.string.too_many_subwindows_dialog_title) 972 .setIcon(android.R.drawable.ic_dialog_alert) 973 .setMessage(R.string.too_many_subwindows_dialog_message) 974 .setPositiveButton(R.string.ok, null) 975 .show(); 976 return false; 977 } else if (!mWebViewController.getTabControl().canCreateNewTab()) { 978 new AlertDialog.Builder(mContext) 979 .setTitle(R.string.too_many_windows_dialog_title) 980 .setIcon(android.R.drawable.ic_dialog_alert) 981 .setMessage(R.string.too_many_windows_dialog_message) 982 .setPositiveButton(R.string.ok, null) 983 .show(); 984 return false; 985 } 986 987 // Short-circuit if this was a user gesture. 988 if (userGesture) { 989 createWindow(dialog, resultMsg); 990 return true; 991 } 992 993 // Allow the popup and create the appropriate window. 994 final AlertDialog.OnClickListener allowListener = 995 new AlertDialog.OnClickListener() { 996 public void onClick(DialogInterface d, 997 int which) { 998 createWindow(dialog, resultMsg); 999 } 1000 }; 1001 1002 // Block the popup by returning a null WebView. 1003 final AlertDialog.OnClickListener blockListener = 1004 new AlertDialog.OnClickListener() { 1005 public void onClick(DialogInterface d, int which) { 1006 resultMsg.sendToTarget(); 1007 } 1008 }; 1009 1010 // Build a confirmation dialog to display to the user. 1011 final AlertDialog d = 1012 new AlertDialog.Builder(mContext) 1013 .setIcon(android.R.drawable.ic_dialog_alert) 1014 .setMessage(R.string.popup_window_attempt) 1015 .setPositiveButton(R.string.allow, allowListener) 1016 .setNegativeButton(R.string.block, blockListener) 1017 .setCancelable(false) 1018 .create(); 1019 1020 // Show the confirmation dialog. 1021 d.show(); 1022 return true; 1023 } 1024 1025 @Override 1026 public void onRequestFocus(WebView view) { 1027 if (!mInForeground) { 1028 mWebViewController.switchToTab(Tab.this); 1029 } 1030 } 1031 1032 @Override 1033 public void onCloseWindow(WebView window) { 1034 if (mParent != null) { 1035 // JavaScript can only close popup window. 1036 if (mInForeground) { 1037 mWebViewController.switchToTab(mParent); 1038 } 1039 mWebViewController.closeTab(Tab.this); 1040 } 1041 } 1042 1043 @Override 1044 public void onProgressChanged(WebView view, int newProgress) { 1045 mPageLoadProgress = newProgress; 1046 if (newProgress == 100) { 1047 mInPageLoad = false; 1048 } 1049 mWebViewController.onProgressChanged(Tab.this); 1050 } 1051 1052 @Override 1053 public void onReceivedTitle(WebView view, final String title) { 1054 mCurrentState.mTitle = title; 1055 mWebViewController.onReceivedTitle(Tab.this, title); 1056 } 1057 1058 @Override 1059 public void onReceivedIcon(WebView view, Bitmap icon) { 1060 mCurrentState.mFavicon = icon; 1061 mWebViewController.onFavicon(Tab.this, view, icon); 1062 } 1063 1064 @Override 1065 public void onReceivedTouchIconUrl(WebView view, String url, 1066 boolean precomposed) { 1067 final ContentResolver cr = mContext.getContentResolver(); 1068 // Let precomposed icons take precedence over non-composed 1069 // icons. 1070 if (precomposed && mTouchIconLoader != null) { 1071 mTouchIconLoader.cancel(false); 1072 mTouchIconLoader = null; 1073 } 1074 // Have only one async task at a time. 1075 if (mTouchIconLoader == null) { 1076 mTouchIconLoader = new DownloadTouchIcon(Tab.this, 1077 mContext, cr, view); 1078 mTouchIconLoader.execute(url); 1079 } 1080 } 1081 1082 @Override 1083 public void onShowCustomView(View view, 1084 WebChromeClient.CustomViewCallback callback) { 1085 Activity activity = mWebViewController.getActivity(); 1086 if (activity != null) { 1087 onShowCustomView(view, activity.getRequestedOrientation(), callback); 1088 } 1089 } 1090 1091 @Override 1092 public void onShowCustomView(View view, int requestedOrientation, 1093 WebChromeClient.CustomViewCallback callback) { 1094 if (mInForeground) mWebViewController.showCustomView(Tab.this, view, 1095 requestedOrientation, callback); 1096 } 1097 1098 @Override 1099 public void onHideCustomView() { 1100 if (mInForeground) mWebViewController.hideCustomView(); 1101 } 1102 1103 /** 1104 * The origin has exceeded its database quota. 1105 * @param url the URL that exceeded the quota 1106 * @param databaseIdentifier the identifier of the database on which the 1107 * transaction that caused the quota overflow was run 1108 * @param currentQuota the current quota for the origin. 1109 * @param estimatedSize the estimated size of the database. 1110 * @param totalUsedQuota is the sum of all origins' quota. 1111 * @param quotaUpdater The callback to run when a decision to allow or 1112 * deny quota has been made. Don't forget to call this! 1113 */ 1114 @Override 1115 public void onExceededDatabaseQuota(String url, 1116 String databaseIdentifier, long currentQuota, long estimatedSize, 1117 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 1118 mSettings.getWebStorageSizeManager() 1119 .onExceededDatabaseQuota(url, databaseIdentifier, 1120 currentQuota, estimatedSize, totalUsedQuota, 1121 quotaUpdater); 1122 } 1123 1124 /** 1125 * The Application Cache has exceeded its max size. 1126 * @param spaceNeeded is the amount of disk space that would be needed 1127 * in order for the last appcache operation to succeed. 1128 * @param totalUsedQuota is the sum of all origins' quota. 1129 * @param quotaUpdater A callback to inform the WebCore thread that a 1130 * new app cache size is available. This callback must always 1131 * be executed at some point to ensure that the sleeping 1132 * WebCore thread is woken up. 1133 */ 1134 @Override 1135 public void onReachedMaxAppCacheSize(long spaceNeeded, 1136 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 1137 mSettings.getWebStorageSizeManager() 1138 .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, 1139 quotaUpdater); 1140 } 1141 1142 /** 1143 * Instructs the browser to show a prompt to ask the user to set the 1144 * Geolocation permission state for the specified origin. 1145 * @param origin The origin for which Geolocation permissions are 1146 * requested. 1147 * @param callback The callback to call once the user has set the 1148 * Geolocation permission state. 1149 */ 1150 @Override 1151 public void onGeolocationPermissionsShowPrompt(String origin, 1152 GeolocationPermissions.Callback callback) { 1153 if (mInForeground) { 1154 getGeolocationPermissionsPrompt().show(origin, callback); 1155 } 1156 } 1157 1158 /** 1159 * Instructs the browser to hide the Geolocation permissions prompt. 1160 */ 1161 @Override 1162 public void onGeolocationPermissionsHidePrompt() { 1163 if (mInForeground && mGeolocationPermissionsPrompt != null) { 1164 mGeolocationPermissionsPrompt.hide(); 1165 } 1166 } 1167 1168 /* Adds a JavaScript error message to the system log and if the JS 1169 * console is enabled in the about:debug options, to that console 1170 * also. 1171 * @param consoleMessage the message object. 1172 */ 1173 @Override 1174 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 1175 if (mInForeground) { 1176 // call getErrorConsole(true) so it will create one if needed 1177 ErrorConsoleView errorConsole = getErrorConsole(true); 1178 errorConsole.addErrorMessage(consoleMessage); 1179 if (mWebViewController.shouldShowErrorConsole() 1180 && errorConsole.getShowState() != 1181 ErrorConsoleView.SHOW_MAXIMIZED) { 1182 errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); 1183 } 1184 } 1185 1186 // Don't log console messages in private browsing mode 1187 if (isPrivateBrowsingEnabled()) return true; 1188 1189 String message = "Console: " + consoleMessage.message() + " " 1190 + consoleMessage.sourceId() + ":" 1191 + consoleMessage.lineNumber(); 1192 1193 switch (consoleMessage.messageLevel()) { 1194 case TIP: 1195 Log.v(CONSOLE_LOGTAG, message); 1196 break; 1197 case LOG: 1198 Log.i(CONSOLE_LOGTAG, message); 1199 break; 1200 case WARNING: 1201 Log.w(CONSOLE_LOGTAG, message); 1202 break; 1203 case ERROR: 1204 Log.e(CONSOLE_LOGTAG, message); 1205 break; 1206 case DEBUG: 1207 Log.d(CONSOLE_LOGTAG, message); 1208 break; 1209 } 1210 1211 return true; 1212 } 1213 1214 /** 1215 * Ask the browser for an icon to represent a <video> element. 1216 * This icon will be used if the Web page did not specify a poster attribute. 1217 * @return Bitmap The icon or null if no such icon is available. 1218 */ 1219 @Override 1220 public Bitmap getDefaultVideoPoster() { 1221 if (mInForeground) { 1222 return mWebViewController.getDefaultVideoPoster(); 1223 } 1224 return null; 1225 } 1226 1227 /** 1228 * Ask the host application for a custom progress view to show while 1229 * a <video> is loading. 1230 * @return View The progress view. 1231 */ 1232 @Override 1233 public View getVideoLoadingProgressView() { 1234 if (mInForeground) { 1235 return mWebViewController.getVideoLoadingProgressView(); 1236 } 1237 return null; 1238 } 1239 1240 @Override 1241 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { 1242 if (mInForeground) { 1243 mWebViewController.openFileChooser(uploadMsg, acceptType); 1244 } else { 1245 uploadMsg.onReceiveValue(null); 1246 } 1247 } 1248 1249 /** 1250 * Deliver a list of already-visited URLs 1251 */ 1252 @Override 1253 public void getVisitedHistory(final ValueCallback<String[]> callback) { 1254 mWebViewController.getVisitedHistory(callback); 1255 } 1256 1257 @Override 1258 public void setupAutoFill(Message message) { 1259 // Prompt the user to set up their profile. 1260 final Message msg = message; 1261 AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 1262 LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 1263 Context.LAYOUT_INFLATER_SERVICE); 1264 final View layout = inflater.inflate(R.layout.setup_autofill_dialog, null); 1265 1266 builder.setView(layout) 1267 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 1268 @Override 1269 public void onClick(DialogInterface dialog, int id) { 1270 CheckBox disableAutoFill = (CheckBox) layout.findViewById( 1271 R.id.setup_autofill_dialog_disable_autofill); 1272 1273 if (disableAutoFill.isChecked()) { 1274 // Disable autofill and show a toast with how to turn it on again. 1275 mSettings.setAutofillEnabled(false); 1276 Toast.makeText(mContext, 1277 R.string.autofill_setup_dialog_negative_toast, 1278 Toast.LENGTH_LONG).show(); 1279 } else { 1280 // Take user to the AutoFill profile editor. When they return, 1281 // we will send the message that we pass here which will trigger 1282 // the form to get filled out with their new profile. 1283 mWebViewController.setupAutoFill(msg); 1284 } 1285 } 1286 }) 1287 .setNegativeButton(R.string.cancel, null) 1288 .show(); 1289 } 1290 }; 1291 1292 // ------------------------------------------------------------------------- 1293 // WebViewClient implementation for the sub window 1294 // ------------------------------------------------------------------------- 1295 1296 // Subclass of WebViewClient used in subwindows to notify the main 1297 // WebViewClient of certain WebView activities. 1298 private static class SubWindowClient extends WebViewClient { 1299 // The main WebViewClient. 1300 private final WebViewClient mClient; 1301 private final WebViewController mController; 1302 1303 SubWindowClient(WebViewClient client, WebViewController controller) { 1304 mClient = client; 1305 mController = controller; 1306 } 1307 @Override 1308 public void onPageStarted(WebView view, String url, Bitmap favicon) { 1309 // Unlike the others, do not call mClient's version, which would 1310 // change the progress bar. However, we do want to remove the 1311 // find or select dialog. 1312 mController.endActionMode(); 1313 } 1314 @Override 1315 public void doUpdateVisitedHistory(WebView view, String url, 1316 boolean isReload) { 1317 mClient.doUpdateVisitedHistory(view, url, isReload); 1318 } 1319 @Override 1320 public boolean shouldOverrideUrlLoading(WebView view, String url) { 1321 return mClient.shouldOverrideUrlLoading(view, url); 1322 } 1323 @Override 1324 public void onReceivedSslError(WebView view, SslErrorHandler handler, 1325 SslError error) { 1326 mClient.onReceivedSslError(view, handler, error); 1327 } 1328 @Override 1329 public void onReceivedClientCertRequest(WebView view, 1330 ClientCertRequestHandler handler, String host_and_port) { 1331 mClient.onReceivedClientCertRequest(view, handler, host_and_port); 1332 } 1333 @Override 1334 public void onReceivedHttpAuthRequest(WebView view, 1335 HttpAuthHandler handler, String host, String realm) { 1336 mClient.onReceivedHttpAuthRequest(view, handler, host, realm); 1337 } 1338 @Override 1339 public void onFormResubmission(WebView view, Message dontResend, 1340 Message resend) { 1341 mClient.onFormResubmission(view, dontResend, resend); 1342 } 1343 @Override 1344 public void onReceivedError(WebView view, int errorCode, 1345 String description, String failingUrl) { 1346 mClient.onReceivedError(view, errorCode, description, failingUrl); 1347 } 1348 @Override 1349 public boolean shouldOverrideKeyEvent(WebView view, 1350 android.view.KeyEvent event) { 1351 return mClient.shouldOverrideKeyEvent(view, event); 1352 } 1353 @Override 1354 public void onUnhandledKeyEvent(WebView view, 1355 android.view.KeyEvent event) { 1356 mClient.onUnhandledKeyEvent(view, event); 1357 } 1358 } 1359 1360 // ------------------------------------------------------------------------- 1361 // WebChromeClient implementation for the sub window 1362 // ------------------------------------------------------------------------- 1363 1364 private class SubWindowChromeClient extends WebChromeClient { 1365 // The main WebChromeClient. 1366 private final WebChromeClient mClient; 1367 1368 SubWindowChromeClient(WebChromeClient client) { 1369 mClient = client; 1370 } 1371 @Override 1372 public void onProgressChanged(WebView view, int newProgress) { 1373 mClient.onProgressChanged(view, newProgress); 1374 } 1375 @Override 1376 public boolean onCreateWindow(WebView view, boolean dialog, 1377 boolean userGesture, android.os.Message resultMsg) { 1378 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg); 1379 } 1380 @Override 1381 public void onCloseWindow(WebView window) { 1382 if (window != mSubView) { 1383 Log.e(LOGTAG, "Can't close the window"); 1384 } 1385 mWebViewController.dismissSubWindow(Tab.this); 1386 } 1387 } 1388 1389 // ------------------------------------------------------------------------- 1390 1391 // Construct a new tab 1392 Tab(WebViewController wvcontroller, WebView w) { 1393 this(wvcontroller, w, null); 1394 } 1395 1396 Tab(WebViewController wvcontroller, Bundle state) { 1397 this(wvcontroller, null, state); 1398 } 1399 1400 Tab(WebViewController wvcontroller, WebView w, Bundle state) { 1401 mWebViewController = wvcontroller; 1402 mContext = mWebViewController.getContext(); 1403 mSettings = BrowserSettings.getInstance(); 1404 mDataController = DataController.getInstance(mContext); 1405 mCurrentState = new PageState(mContext, w != null 1406 ? w.isPrivateBrowsingEnabled() : false); 1407 mInPageLoad = false; 1408 mInForeground = false; 1409 1410 mDownloadListener = new DownloadListener() { 1411 public void onDownloadStart(String url, String userAgent, 1412 String contentDisposition, String mimetype, 1413 long contentLength) { 1414 mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition, 1415 mimetype, contentLength); 1416 } 1417 }; 1418 mWebBackForwardListClient = new WebBackForwardListClient() { 1419 @Override 1420 public void onNewHistoryItem(WebHistoryItem item) { 1421 if (isInVoiceSearchMode()) { 1422 item.setCustomData(mVoiceSearchData.mVoiceSearchIntent); 1423 } 1424 if (mClearHistoryUrlPattern != null) { 1425 boolean match = 1426 mClearHistoryUrlPattern.matcher(item.getOriginalUrl()).matches(); 1427 if (LOGD_ENABLED) { 1428 Log.d(LOGTAG, "onNewHistoryItem: match=" + match + "\n\t" 1429 + item.getUrl() + "\n\t" 1430 + mClearHistoryUrlPattern); 1431 } 1432 if (match) { 1433 if (mMainView != null) { 1434 mMainView.clearHistory(); 1435 } 1436 } 1437 mClearHistoryUrlPattern = null; 1438 } 1439 } 1440 @Override 1441 public void onIndexChanged(WebHistoryItem item, int index) { 1442 Object data = item.getCustomData(); 1443 if (data != null && data instanceof Intent) { 1444 activateVoiceSearchMode((Intent) data); 1445 } 1446 } 1447 }; 1448 1449 mCaptureWidth = mContext.getResources().getDimensionPixelSize( 1450 R.dimen.tab_thumbnail_width); 1451 mCaptureHeight = mContext.getResources().getDimensionPixelSize( 1452 R.dimen.tab_thumbnail_height); 1453 updateShouldCaptureThumbnails(); 1454 restoreState(state); 1455 if (getId() == -1) { 1456 mId = TabControl.getNextId(); 1457 } 1458 setWebView(w); 1459 mHandler = new Handler() { 1460 @Override 1461 public void handleMessage(Message m) { 1462 switch (m.what) { 1463 case MSG_CAPTURE: 1464 capture(); 1465 break; 1466 } 1467 } 1468 }; 1469 } 1470 1471 /** 1472 * This is used to get a new ID when the tab has been preloaded, before it is displayed and 1473 * added to TabControl. Preloaded tabs can be created before restoreInstanceState, leading 1474 * to overlapping IDs between the preloaded and restored tabs. 1475 */ 1476 public void refreshIdAfterPreload() { 1477 mId = TabControl.getNextId(); 1478 } 1479 1480 public void updateShouldCaptureThumbnails() { 1481 if (mWebViewController.shouldCaptureThumbnails()) { 1482 synchronized (Tab.this) { 1483 if (mCapture == null) { 1484 mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight, 1485 Bitmap.Config.RGB_565); 1486 mCapture.eraseColor(Color.WHITE); 1487 if (mInForeground) { 1488 postCapture(); 1489 } 1490 } 1491 } 1492 } else { 1493 synchronized (Tab.this) { 1494 mCapture = null; 1495 deleteThumbnail(); 1496 } 1497 } 1498 } 1499 1500 public void setController(WebViewController ctl) { 1501 mWebViewController = ctl; 1502 updateShouldCaptureThumbnails(); 1503 } 1504 1505 public long getId() { 1506 return mId; 1507 } 1508 1509 /** 1510 * Sets the WebView for this tab, correctly removing the old WebView from 1511 * the container view. 1512 */ 1513 void setWebView(WebView w) { 1514 if (mMainView == w) { 1515 return; 1516 } 1517 1518 // If the WebView is changing, the page will be reloaded, so any ongoing 1519 // Geolocation permission requests are void. 1520 if (mGeolocationPermissionsPrompt != null) { 1521 mGeolocationPermissionsPrompt.hide(); 1522 } 1523 1524 mWebViewController.onSetWebView(this, w); 1525 1526 if (mMainView != null) { 1527 mMainView.setPictureListener(null); 1528 if (w != null) { 1529 syncCurrentState(w, null); 1530 } else { 1531 mCurrentState = new PageState(mContext, false); 1532 } 1533 } 1534 // set the new one 1535 mMainView = w; 1536 // attach the WebViewClient, WebChromeClient and DownloadListener 1537 if (mMainView != null) { 1538 mMainView.setWebViewClient(mWebViewClient); 1539 mMainView.setWebChromeClient(mWebChromeClient); 1540 // Attach DownloadManager so that downloads can start in an active 1541 // or a non-active window. This can happen when going to a site that 1542 // does a redirect after a period of time. The user could have 1543 // switched to another tab while waiting for the download to start. 1544 mMainView.setDownloadListener(mDownloadListener); 1545 mMainView.setWebBackForwardListClient(mWebBackForwardListClient); 1546 TabControl tc = mWebViewController.getTabControl(); 1547 if (tc != null && tc.getOnThumbnailUpdatedListener() != null) { 1548 mMainView.setPictureListener(this); 1549 } 1550 if (mSavedState != null) { 1551 WebBackForwardList restoredState 1552 = mMainView.restoreState(mSavedState); 1553 if (restoredState == null || restoredState.getSize() == 0) { 1554 Log.w(LOGTAG, "Failed to restore WebView state!"); 1555 loadUrl(mCurrentState.mOriginalUrl, null); 1556 } 1557 mSavedState = null; 1558 } 1559 } 1560 } 1561 1562 /** 1563 * Destroy the tab's main WebView and subWindow if any 1564 */ 1565 void destroy() { 1566 if (mMainView != null) { 1567 dismissSubWindow(); 1568 // Make sure the embedded title bar isn't still attached 1569 mMainView.setEmbeddedTitleBar(null); 1570 // save the WebView to call destroy() after detach it from the tab 1571 WebView webView = mMainView; 1572 setWebView(null); 1573 webView.destroy(); 1574 } 1575 } 1576 1577 /** 1578 * Remove the tab from the parent 1579 */ 1580 void removeFromTree() { 1581 // detach the children 1582 if (mChildren != null) { 1583 for(Tab t : mChildren) { 1584 t.setParent(null); 1585 } 1586 } 1587 // remove itself from the parent list 1588 if (mParent != null) { 1589 mParent.mChildren.remove(this); 1590 } 1591 deleteThumbnail(); 1592 } 1593 1594 /** 1595 * Create a new subwindow unless a subwindow already exists. 1596 * @return True if a new subwindow was created. False if one already exists. 1597 */ 1598 boolean createSubWindow() { 1599 if (mSubView == null) { 1600 mWebViewController.createSubWindow(this); 1601 mSubView.setWebViewClient(new SubWindowClient(mWebViewClient, 1602 mWebViewController)); 1603 mSubView.setWebChromeClient(new SubWindowChromeClient( 1604 mWebChromeClient)); 1605 // Set a different DownloadListener for the mSubView, since it will 1606 // just need to dismiss the mSubView, rather than close the Tab 1607 mSubView.setDownloadListener(new DownloadListener() { 1608 public void onDownloadStart(String url, String userAgent, 1609 String contentDisposition, String mimetype, 1610 long contentLength) { 1611 mWebViewController.onDownloadStart(Tab.this, url, userAgent, 1612 contentDisposition, mimetype, contentLength); 1613 if (mSubView.copyBackForwardList().getSize() == 0) { 1614 // This subwindow was opened for the sole purpose of 1615 // downloading a file. Remove it. 1616 mWebViewController.dismissSubWindow(Tab.this); 1617 } 1618 } 1619 }); 1620 mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity()); 1621 return true; 1622 } 1623 return false; 1624 } 1625 1626 /** 1627 * Dismiss the subWindow for the tab. 1628 */ 1629 void dismissSubWindow() { 1630 if (mSubView != null) { 1631 mWebViewController.endActionMode(); 1632 mSubView.destroy(); 1633 mSubView = null; 1634 mSubViewContainer = null; 1635 } 1636 } 1637 1638 1639 /** 1640 * Set the parent tab of this tab. 1641 */ 1642 void setParent(Tab parent) { 1643 if (parent == this) { 1644 throw new IllegalStateException("Cannot set parent to self!"); 1645 } 1646 mParent = parent; 1647 // This tab may have been freed due to low memory. If that is the case, 1648 // the parent tab id is already saved. If we are changing that id 1649 // (most likely due to removing the parent tab) we must update the 1650 // parent tab id in the saved Bundle. 1651 if (mSavedState != null) { 1652 if (parent == null) { 1653 mSavedState.remove(PARENTTAB); 1654 } else { 1655 mSavedState.putLong(PARENTTAB, parent.getId()); 1656 } 1657 } 1658 1659 // Sync the WebView useragent with the parent 1660 if (parent != null && mSettings.hasDesktopUseragent(parent.getWebView()) 1661 != mSettings.hasDesktopUseragent(getWebView())) { 1662 mSettings.toggleDesktopUseragent(getWebView()); 1663 } 1664 1665 if (parent != null && parent.getId() == getId()) { 1666 throw new IllegalStateException("Parent has same ID as child!"); 1667 } 1668 } 1669 1670 /** 1671 * If this Tab was created through another Tab, then this method returns 1672 * that Tab. 1673 * @return the Tab parent or null 1674 */ 1675 public Tab getParent() { 1676 return mParent; 1677 } 1678 1679 /** 1680 * When a Tab is created through the content of another Tab, then we 1681 * associate the Tabs. 1682 * @param child the Tab that was created from this Tab 1683 */ 1684 void addChildTab(Tab child) { 1685 if (mChildren == null) { 1686 mChildren = new Vector<Tab>(); 1687 } 1688 mChildren.add(child); 1689 child.setParent(this); 1690 } 1691 1692 Vector<Tab> getChildren() { 1693 return mChildren; 1694 } 1695 1696 void resume() { 1697 if (mMainView != null) { 1698 setupHwAcceleration(mMainView); 1699 mMainView.onResume(); 1700 if (mSubView != null) { 1701 mSubView.onResume(); 1702 } 1703 } 1704 } 1705 1706 private void setupHwAcceleration(View web) { 1707 if (web == null) return; 1708 BrowserSettings settings = BrowserSettings.getInstance(); 1709 if (settings.isHardwareAccelerated()) { 1710 web.setLayerType(View.LAYER_TYPE_NONE, null); 1711 } else { 1712 web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 1713 } 1714 } 1715 1716 void pause() { 1717 if (mMainView != null) { 1718 mMainView.onPause(); 1719 if (mSubView != null) { 1720 mSubView.onPause(); 1721 } 1722 } 1723 } 1724 1725 void putInForeground() { 1726 if (mInForeground) { 1727 return; 1728 } 1729 mInForeground = true; 1730 resume(); 1731 Activity activity = mWebViewController.getActivity(); 1732 mMainView.setOnCreateContextMenuListener(activity); 1733 if (mSubView != null) { 1734 mSubView.setOnCreateContextMenuListener(activity); 1735 } 1736 // Show the pending error dialog if the queue is not empty 1737 if (mQueuedErrors != null && mQueuedErrors.size() > 0) { 1738 showError(mQueuedErrors.getFirst()); 1739 } 1740 mWebViewController.bookmarkedStatusHasChanged(this); 1741 } 1742 1743 void putInBackground() { 1744 if (!mInForeground) { 1745 return; 1746 } 1747 capture(); 1748 mInForeground = false; 1749 pause(); 1750 mMainView.setOnCreateContextMenuListener(null); 1751 if (mSubView != null) { 1752 mSubView.setOnCreateContextMenuListener(null); 1753 } 1754 } 1755 1756 boolean inForeground() { 1757 return mInForeground; 1758 } 1759 1760 /** 1761 * Return the top window of this tab; either the subwindow if it is not 1762 * null or the main window. 1763 * @return The top window of this tab. 1764 */ 1765 WebView getTopWindow() { 1766 if (mSubView != null) { 1767 return mSubView; 1768 } 1769 return mMainView; 1770 } 1771 1772 /** 1773 * Return the main window of this tab. Note: if a tab is freed in the 1774 * background, this can return null. It is only guaranteed to be 1775 * non-null for the current tab. 1776 * @return The main WebView of this tab. 1777 */ 1778 WebView getWebView() { 1779 return mMainView; 1780 } 1781 1782 void setViewContainer(View container) { 1783 mContainer = container; 1784 } 1785 1786 View getViewContainer() { 1787 return mContainer; 1788 } 1789 1790 /** 1791 * Return whether private browsing is enabled for the main window of 1792 * this tab. 1793 * @return True if private browsing is enabled. 1794 */ 1795 boolean isPrivateBrowsingEnabled() { 1796 return mCurrentState.mIncognito; 1797 } 1798 1799 /** 1800 * Return the subwindow of this tab or null if there is no subwindow. 1801 * @return The subwindow of this tab or null. 1802 */ 1803 WebView getSubWebView() { 1804 return mSubView; 1805 } 1806 1807 void setSubWebView(WebView subView) { 1808 mSubView = subView; 1809 } 1810 1811 View getSubViewContainer() { 1812 return mSubViewContainer; 1813 } 1814 1815 void setSubViewContainer(View subViewContainer) { 1816 mSubViewContainer = subViewContainer; 1817 } 1818 1819 /** 1820 * @return The geolocation permissions prompt for this tab. 1821 */ 1822 GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() { 1823 if (mGeolocationPermissionsPrompt == null) { 1824 ViewStub stub = (ViewStub) mContainer 1825 .findViewById(R.id.geolocation_permissions_prompt); 1826 mGeolocationPermissionsPrompt = (GeolocationPermissionsPrompt) stub 1827 .inflate(); 1828 } 1829 return mGeolocationPermissionsPrompt; 1830 } 1831 1832 /** 1833 * @return The application id string 1834 */ 1835 String getAppId() { 1836 return mAppId; 1837 } 1838 1839 /** 1840 * Set the application id string 1841 * @param id 1842 */ 1843 void setAppId(String id) { 1844 mAppId = id; 1845 } 1846 1847 boolean closeOnBack() { 1848 return mCloseOnBack; 1849 } 1850 1851 void setCloseOnBack(boolean close) { 1852 mCloseOnBack = close; 1853 } 1854 1855 String getUrl() { 1856 return UrlUtils.filteredUrl(mCurrentState.mUrl); 1857 } 1858 1859 String getOriginalUrl() { 1860 if (mCurrentState.mOriginalUrl == null) { 1861 return getUrl(); 1862 } 1863 return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl); 1864 } 1865 1866 /** 1867 * Get the title of this tab. 1868 */ 1869 String getTitle() { 1870 if (mCurrentState.mTitle == null && mInPageLoad) { 1871 return mContext.getString(R.string.title_bar_loading); 1872 } 1873 return mCurrentState.mTitle; 1874 } 1875 1876 /** 1877 * Get the favicon of this tab. 1878 */ 1879 Bitmap getFavicon() { 1880 if (mCurrentState.mFavicon != null) { 1881 return mCurrentState.mFavicon; 1882 } 1883 return getDefaultFavicon(mContext); 1884 } 1885 1886 public boolean isBookmarkedSite() { 1887 return mCurrentState.mIsBookmarkedSite; 1888 } 1889 1890 /** 1891 * Return the tab's error console. Creates the console if createIfNEcessary 1892 * is true and we haven't already created the console. 1893 * @param createIfNecessary Flag to indicate if the console should be 1894 * created if it has not been already. 1895 * @return The tab's error console, or null if one has not been created and 1896 * createIfNecessary is false. 1897 */ 1898 ErrorConsoleView getErrorConsole(boolean createIfNecessary) { 1899 if (createIfNecessary && mErrorConsole == null) { 1900 mErrorConsole = new ErrorConsoleView(mContext); 1901 mErrorConsole.setWebView(mMainView); 1902 } 1903 return mErrorConsole; 1904 } 1905 1906 /** 1907 * Sets the security state, clears the SSL certificate error and informs 1908 * the controller. 1909 */ 1910 private void setSecurityState(SecurityState securityState) { 1911 mCurrentState.mSecurityState = securityState; 1912 mCurrentState.mSslCertificateError = null; 1913 mWebViewController.onUpdatedSecurityState(this); 1914 } 1915 1916 /** 1917 * @return The tab's security state. 1918 */ 1919 SecurityState getSecurityState() { 1920 return mCurrentState.mSecurityState; 1921 } 1922 1923 /** 1924 * Gets the SSL certificate error, if any, for the page's main resource. 1925 * This is only non-null when the security state is 1926 * SECURITY_STATE_BAD_CERTIFICATE. 1927 */ 1928 SslError getSslCertificateError() { 1929 return mCurrentState.mSslCertificateError; 1930 } 1931 1932 int getLoadProgress() { 1933 if (mInPageLoad) { 1934 return mPageLoadProgress; 1935 } 1936 return 100; 1937 } 1938 1939 /** 1940 * @return TRUE if onPageStarted is called while onPageFinished is not 1941 * called yet. 1942 */ 1943 boolean inPageLoad() { 1944 return mInPageLoad; 1945 } 1946 1947 // force mInLoad to be false. This should only be called before closing the 1948 // tab to ensure BrowserActivity's pauseWebViewTimers() is called correctly. 1949 void clearInPageLoad() { 1950 mInPageLoad = false; 1951 } 1952 1953 /** 1954 * @return The Bundle with the tab's state if it can be saved, otherwise null 1955 */ 1956 public Bundle saveState() { 1957 // If the WebView is null it means we ran low on memory and we already 1958 // stored the saved state in mSavedState. 1959 if (mMainView == null) { 1960 return mSavedState; 1961 } 1962 1963 if (TextUtils.isEmpty(mCurrentState.mUrl)) { 1964 return null; 1965 } 1966 1967 mSavedState = new Bundle(); 1968 WebBackForwardList savedList = mMainView.saveState(mSavedState); 1969 if (savedList == null || savedList.getSize() == 0) { 1970 Log.w(LOGTAG, "Failed to save back/forward list for " 1971 + mCurrentState.mUrl); 1972 } 1973 1974 mSavedState.putLong(ID, mId); 1975 mSavedState.putString(CURRURL, mCurrentState.mUrl); 1976 mSavedState.putString(CURRTITLE, mCurrentState.mTitle); 1977 mSavedState.putBoolean(INCOGNITO, mMainView.isPrivateBrowsingEnabled()); 1978 if (mAppId != null) { 1979 mSavedState.putString(APPID, mAppId); 1980 } 1981 mSavedState.putBoolean(CLOSEFLAG, mCloseOnBack); 1982 // Remember the parent tab so the relationship can be restored. 1983 if (mParent != null) { 1984 mSavedState.putLong(PARENTTAB, mParent.mId); 1985 } 1986 mSavedState.putBoolean(USERAGENT, 1987 mSettings.hasDesktopUseragent(getWebView())); 1988 return mSavedState; 1989 } 1990 1991 /* 1992 * Restore the state of the tab. 1993 */ 1994 private void restoreState(Bundle b) { 1995 mSavedState = b; 1996 if (mSavedState == null) { 1997 return; 1998 } 1999 // Restore the internal state even if the WebView fails to restore. 2000 // This will maintain the app id, original url and close-on-exit values. 2001 mId = b.getLong(ID); 2002 mAppId = b.getString(APPID); 2003 mCloseOnBack = b.getBoolean(CLOSEFLAG); 2004 if (b.getBoolean(USERAGENT) 2005 != mSettings.hasDesktopUseragent(getWebView())) { 2006 mSettings.toggleDesktopUseragent(getWebView()); 2007 } 2008 String url = b.getString(CURRURL); 2009 String title = b.getString(CURRTITLE); 2010 boolean incognito = b.getBoolean(INCOGNITO); 2011 mCurrentState = new PageState(mContext, incognito, url, null); 2012 mCurrentState.mTitle = title; 2013 synchronized (Tab.this) { 2014 if (mCapture != null) { 2015 DataController.getInstance(mContext).loadThumbnail(this); 2016 } 2017 } 2018 } 2019 2020 public void updateBookmarkedStatus() { 2021 mDataController.queryBookmarkStatus(getUrl(), mIsBookmarkCallback); 2022 } 2023 2024 private DataController.OnQueryUrlIsBookmark mIsBookmarkCallback 2025 = new DataController.OnQueryUrlIsBookmark() { 2026 @Override 2027 public void onQueryUrlIsBookmark(String url, boolean isBookmark) { 2028 if (mCurrentState.mUrl.equals(url)) { 2029 mCurrentState.mIsBookmarkedSite = isBookmark; 2030 mWebViewController.bookmarkedStatusHasChanged(Tab.this); 2031 } 2032 } 2033 }; 2034 2035 public Bitmap getScreenshot() { 2036 synchronized (Tab.this) { 2037 return mCapture; 2038 } 2039 } 2040 2041 public boolean isSnapshot() { 2042 return false; 2043 } 2044 2045 public ContentValues createSnapshotValues() { 2046 if (mMainView == null) return null; 2047 SnapshotByteArrayOutputStream bos = new SnapshotByteArrayOutputStream(); 2048 try { 2049 GZIPOutputStream stream = new GZIPOutputStream(bos); 2050 if (!mMainView.saveViewState(stream)) { 2051 return null; 2052 } 2053 stream.flush(); 2054 stream.close(); 2055 } catch (Exception e) { 2056 Log.w(LOGTAG, "Failed to save view state", e); 2057 return null; 2058 } 2059 byte[] data = bos.toByteArray(); 2060 ContentValues values = new ContentValues(); 2061 values.put(Snapshots.TITLE, mCurrentState.mTitle); 2062 values.put(Snapshots.URL, mCurrentState.mUrl); 2063 values.put(Snapshots.VIEWSTATE, data); 2064 values.put(Snapshots.BACKGROUND, mMainView.getPageBackgroundColor()); 2065 values.put(Snapshots.DATE_CREATED, System.currentTimeMillis()); 2066 values.put(Snapshots.FAVICON, compressBitmap(getFavicon())); 2067 Bitmap screenshot = Controller.createScreenshot(mMainView, 2068 Controller.getDesiredThumbnailWidth(mContext), 2069 Controller.getDesiredThumbnailHeight(mContext)); 2070 values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot)); 2071 return values; 2072 } 2073 2074 public byte[] compressBitmap(Bitmap bitmap) { 2075 if (bitmap == null) { 2076 return null; 2077 } 2078 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 2079 bitmap.compress(CompressFormat.PNG, 100, stream); 2080 return stream.toByteArray(); 2081 } 2082 2083 public void loadUrl(String url, Map<String, String> headers) { 2084 if (mMainView != null) { 2085 mPageLoadProgress = INITIAL_PROGRESS; 2086 mInPageLoad = true; 2087 mCurrentState = new PageState(mContext, false, url, null); 2088 mWebViewController.onPageStarted(this, mMainView, null); 2089 mMainView.loadUrl(url, headers); 2090 } 2091 } 2092 2093 protected void capture() { 2094 if (mMainView == null || mCapture == null) return; 2095 if (mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0) { 2096 return; 2097 } 2098 Canvas c = new Canvas(mCapture); 2099 final int left = mMainView.getScrollX(); 2100 final int top = mMainView.getScrollY() + mMainView.getVisibleTitleHeight(); 2101 int state = c.save(); 2102 c.translate(-left, -top); 2103 float scale = mCaptureWidth / (float) mMainView.getWidth(); 2104 c.scale(scale, scale, left, top); 2105 if (mMainView instanceof BrowserWebView) { 2106 ((BrowserWebView)mMainView).drawContent(c); 2107 } else { 2108 mMainView.draw(c); 2109 } 2110 c.restoreToCount(state); 2111 // manually anti-alias the edges for the tilt 2112 c.drawRect(0, 0, 1, mCapture.getHeight(), sAlphaPaint); 2113 c.drawRect(mCapture.getWidth() - 1, 0, mCapture.getWidth(), 2114 mCapture.getHeight(), sAlphaPaint); 2115 c.drawRect(0, 0, mCapture.getWidth(), 1, sAlphaPaint); 2116 c.drawRect(0, mCapture.getHeight() - 1, mCapture.getWidth(), 2117 mCapture.getHeight(), sAlphaPaint); 2118 c.setBitmap(null); 2119 mHandler.removeMessages(MSG_CAPTURE); 2120 persistThumbnail(); 2121 TabControl tc = mWebViewController.getTabControl(); 2122 if (tc != null) { 2123 OnThumbnailUpdatedListener updateListener 2124 = tc.getOnThumbnailUpdatedListener(); 2125 if (updateListener != null) { 2126 updateListener.onThumbnailUpdated(this); 2127 } 2128 } 2129 } 2130 2131 @Override 2132 public void onNewPicture(WebView view, Picture picture) { 2133 //update screenshot 2134 postCapture(); 2135 } 2136 2137 private void postCapture() { 2138 if (!mHandler.hasMessages(MSG_CAPTURE)) { 2139 mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY); 2140 } 2141 } 2142 2143 public boolean canGoBack() { 2144 return mMainView != null ? mMainView.canGoBack() : false; 2145 } 2146 2147 public boolean canGoForward() { 2148 return mMainView != null ? mMainView.canGoForward() : false; 2149 } 2150 2151 public void goBack() { 2152 if (mMainView != null) { 2153 mMainView.goBack(); 2154 } 2155 } 2156 2157 public void goForward() { 2158 if (mMainView != null) { 2159 mMainView.goForward(); 2160 } 2161 } 2162 2163 /** 2164 * Causes the tab back/forward stack to be cleared once, if the given URL is the next URL 2165 * to be added to the stack. 2166 * 2167 * This is used to ensure that preloaded URLs that are not subsequently seen by the user do 2168 * not appear in the back stack. 2169 */ 2170 public void clearBackStackWhenItemAdded(Pattern urlPattern) { 2171 mClearHistoryUrlPattern = urlPattern; 2172 } 2173 2174 protected void persistThumbnail() { 2175 DataController.getInstance(mContext).saveThumbnail(this); 2176 } 2177 2178 protected void deleteThumbnail() { 2179 DataController.getInstance(mContext).deleteThumbnail(this); 2180 } 2181 2182 void updateCaptureFromBlob(byte[] blob) { 2183 synchronized (Tab.this) { 2184 if (mCapture == null) { 2185 return; 2186 } 2187 ByteBuffer buffer = ByteBuffer.wrap(blob); 2188 try { 2189 mCapture.copyPixelsFromBuffer(buffer); 2190 } catch (RuntimeException rex) { 2191 Log.e(LOGTAG, "Load capture has mismatched sizes; buffer: " 2192 + buffer.capacity() + " blob: " + blob.length 2193 + "capture: " + mCapture.getByteCount()); 2194 throw rex; 2195 } 2196 } 2197 } 2198 2199 @Override 2200 public String toString() { 2201 StringBuilder builder = new StringBuilder(100); 2202 builder.append(mId); 2203 builder.append(") has parent: "); 2204 if (getParent() != null) { 2205 builder.append("true["); 2206 builder.append(getParent().getId()); 2207 builder.append("]"); 2208 } else { 2209 builder.append("false"); 2210 } 2211 builder.append(", incog: "); 2212 builder.append(isPrivateBrowsingEnabled()); 2213 if (!isPrivateBrowsingEnabled()) { 2214 builder.append(", title: "); 2215 builder.append(getTitle()); 2216 builder.append(", url: "); 2217 builder.append(getUrl()); 2218 } 2219 return builder.toString(); 2220 } 2221 2222 private void handleProceededAfterSslError(SslError error) { 2223 if (error.getUrl().equals(mCurrentState.mUrl)) { 2224 // The security state should currently be SECURITY_STATE_SECURE. 2225 setSecurityState(SecurityState.SECURITY_STATE_BAD_CERTIFICATE); 2226 mCurrentState.mSslCertificateError = error; 2227 } else if (getSecurityState() == SecurityState.SECURITY_STATE_SECURE) { 2228 // The page's main resource is secure and this error is for a 2229 // sub-resource. 2230 setSecurityState(SecurityState.SECURITY_STATE_MIXED); 2231 } 2232 } 2233 } 2234