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