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