Home | History | Annotate | Download | only in browser
      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