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