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