• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR))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 
getDefaultFavicon(Context context)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 
PageState(Context c, boolean incognito)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 
PageState(Context c, boolean incognito, String url, Bitmap favicon)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      */
revertVoiceSearchMode()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      */
isInVoiceSearchMode()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      */
voiceSearchSourceIsGoogle()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      */
getVoiceDisplayTitle()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      */
getVoiceSearchResults()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      */
activateVoiceSearchMode(Intent intent)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 {
VoiceSearchData(ArrayList<String> results, ArrayList<String> urls, ArrayList<String> htmls, ArrayList<String> baseUrls)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;
ErrorDialog(int title, String desc, int error)490         ErrorDialog(int title, String desc, int error) {
491             mTitle = title;
492             mDescription = desc;
493             mError = error;
494         }
495     }
496 
processNextError()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 
queueError(int err, String desc)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 
showError(ErrorDialog errDialog)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 
syncCurrentState(WebView view, String url)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.
setDeviceAccountLogin(DeviceAccountLogin login)935     void setDeviceAccountLogin(DeviceAccountLogin login) {
936         mDeviceAccountLogin = login;
937     }
938 
939     // Returns non-null if the title bar should display the auto-login UI.
getDeviceAccountLogin()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 
SubWindowClient(WebViewClient client, WebViewController controller)1306         SubWindowClient(WebViewClient client, WebViewController controller) {
1307             mClient = client;
1308             mController = controller;
1309         }
1310         @Override
onPageStarted(WebView view, String url, Bitmap favicon)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
doUpdateVisitedHistory(WebView view, String url, boolean isReload)1318         public void doUpdateVisitedHistory(WebView view, String url,
1319                 boolean isReload) {
1320             mClient.doUpdateVisitedHistory(view, url, isReload);
1321         }
1322         @Override
shouldOverrideUrlLoading(WebView view, String url)1323         public boolean shouldOverrideUrlLoading(WebView view, String url) {
1324             return mClient.shouldOverrideUrlLoading(view, url);
1325         }
1326         @Override
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)1327         public void onReceivedSslError(WebView view, SslErrorHandler handler,
1328                 SslError error) {
1329             mClient.onReceivedSslError(view, handler, error);
1330         }
1331         @Override
onReceivedClientCertRequest(WebView view, ClientCertRequestHandler handler, String host_and_port)1332         public void onReceivedClientCertRequest(WebView view,
1333                 ClientCertRequestHandler handler, String host_and_port) {
1334             mClient.onReceivedClientCertRequest(view, handler, host_and_port);
1335         }
1336         @Override
onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)1337         public void onReceivedHttpAuthRequest(WebView view,
1338                 HttpAuthHandler handler, String host, String realm) {
1339             mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
1340         }
1341         @Override
onFormResubmission(WebView view, Message dontResend, Message resend)1342         public void onFormResubmission(WebView view, Message dontResend,
1343                 Message resend) {
1344             mClient.onFormResubmission(view, dontResend, resend);
1345         }
1346         @Override
onReceivedError(WebView view, int errorCode, String description, String failingUrl)1347         public void onReceivedError(WebView view, int errorCode,
1348                 String description, String failingUrl) {
1349             mClient.onReceivedError(view, errorCode, description, failingUrl);
1350         }
1351         @Override
shouldOverrideKeyEvent(WebView view, android.view.KeyEvent event)1352         public boolean shouldOverrideKeyEvent(WebView view,
1353                 android.view.KeyEvent event) {
1354             return mClient.shouldOverrideKeyEvent(view, event);
1355         }
1356         @Override
onUnhandledKeyEvent(WebView view, android.view.KeyEvent event)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 
SubWindowChromeClient(WebChromeClient client)1371         SubWindowChromeClient(WebChromeClient client) {
1372             mClient = client;
1373         }
1374         @Override
onProgressChanged(WebView view, int newProgress)1375         public void onProgressChanged(WebView view, int newProgress) {
1376             mClient.onProgressChanged(view, newProgress);
1377         }
1378         @Override
onCreateWindow(WebView view, boolean dialog, boolean userGesture, android.os.Message resultMsg)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
onCloseWindow(WebView window)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
Tab(WebViewController wvcontroller, WebView w)1395     Tab(WebViewController wvcontroller, WebView w) {
1396         this(wvcontroller, w, null);
1397     }
1398 
Tab(WebViewController wvcontroller, Bundle state)1399     Tab(WebViewController wvcontroller, Bundle state) {
1400         this(wvcontroller, null, state);
1401     }
1402 
Tab(WebViewController wvcontroller, WebView w, Bundle state)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      */
refreshIdAfterPreload()1479     public void refreshIdAfterPreload() {
1480         mId = TabControl.getNextId();
1481     }
1482 
updateShouldCaptureThumbnails()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 
setController(WebViewController ctl)1503     public void setController(WebViewController ctl) {
1504         mWebViewController = ctl;
1505         updateShouldCaptureThumbnails();
1506     }
1507 
getId()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      */
setWebView(WebView w)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      */
destroy()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      */
removeFromTree()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      */
createSubWindow()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      */
dismissSubWindow()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      */
setParent(Tab parent)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      */
getParent()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      */
addChildTab(Tab child)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 
getChildren()1695     Vector<Tab> getChildren() {
1696         return mChildren;
1697     }
1698 
resume()1699     void resume() {
1700         if (mMainView != null) {
1701             setupHwAcceleration(mMainView);
1702             mMainView.onResume();
1703             if (mSubView != null) {
1704                 mSubView.onResume();
1705             }
1706         }
1707     }
1708 
setupHwAcceleration(View web)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 
pause()1719     void pause() {
1720         if (mMainView != null) {
1721             mMainView.onPause();
1722             if (mSubView != null) {
1723                 mSubView.onPause();
1724             }
1725         }
1726     }
1727 
putInForeground()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 
putInBackground()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 
inForeground()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      */
getTopWindow()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      */
getWebView()1781     WebView getWebView() {
1782         return mMainView;
1783     }
1784 
setViewContainer(View container)1785     void setViewContainer(View container) {
1786         mContainer = container;
1787     }
1788 
getViewContainer()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      */
isPrivateBrowsingEnabled()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      */
getSubWebView()1806     WebView getSubWebView() {
1807         return mSubView;
1808     }
1809 
setSubWebView(WebView subView)1810     void setSubWebView(WebView subView) {
1811         mSubView = subView;
1812     }
1813 
getSubViewContainer()1814     View getSubViewContainer() {
1815         return mSubViewContainer;
1816     }
1817 
setSubViewContainer(View subViewContainer)1818     void setSubViewContainer(View subViewContainer) {
1819         mSubViewContainer = subViewContainer;
1820     }
1821 
1822     /**
1823      * @return The geolocation permissions prompt for this tab.
1824      */
getGeolocationPermissionsPrompt()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      */
getAppId()1838     String getAppId() {
1839         return mAppId;
1840     }
1841 
1842     /**
1843      * Set the application id string
1844      * @param id
1845      */
setAppId(String id)1846     void setAppId(String id) {
1847         mAppId = id;
1848     }
1849 
closeOnBack()1850     boolean closeOnBack() {
1851         return mCloseOnBack;
1852     }
1853 
setCloseOnBack(boolean close)1854     void setCloseOnBack(boolean close) {
1855         mCloseOnBack = close;
1856     }
1857 
getUrl()1858     String getUrl() {
1859         return UrlUtils.filteredUrl(mCurrentState.mUrl);
1860     }
1861 
getOriginalUrl()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      */
getTitle()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      */
getFavicon()1882     Bitmap getFavicon() {
1883         if (mCurrentState.mFavicon != null) {
1884             return mCurrentState.mFavicon;
1885         }
1886         return getDefaultFavicon(mContext);
1887     }
1888 
isBookmarkedSite()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      */
getErrorConsole(boolean createIfNecessary)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      */
setSecurityState(SecurityState securityState)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      */
getSecurityState()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      */
getSslCertificateError()1931     SslError getSslCertificateError() {
1932         return mCurrentState.mSslCertificateError;
1933     }
1934 
getLoadProgress()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      */
inPageLoad()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.
clearInPageLoad()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      */
saveState()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      */
restoreState(Bundle b)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 
updateBookmarkedStatus()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 
getScreenshot()2038     public Bitmap getScreenshot() {
2039         synchronized (Tab.this) {
2040             return mCapture;
2041         }
2042     }
2043 
isSnapshot()2044     public boolean isSnapshot() {
2045         return false;
2046     }
2047 
createSnapshotValues()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 
compressBitmap(Bitmap bitmap)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 
loadUrl(String url, Map<String, String> headers)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 
capture()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
onNewPicture(WebView view, Picture picture)2132     public void onNewPicture(WebView view, Picture picture) {
2133         //update screenshot
2134         postCapture();
2135     }
2136 
postCapture()2137     private void postCapture() {
2138         if (!mHandler.hasMessages(MSG_CAPTURE)) {
2139             mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY);
2140         }
2141     }
2142 
canGoBack()2143     public boolean canGoBack() {
2144         return mMainView != null ? mMainView.canGoBack() : false;
2145     }
2146 
canGoForward()2147     public boolean canGoForward() {
2148         return mMainView != null ? mMainView.canGoForward() : false;
2149     }
2150 
goBack()2151     public void goBack() {
2152         if (mMainView != null) {
2153             mMainView.goBack();
2154         }
2155     }
2156 
goForward()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      */
clearBackStackWhenItemAdded(Pattern urlPattern)2170     public void clearBackStackWhenItemAdded(Pattern urlPattern) {
2171         mClearHistoryUrlPattern = urlPattern;
2172     }
2173 
persistThumbnail()2174     protected void persistThumbnail() {
2175         BackgroundHandler.execute(mSaveThumbnail);
2176     }
2177 
deleteThumbnail()2178     protected void deleteThumbnail() {
2179         BackgroundHandler.execute(mDeleteThumbnail);
2180     }
2181 
updateCaptureFromBlob(byte[] blob)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 
getCaptureBlob()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
toString()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 
handleProceededAfterSslError(SslError error)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