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