Tab.java revision 4dce3bf588b7eaf4de555a93b67f5ddf9bc1ba77
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 java.io.File;
20import java.util.ArrayList;
21import java.util.LinkedList;
22import java.util.Vector;
23
24import android.app.AlertDialog;
25import android.app.SearchManager;
26import android.content.ContentResolver;
27import android.content.ContentValues;
28import android.content.DialogInterface;
29import android.content.DialogInterface.OnCancelListener;
30import android.content.Intent;
31import android.database.Cursor;
32import android.database.sqlite.SQLiteDatabase;
33import android.database.sqlite.SQLiteException;
34import android.graphics.Bitmap;
35import android.net.Uri;
36import android.net.http.SslError;
37import android.os.AsyncTask;
38import android.os.Bundle;
39import android.os.Message;
40import android.os.SystemClock;
41import android.provider.Browser;
42import android.speech.RecognizerResultsIntent;
43import android.util.Log;
44import android.view.KeyEvent;
45import android.view.LayoutInflater;
46import android.view.View;
47import android.view.ViewGroup;
48import android.view.View.OnClickListener;
49import android.webkit.CookieSyncManager;
50import android.webkit.GeolocationPermissions;
51import android.webkit.HttpAuthHandler;
52import android.webkit.SslErrorHandler;
53import android.webkit.URLUtil;
54import android.webkit.ValueCallback;
55import android.webkit.WebBackForwardList;
56import android.webkit.WebChromeClient;
57import android.webkit.WebHistoryItem;
58import android.webkit.WebIconDatabase;
59import android.webkit.WebStorage;
60import android.webkit.WebView;
61import android.webkit.WebViewClient;
62import android.widget.FrameLayout;
63import android.widget.ImageButton;
64import android.widget.LinearLayout;
65import android.widget.TextView;
66
67/**
68 * Class for maintaining Tabs with a main WebView and a subwindow.
69 */
70class Tab {
71    // Log Tag
72    private static final String LOGTAG = "Tab";
73    // The Geolocation permissions prompt
74    private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt;
75    // Main WebView wrapper
76    private View mContainer;
77    // Main WebView
78    private WebView mMainView;
79    // Subwindow container
80    private View mSubViewContainer;
81    // Subwindow WebView
82    private WebView mSubView;
83    // Saved bundle for when we are running low on memory. It contains the
84    // information needed to restore the WebView if the user goes back to the
85    // tab.
86    private Bundle mSavedState;
87    // Data used when displaying the tab in the picker.
88    private PickerData mPickerData;
89    // Parent Tab. This is the Tab that created this Tab, or null if the Tab was
90    // created by the UI
91    private Tab mParentTab;
92    // Tab that constructed by this Tab. This is used when this Tab is
93    // destroyed, it clears all mParentTab values in the children.
94    private Vector<Tab> mChildTabs;
95    // If true, the tab will be removed when back out of the first page.
96    private boolean mCloseOnExit;
97    // If true, the tab is in the foreground of the current activity.
98    private boolean mInForeground;
99    // If true, the tab is in loading state.
100    private boolean mInLoad;
101    // The time the load started, used to find load page time
102    private long mLoadStartTime;
103    // Application identifier used to find tabs that another application wants
104    // to reuse.
105    private String mAppId;
106    // Keep the original url around to avoid killing the old WebView if the url
107    // has not changed.
108    private String mOriginalUrl;
109    // Error console for the tab
110    private ErrorConsoleView mErrorConsole;
111    // the lock icon type and previous lock icon type for the tab
112    private int mLockIconType;
113    private int mPrevLockIconType;
114    // Inflation service for making subwindows.
115    private final LayoutInflater mInflateService;
116    // The BrowserActivity which owners the Tab
117    private final BrowserActivity mActivity;
118
119    // AsyncTask for downloading touch icons
120    DownloadTouchIcon mTouchIconLoader;
121
122    // Extra saved information for displaying the tab in the picker.
123    private static class PickerData {
124        String  mUrl;
125        String  mTitle;
126        Bitmap  mFavicon;
127    }
128
129    // Used for saving and restoring each Tab
130    static final String WEBVIEW = "webview";
131    static final String NUMTABS = "numTabs";
132    static final String CURRTAB = "currentTab";
133    static final String CURRURL = "currentUrl";
134    static final String CURRTITLE = "currentTitle";
135    static final String CURRPICTURE = "currentPicture";
136    static final String CLOSEONEXIT = "closeonexit";
137    static final String PARENTTAB = "parentTab";
138    static final String APPID = "appid";
139    static final String ORIGINALURL = "originalUrl";
140
141    // -------------------------------------------------------------------------
142
143    /**
144     * Private information regarding the latest voice search.  If the Tab is not
145     * in voice search mode, this will be null.
146     */
147    private VoiceSearchData mVoiceSearchData;
148    /**
149     * Return whether the tab is in voice search mode.
150     */
151    public boolean isInVoiceSearchMode() {
152        return mVoiceSearchData != null;
153    }
154    /**
155     * Get the title to display for the current voice search page.  If the Tab
156     * is not in voice search mode, return null.
157     */
158    public String getVoiceDisplayTitle() {
159        if (mVoiceSearchData == null) return null;
160        return mVoiceSearchData.mLastVoiceSearchTitle;
161    }
162    /**
163     * Get the latest array of voice search results, to be passed to the
164     * BrowserProvider.  If the Tab is not in voice search mode, return null.
165     */
166    public ArrayList<String> getVoiceSearchResults() {
167        if (mVoiceSearchData == null) return null;
168        return mVoiceSearchData.mVoiceSearchResults;
169    }
170    /**
171     * Activate voice search mode.
172     * @param intent Intent which has the results to use, or an index into the
173     *      results when reusing the old results.
174     */
175    /* package */ void activateVoiceSearchMode(Intent intent) {
176        ArrayList<String> results = intent.getStringArrayListExtra(
177                    RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS);
178        ArrayList<String> urls = intent.getStringArrayListExtra(
179                    RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS);
180        if (results != null) {
181            // This tab is now entering voice search mode for the first time, or
182            // a new voice search was done.
183            if (urls == null || results.size() != urls.size()) {
184                throw new AssertionError("improper extras passed in Intent");
185            }
186            mVoiceSearchData = new VoiceSearchData(results, urls);
187        } else {
188            String extraData = intent.getStringExtra(
189                    SearchManager.EXTRA_DATA_KEY);
190            if (extraData != null) {
191                mVoiceSearchData.mLastVoiceSearchIndex
192                        = Integer.parseInt(extraData);
193                if (mVoiceSearchData.mLastVoiceSearchIndex
194                        >= mVoiceSearchData.mVoiceSearchResults.size()) {
195                    throw new AssertionError("index must be less than "
196                            + " size of mVoiceSearchResults");
197                }
198            }
199        }
200        mVoiceSearchData.mLastVoiceSearchTitle
201                = mVoiceSearchData.mVoiceSearchResults.get(mVoiceSearchData.
202                mLastVoiceSearchIndex);
203        if (mInForeground) {
204            mActivity.showVoiceTitleBar(mVoiceSearchData.mLastVoiceSearchTitle);
205        }
206        mVoiceSearchData.mLastVoiceSearchUrl
207                = mVoiceSearchData.mVoiceSearchUrls.get(mVoiceSearchData.
208                mLastVoiceSearchIndex);
209        mMainView.loadUrl(mVoiceSearchData.mLastVoiceSearchUrl);
210    }
211    /* package */ static class VoiceSearchData {
212        public VoiceSearchData(ArrayList<String> results,
213                ArrayList<String> urls) {
214            mVoiceSearchResults = results;
215            mVoiceSearchUrls = urls;
216            mLastVoiceSearchIndex = 0;
217        }
218        /*
219         * ArrayList of suggestions to be displayed when opening the
220         * SearchManager
221         */
222        public ArrayList<String> mVoiceSearchResults;
223        /*
224         * ArrayList of urls, associated with the suggestions in
225         * mVoiceSearchResults.
226         */
227        public ArrayList<String> mVoiceSearchUrls;
228        /*
229         * The last url provided by voice search.  Used for comparison to see if
230         * we are going to a page by some method besides voice search.  Only
231         * meaningful in voice search mode.
232         */
233        public String mLastVoiceSearchUrl;
234        /**
235         * The last title used for voice search.  Needed to update the title bar
236         * when switching tabs.
237         */
238        public String mLastVoiceSearchTitle;
239        /*
240         * The index into mVoiceSearchResults and mVoiceSearchUrls of the last
241         * voice search performed. Stored so it can be used to index into
242         * mVoiceSearchUrls to determine the url in getUrlDataFromIntent.
243         */
244        public int mLastVoiceSearchIndex;
245    }
246
247    // Container class for the next error dialog that needs to be displayed
248    private class ErrorDialog {
249        public final int mTitle;
250        public final String mDescription;
251        public final int mError;
252        ErrorDialog(int title, String desc, int error) {
253            mTitle = title;
254            mDescription = desc;
255            mError = error;
256        }
257    };
258
259    private void processNextError() {
260        if (mQueuedErrors == null) {
261            return;
262        }
263        // The first one is currently displayed so just remove it.
264        mQueuedErrors.removeFirst();
265        if (mQueuedErrors.size() == 0) {
266            mQueuedErrors = null;
267            return;
268        }
269        showError(mQueuedErrors.getFirst());
270    }
271
272    private DialogInterface.OnDismissListener mDialogListener =
273            new DialogInterface.OnDismissListener() {
274                public void onDismiss(DialogInterface d) {
275                    processNextError();
276                }
277            };
278    private LinkedList<ErrorDialog> mQueuedErrors;
279
280    private void queueError(int err, String desc) {
281        if (mQueuedErrors == null) {
282            mQueuedErrors = new LinkedList<ErrorDialog>();
283        }
284        for (ErrorDialog d : mQueuedErrors) {
285            if (d.mError == err) {
286                // Already saw a similar error, ignore the new one.
287                return;
288            }
289        }
290        ErrorDialog errDialog = new ErrorDialog(
291                err == WebViewClient.ERROR_FILE_NOT_FOUND ?
292                R.string.browserFrameFileErrorLabel :
293                R.string.browserFrameNetworkErrorLabel,
294                desc, err);
295        mQueuedErrors.addLast(errDialog);
296
297        // Show the dialog now if the queue was empty and it is in foreground
298        if (mQueuedErrors.size() == 1 && mInForeground) {
299            showError(errDialog);
300        }
301    }
302
303    private void showError(ErrorDialog errDialog) {
304        if (mInForeground) {
305            AlertDialog d = new AlertDialog.Builder(mActivity)
306                    .setTitle(errDialog.mTitle)
307                    .setMessage(errDialog.mDescription)
308                    .setPositiveButton(R.string.ok, null)
309                    .create();
310            d.setOnDismissListener(mDialogListener);
311            d.show();
312        }
313    }
314
315    // -------------------------------------------------------------------------
316    // WebViewClient implementation for the main WebView
317    // -------------------------------------------------------------------------
318
319    private final WebViewClient mWebViewClient = new WebViewClient() {
320        @Override
321        public void onPageStarted(WebView view, String url, Bitmap favicon) {
322            mInLoad = true;
323            mLoadStartTime = SystemClock.uptimeMillis();
324            if (mVoiceSearchData != null
325                    && !url.equals(mVoiceSearchData.mLastVoiceSearchUrl)) {
326                mVoiceSearchData = null;
327                if (mInForeground) {
328                    mActivity.revertVoiceTitleBar();
329                }
330            }
331
332            // We've started to load a new page. If there was a pending message
333            // to save a screenshot then we will now take the new page and save
334            // an incorrect screenshot. Therefore, remove any pending thumbnail
335            // messages from the queue.
336            mActivity.removeMessages(BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL,
337                    view);
338
339            // If we start a touch icon load and then load a new page, we don't
340            // want to cancel the current touch icon loader. But, we do want to
341            // create a new one when the touch icon url is known.
342            if (mTouchIconLoader != null) {
343                mTouchIconLoader.mTab = null;
344                mTouchIconLoader = null;
345            }
346
347            // reset the error console
348            if (mErrorConsole != null) {
349                mErrorConsole.clearErrorMessages();
350                if (mActivity.shouldShowErrorConsole()) {
351                    mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
352                }
353            }
354
355            // update the bookmark database for favicon
356            if (favicon != null) {
357                BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity
358                        .getContentResolver(), view.getOriginalUrl(), view
359                        .getUrl(), favicon);
360            }
361
362            // reset sync timer to avoid sync starts during loading a page
363            CookieSyncManager.getInstance().resetSync();
364
365            if (!mActivity.isNetworkUp()) {
366                view.setNetworkAvailable(false);
367            }
368
369            // finally update the UI in the activity if it is in the foreground
370            if (mInForeground) {
371                mActivity.onPageStarted(view, url, favicon);
372            }
373        }
374
375        @Override
376        public void onPageFinished(WebView view, String url) {
377            LogTag.logPageFinishedLoading(
378                    url, SystemClock.uptimeMillis() - mLoadStartTime);
379            mInLoad = false;
380
381            if (mInForeground && !mActivity.didUserStopLoading()
382                    || !mInForeground) {
383                // Only update the bookmark screenshot if the user did not
384                // cancel the load early.
385                mActivity.postMessage(
386                        BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, 0, 0, view,
387                        500);
388            }
389
390            // finally update the UI in the activity if it is in the foreground
391            if (mInForeground) {
392                mActivity.onPageFinished(view, url);
393            }
394        }
395
396        // return true if want to hijack the url to let another app to handle it
397        @Override
398        public boolean shouldOverrideUrlLoading(WebView view, String url) {
399            if (mInForeground) {
400                return mActivity.shouldOverrideUrlLoading(view, url);
401            } else {
402                return false;
403            }
404        }
405
406        /**
407         * Updates the lock icon. This method is called when we discover another
408         * resource to be loaded for this page (for example, javascript). While
409         * we update the icon type, we do not update the lock icon itself until
410         * we are done loading, it is slightly more secure this way.
411         */
412        @Override
413        public void onLoadResource(WebView view, String url) {
414            if (url != null && url.length() > 0) {
415                // It is only if the page claims to be secure that we may have
416                // to update the lock:
417                if (mLockIconType == BrowserActivity.LOCK_ICON_SECURE) {
418                    // If NOT a 'safe' url, change the lock to mixed content!
419                    if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url)
420                            || URLUtil.isAboutUrl(url))) {
421                        mLockIconType = BrowserActivity.LOCK_ICON_MIXED;
422                    }
423                }
424            }
425        }
426
427        /**
428         * Show a dialog informing the user of the network error reported by
429         * WebCore if it is in the foreground.
430         */
431        @Override
432        public void onReceivedError(WebView view, int errorCode,
433                String description, String failingUrl) {
434            if (errorCode != WebViewClient.ERROR_HOST_LOOKUP &&
435                    errorCode != WebViewClient.ERROR_CONNECT &&
436                    errorCode != WebViewClient.ERROR_BAD_URL &&
437                    errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME &&
438                    errorCode != WebViewClient.ERROR_FILE) {
439                queueError(errorCode, description);
440            }
441            Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl
442                    + " " + description);
443
444            // We need to reset the title after an error if it is in foreground.
445            if (mInForeground) {
446                mActivity.resetTitleAndRevertLockIcon();
447            }
448        }
449
450        /**
451         * Check with the user if it is ok to resend POST data as the page they
452         * are trying to navigate to is the result of a POST.
453         */
454        @Override
455        public void onFormResubmission(WebView view, final Message dontResend,
456                                       final Message resend) {
457            if (!mInForeground) {
458                dontResend.sendToTarget();
459                return;
460            }
461            new AlertDialog.Builder(mActivity).setTitle(
462                    R.string.browserFrameFormResubmitLabel).setMessage(
463                    R.string.browserFrameFormResubmitMessage)
464                    .setPositiveButton(R.string.ok,
465                            new DialogInterface.OnClickListener() {
466                                public void onClick(DialogInterface dialog,
467                                        int which) {
468                                    resend.sendToTarget();
469                                }
470                            }).setNegativeButton(R.string.cancel,
471                            new DialogInterface.OnClickListener() {
472                                public void onClick(DialogInterface dialog,
473                                        int which) {
474                                    dontResend.sendToTarget();
475                                }
476                            }).setOnCancelListener(new OnCancelListener() {
477                        public void onCancel(DialogInterface dialog) {
478                            dontResend.sendToTarget();
479                        }
480                    }).show();
481        }
482
483        /**
484         * Insert the url into the visited history database.
485         * @param url The url to be inserted.
486         * @param isReload True if this url is being reloaded.
487         * FIXME: Not sure what to do when reloading the page.
488         */
489        @Override
490        public void doUpdateVisitedHistory(WebView view, String url,
491                boolean isReload) {
492            if (url.regionMatches(true, 0, "about:", 0, 6)) {
493                return;
494            }
495            // remove "client" before updating it to the history so that it wont
496            // show up in the auto-complete list.
497            int index = url.indexOf("client=ms-");
498            if (index > 0 && url.contains(".google.")) {
499                int end = url.indexOf('&', index);
500                if (end > 0) {
501                    url = url.substring(0, index)
502                            .concat(url.substring(end + 1));
503                } else {
504                    // the url.charAt(index-1) should be either '?' or '&'
505                    url = url.substring(0, index-1);
506                }
507            }
508            Browser.updateVisitedHistory(mActivity.getContentResolver(), url,
509                    true);
510            WebIconDatabase.getInstance().retainIconForPageUrl(url);
511        }
512
513        /**
514         * Displays SSL error(s) dialog to the user.
515         */
516        @Override
517        public void onReceivedSslError(final WebView view,
518                final SslErrorHandler handler, final SslError error) {
519            if (!mInForeground) {
520                handler.cancel();
521                return;
522            }
523            if (BrowserSettings.getInstance().showSecurityWarnings()) {
524                final LayoutInflater factory =
525                    LayoutInflater.from(mActivity);
526                final View warningsView =
527                    factory.inflate(R.layout.ssl_warnings, null);
528                final LinearLayout placeholder =
529                    (LinearLayout)warningsView.findViewById(R.id.placeholder);
530
531                if (error.hasError(SslError.SSL_UNTRUSTED)) {
532                    LinearLayout ll = (LinearLayout)factory
533                        .inflate(R.layout.ssl_warning, null);
534                    ((TextView)ll.findViewById(R.id.warning))
535                        .setText(R.string.ssl_untrusted);
536                    placeholder.addView(ll);
537                }
538
539                if (error.hasError(SslError.SSL_IDMISMATCH)) {
540                    LinearLayout ll = (LinearLayout)factory
541                        .inflate(R.layout.ssl_warning, null);
542                    ((TextView)ll.findViewById(R.id.warning))
543                        .setText(R.string.ssl_mismatch);
544                    placeholder.addView(ll);
545                }
546
547                if (error.hasError(SslError.SSL_EXPIRED)) {
548                    LinearLayout ll = (LinearLayout)factory
549                        .inflate(R.layout.ssl_warning, null);
550                    ((TextView)ll.findViewById(R.id.warning))
551                        .setText(R.string.ssl_expired);
552                    placeholder.addView(ll);
553                }
554
555                if (error.hasError(SslError.SSL_NOTYETVALID)) {
556                    LinearLayout ll = (LinearLayout)factory
557                        .inflate(R.layout.ssl_warning, null);
558                    ((TextView)ll.findViewById(R.id.warning))
559                        .setText(R.string.ssl_not_yet_valid);
560                    placeholder.addView(ll);
561                }
562
563                new AlertDialog.Builder(mActivity).setTitle(
564                        R.string.security_warning).setIcon(
565                        android.R.drawable.ic_dialog_alert).setView(
566                        warningsView).setPositiveButton(R.string.ssl_continue,
567                        new DialogInterface.OnClickListener() {
568                            public void onClick(DialogInterface dialog,
569                                    int whichButton) {
570                                handler.proceed();
571                            }
572                        }).setNeutralButton(R.string.view_certificate,
573                        new DialogInterface.OnClickListener() {
574                            public void onClick(DialogInterface dialog,
575                                    int whichButton) {
576                                mActivity.showSSLCertificateOnError(view,
577                                        handler, error);
578                            }
579                        }).setNegativeButton(R.string.cancel,
580                        new DialogInterface.OnClickListener() {
581                            public void onClick(DialogInterface dialog,
582                                    int whichButton) {
583                                handler.cancel();
584                                mActivity.resetTitleAndRevertLockIcon();
585                            }
586                        }).setOnCancelListener(
587                        new DialogInterface.OnCancelListener() {
588                            public void onCancel(DialogInterface dialog) {
589                                handler.cancel();
590                                mActivity.resetTitleAndRevertLockIcon();
591                            }
592                        }).show();
593            } else {
594                handler.proceed();
595            }
596        }
597
598        /**
599         * Handles an HTTP authentication request.
600         *
601         * @param handler The authentication handler
602         * @param host The host
603         * @param realm The realm
604         */
605        @Override
606        public void onReceivedHttpAuthRequest(WebView view,
607                final HttpAuthHandler handler, final String host,
608                final String realm) {
609            String username = null;
610            String password = null;
611
612            boolean reuseHttpAuthUsernamePassword = handler
613                    .useHttpAuthUsernamePassword();
614
615            if (reuseHttpAuthUsernamePassword && mMainView != null) {
616                String[] credentials = mMainView.getHttpAuthUsernamePassword(
617                        host, realm);
618                if (credentials != null && credentials.length == 2) {
619                    username = credentials[0];
620                    password = credentials[1];
621                }
622            }
623
624            if (username != null && password != null) {
625                handler.proceed(username, password);
626            } else {
627                if (mInForeground) {
628                    mActivity.showHttpAuthentication(handler, host, realm,
629                            null, null, null, 0);
630                } else {
631                    handler.cancel();
632                }
633            }
634        }
635
636        @Override
637        public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
638            if (!mInForeground) {
639                return false;
640            }
641            if (mActivity.isMenuDown()) {
642                // only check shortcut key when MENU is held
643                return mActivity.getWindow().isShortcutKey(event.getKeyCode(),
644                        event);
645            } else {
646                return false;
647            }
648        }
649
650        @Override
651        public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
652            if (!mInForeground) {
653                return;
654            }
655            if (event.isDown()) {
656                mActivity.onKeyDown(event.getKeyCode(), event);
657            } else {
658                mActivity.onKeyUp(event.getKeyCode(), event);
659            }
660        }
661    };
662
663    // -------------------------------------------------------------------------
664    // WebChromeClient implementation for the main WebView
665    // -------------------------------------------------------------------------
666
667    private final WebChromeClient mWebChromeClient = new WebChromeClient() {
668        // Helper method to create a new tab or sub window.
669        private void createWindow(final boolean dialog, final Message msg) {
670            WebView.WebViewTransport transport =
671                    (WebView.WebViewTransport) msg.obj;
672            if (dialog) {
673                createSubWindow();
674                mActivity.attachSubWindow(Tab.this);
675                transport.setWebView(mSubView);
676            } else {
677                final Tab newTab = mActivity.openTabAndShow(
678                        BrowserActivity.EMPTY_URL_DATA, false, null);
679                if (newTab != Tab.this) {
680                    Tab.this.addChildTab(newTab);
681                }
682                transport.setWebView(newTab.getWebView());
683            }
684            msg.sendToTarget();
685        }
686
687        @Override
688        public boolean onCreateWindow(WebView view, final boolean dialog,
689                final boolean userGesture, final Message resultMsg) {
690            // only allow new window or sub window for the foreground case
691            if (!mInForeground) {
692                return false;
693            }
694            // Short-circuit if we can't create any more tabs or sub windows.
695            if (dialog && mSubView != null) {
696                new AlertDialog.Builder(mActivity)
697                        .setTitle(R.string.too_many_subwindows_dialog_title)
698                        .setIcon(android.R.drawable.ic_dialog_alert)
699                        .setMessage(R.string.too_many_subwindows_dialog_message)
700                        .setPositiveButton(R.string.ok, null)
701                        .show();
702                return false;
703            } else if (!mActivity.getTabControl().canCreateNewTab()) {
704                new AlertDialog.Builder(mActivity)
705                        .setTitle(R.string.too_many_windows_dialog_title)
706                        .setIcon(android.R.drawable.ic_dialog_alert)
707                        .setMessage(R.string.too_many_windows_dialog_message)
708                        .setPositiveButton(R.string.ok, null)
709                        .show();
710                return false;
711            }
712
713            // Short-circuit if this was a user gesture.
714            if (userGesture) {
715                createWindow(dialog, resultMsg);
716                return true;
717            }
718
719            // Allow the popup and create the appropriate window.
720            final AlertDialog.OnClickListener allowListener =
721                    new AlertDialog.OnClickListener() {
722                        public void onClick(DialogInterface d,
723                                int which) {
724                            createWindow(dialog, resultMsg);
725                        }
726                    };
727
728            // Block the popup by returning a null WebView.
729            final AlertDialog.OnClickListener blockListener =
730                    new AlertDialog.OnClickListener() {
731                        public void onClick(DialogInterface d, int which) {
732                            resultMsg.sendToTarget();
733                        }
734                    };
735
736            // Build a confirmation dialog to display to the user.
737            final AlertDialog d =
738                    new AlertDialog.Builder(mActivity)
739                    .setTitle(R.string.attention)
740                    .setIcon(android.R.drawable.ic_dialog_alert)
741                    .setMessage(R.string.popup_window_attempt)
742                    .setPositiveButton(R.string.allow, allowListener)
743                    .setNegativeButton(R.string.block, blockListener)
744                    .setCancelable(false)
745                    .create();
746
747            // Show the confirmation dialog.
748            d.show();
749            return true;
750        }
751
752        @Override
753        public void onRequestFocus(WebView view) {
754            if (!mInForeground) {
755                mActivity.switchToTab(mActivity.getTabControl().getTabIndex(
756                        Tab.this));
757            }
758        }
759
760        @Override
761        public void onCloseWindow(WebView window) {
762            if (mParentTab != null) {
763                // JavaScript can only close popup window.
764                if (mInForeground) {
765                    mActivity.switchToTab(mActivity.getTabControl()
766                            .getTabIndex(mParentTab));
767                }
768                mActivity.closeTab(Tab.this);
769            }
770        }
771
772        @Override
773        public void onProgressChanged(WebView view, int newProgress) {
774            if (newProgress == 100) {
775                // sync cookies and cache promptly here.
776                CookieSyncManager.getInstance().sync();
777            }
778            if (mInForeground) {
779                mActivity.onProgressChanged(view, newProgress);
780            }
781        }
782
783        @Override
784        public void onReceivedTitle(WebView view, String title) {
785            String url = view.getUrl();
786            if (mInForeground) {
787                // here, if url is null, we want to reset the title
788                mActivity.setUrlTitle(url, title);
789            }
790            if (url == null ||
791                url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
792                return;
793            }
794            // See if we can find the current url in our history database and
795            // add the new title to it.
796            if (url.startsWith("http://www.")) {
797                url = url.substring(11);
798            } else if (url.startsWith("http://")) {
799                url = url.substring(4);
800            }
801            try {
802                final ContentResolver cr = mActivity.getContentResolver();
803                url = "%" + url;
804                String [] selArgs = new String[] { url };
805                String where = Browser.BookmarkColumns.URL + " LIKE ? AND "
806                        + Browser.BookmarkColumns.BOOKMARK + " = 0";
807                Cursor c = cr.query(Browser.BOOKMARKS_URI,
808                        Browser.HISTORY_PROJECTION, where, selArgs, null);
809                if (c.moveToFirst()) {
810                    // Current implementation of database only has one entry per
811                    // url.
812                    ContentValues map = new ContentValues();
813                    map.put(Browser.BookmarkColumns.TITLE, title);
814                    cr.update(Browser.BOOKMARKS_URI, map, "_id = "
815                            + c.getInt(0), null);
816                }
817                c.close();
818            } catch (IllegalStateException e) {
819                Log.e(LOGTAG, "Tab onReceived title", e);
820            } catch (SQLiteException ex) {
821                Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex);
822            }
823        }
824
825        @Override
826        public void onReceivedIcon(WebView view, Bitmap icon) {
827            if (icon != null) {
828                BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity
829                        .getContentResolver(), view.getOriginalUrl(), view
830                        .getUrl(), icon);
831            }
832            if (mInForeground) {
833                mActivity.setFavicon(icon);
834            }
835        }
836
837        @Override
838        public void onReceivedTouchIconUrl(WebView view, String url,
839                boolean precomposed) {
840            final ContentResolver cr = mActivity.getContentResolver();
841            final Cursor c = BrowserBookmarksAdapter.queryBookmarksForUrl(cr,
842                            view.getOriginalUrl(), view.getUrl(), true);
843            if (c != null) {
844                if (c.getCount() > 0) {
845                    // Let precomposed icons take precedence over non-composed
846                    // icons.
847                    if (precomposed && mTouchIconLoader != null) {
848                        mTouchIconLoader.cancel(false);
849                        mTouchIconLoader = null;
850                    }
851                    // Have only one async task at a time.
852                    if (mTouchIconLoader == null) {
853                        mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr,
854                                c, view);
855                        mTouchIconLoader.execute(url);
856                    }
857                } else {
858                    c.close();
859                }
860            }
861        }
862
863        @Override
864        public void onShowCustomView(View view,
865                WebChromeClient.CustomViewCallback callback) {
866            if (mInForeground) mActivity.onShowCustomView(view, callback);
867        }
868
869        @Override
870        public void onHideCustomView() {
871            if (mInForeground) mActivity.onHideCustomView();
872        }
873
874        /**
875         * The origin has exceeded its database quota.
876         * @param url the URL that exceeded the quota
877         * @param databaseIdentifier the identifier of the database on which the
878         *            transaction that caused the quota overflow was run
879         * @param currentQuota the current quota for the origin.
880         * @param estimatedSize the estimated size of the database.
881         * @param totalUsedQuota is the sum of all origins' quota.
882         * @param quotaUpdater The callback to run when a decision to allow or
883         *            deny quota has been made. Don't forget to call this!
884         */
885        @Override
886        public void onExceededDatabaseQuota(String url,
887            String databaseIdentifier, long currentQuota, long estimatedSize,
888            long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
889            BrowserSettings.getInstance().getWebStorageSizeManager()
890                    .onExceededDatabaseQuota(url, databaseIdentifier,
891                            currentQuota, estimatedSize, totalUsedQuota,
892                            quotaUpdater);
893        }
894
895        /**
896         * The Application Cache has exceeded its max size.
897         * @param spaceNeeded is the amount of disk space that would be needed
898         *            in order for the last appcache operation to succeed.
899         * @param totalUsedQuota is the sum of all origins' quota.
900         * @param quotaUpdater A callback to inform the WebCore thread that a
901         *            new app cache size is available. This callback must always
902         *            be executed at some point to ensure that the sleeping
903         *            WebCore thread is woken up.
904         */
905        @Override
906        public void onReachedMaxAppCacheSize(long spaceNeeded,
907                long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
908            BrowserSettings.getInstance().getWebStorageSizeManager()
909                    .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota,
910                            quotaUpdater);
911        }
912
913        /**
914         * Instructs the browser to show a prompt to ask the user to set the
915         * Geolocation permission state for the specified origin.
916         * @param origin The origin for which Geolocation permissions are
917         *     requested.
918         * @param callback The callback to call once the user has set the
919         *     Geolocation permission state.
920         */
921        @Override
922        public void onGeolocationPermissionsShowPrompt(String origin,
923                GeolocationPermissions.Callback callback) {
924            if (mInForeground) {
925                mGeolocationPermissionsPrompt.show(origin, callback);
926            }
927        }
928
929        /**
930         * Instructs the browser to hide the Geolocation permissions prompt.
931         */
932        @Override
933        public void onGeolocationPermissionsHidePrompt() {
934            if (mInForeground) {
935                mGeolocationPermissionsPrompt.hide();
936            }
937        }
938
939        /* Adds a JavaScript error message to the system log and if the JS
940         * console is enabled in the about:debug options, to that console
941         * also.
942         * @param message The error message to report.
943         * @param lineNumber The line number of the error.
944         * @param sourceID The name of the source file that caused the error.
945         */
946        @Override
947        public void onConsoleMessage(String message, int lineNumber,
948                String sourceID) {
949            if (mInForeground) {
950                // call getErrorConsole(true) so it will create one if needed
951                ErrorConsoleView errorConsole = getErrorConsole(true);
952                errorConsole.addErrorMessage(message, sourceID, lineNumber);
953                if (mActivity.shouldShowErrorConsole()
954                        && errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) {
955                    errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
956                }
957            }
958            Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":"
959                    + lineNumber);
960        }
961
962        /**
963         * Ask the browser for an icon to represent a <video> element.
964         * This icon will be used if the Web page did not specify a poster attribute.
965         * @return Bitmap The icon or null if no such icon is available.
966         */
967        @Override
968        public Bitmap getDefaultVideoPoster() {
969            if (mInForeground) {
970                return mActivity.getDefaultVideoPoster();
971            }
972            return null;
973        }
974
975        /**
976         * Ask the host application for a custom progress view to show while
977         * a <video> is loading.
978         * @return View The progress view.
979         */
980        @Override
981        public View getVideoLoadingProgressView() {
982            if (mInForeground) {
983                return mActivity.getVideoLoadingProgressView();
984            }
985            return null;
986        }
987
988        @Override
989        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
990            if (mInForeground) {
991                mActivity.openFileChooser(uploadMsg);
992            } else {
993                uploadMsg.onReceiveValue(null);
994            }
995        }
996
997        /**
998         * Deliver a list of already-visited URLs
999         */
1000        @Override
1001        public void getVisitedHistory(final ValueCallback<String[]> callback) {
1002            AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() {
1003                public String[] doInBackground(Void... unused) {
1004                    return Browser.getVisitedHistory(mActivity
1005                            .getContentResolver());
1006                }
1007                public void onPostExecute(String[] result) {
1008                    callback.onReceiveValue(result);
1009                };
1010            };
1011            task.execute();
1012        };
1013    };
1014
1015    // -------------------------------------------------------------------------
1016    // WebViewClient implementation for the sub window
1017    // -------------------------------------------------------------------------
1018
1019    // Subclass of WebViewClient used in subwindows to notify the main
1020    // WebViewClient of certain WebView activities.
1021    private static class SubWindowClient extends WebViewClient {
1022        // The main WebViewClient.
1023        private final WebViewClient mClient;
1024
1025        SubWindowClient(WebViewClient client) {
1026            mClient = client;
1027        }
1028        @Override
1029        public void doUpdateVisitedHistory(WebView view, String url,
1030                boolean isReload) {
1031            mClient.doUpdateVisitedHistory(view, url, isReload);
1032        }
1033        @Override
1034        public boolean shouldOverrideUrlLoading(WebView view, String url) {
1035            return mClient.shouldOverrideUrlLoading(view, url);
1036        }
1037        @Override
1038        public void onReceivedSslError(WebView view, SslErrorHandler handler,
1039                SslError error) {
1040            mClient.onReceivedSslError(view, handler, error);
1041        }
1042        @Override
1043        public void onReceivedHttpAuthRequest(WebView view,
1044                HttpAuthHandler handler, String host, String realm) {
1045            mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
1046        }
1047        @Override
1048        public void onFormResubmission(WebView view, Message dontResend,
1049                Message resend) {
1050            mClient.onFormResubmission(view, dontResend, resend);
1051        }
1052        @Override
1053        public void onReceivedError(WebView view, int errorCode,
1054                String description, String failingUrl) {
1055            mClient.onReceivedError(view, errorCode, description, failingUrl);
1056        }
1057        @Override
1058        public boolean shouldOverrideKeyEvent(WebView view,
1059                android.view.KeyEvent event) {
1060            return mClient.shouldOverrideKeyEvent(view, event);
1061        }
1062        @Override
1063        public void onUnhandledKeyEvent(WebView view,
1064                android.view.KeyEvent event) {
1065            mClient.onUnhandledKeyEvent(view, event);
1066        }
1067    }
1068
1069    // -------------------------------------------------------------------------
1070    // WebChromeClient implementation for the sub window
1071    // -------------------------------------------------------------------------
1072
1073    private class SubWindowChromeClient extends WebChromeClient {
1074        // The main WebChromeClient.
1075        private final WebChromeClient mClient;
1076
1077        SubWindowChromeClient(WebChromeClient client) {
1078            mClient = client;
1079        }
1080        @Override
1081        public void onProgressChanged(WebView view, int newProgress) {
1082            mClient.onProgressChanged(view, newProgress);
1083        }
1084        @Override
1085        public boolean onCreateWindow(WebView view, boolean dialog,
1086                boolean userGesture, android.os.Message resultMsg) {
1087            return mClient.onCreateWindow(view, dialog, userGesture, resultMsg);
1088        }
1089        @Override
1090        public void onCloseWindow(WebView window) {
1091            if (window != mSubView) {
1092                Log.e(LOGTAG, "Can't close the window");
1093            }
1094            mActivity.dismissSubWindow(Tab.this);
1095        }
1096    }
1097
1098    // -------------------------------------------------------------------------
1099
1100    // Construct a new tab
1101    Tab(BrowserActivity activity, WebView w, boolean closeOnExit, String appId,
1102            String url) {
1103        mActivity = activity;
1104        mCloseOnExit = closeOnExit;
1105        mAppId = appId;
1106        mOriginalUrl = url;
1107        mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
1108        mPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
1109        mInLoad = false;
1110        mInForeground = false;
1111
1112        mInflateService = LayoutInflater.from(activity);
1113
1114        // The tab consists of a container view, which contains the main
1115        // WebView, as well as any other UI elements associated with the tab.
1116        mContainer = mInflateService.inflate(R.layout.tab, null);
1117
1118        mGeolocationPermissionsPrompt =
1119            (GeolocationPermissionsPrompt) mContainer.findViewById(
1120                R.id.geolocation_permissions_prompt);
1121
1122        setWebView(w);
1123    }
1124
1125    /**
1126     * Sets the WebView for this tab, correctly removing the old WebView from
1127     * the container view.
1128     */
1129    void setWebView(WebView w) {
1130        if (mMainView == w) {
1131            return;
1132        }
1133        // If the WebView is changing, the page will be reloaded, so any ongoing
1134        // Geolocation permission requests are void.
1135        mGeolocationPermissionsPrompt.hide();
1136
1137        // Just remove the old one.
1138        FrameLayout wrapper =
1139                (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
1140        wrapper.removeView(mMainView);
1141
1142        // set the new one
1143        mMainView = w;
1144        // attached the WebViewClient and WebChromeClient
1145        if (mMainView != null) {
1146            mMainView.setWebViewClient(mWebViewClient);
1147            mMainView.setWebChromeClient(mWebChromeClient);
1148        }
1149    }
1150
1151    /**
1152     * Destroy the tab's main WebView and subWindow if any
1153     */
1154    void destroy() {
1155        if (mMainView != null) {
1156            dismissSubWindow();
1157            BrowserSettings.getInstance().deleteObserver(mMainView.getSettings());
1158            // save the WebView to call destroy() after detach it from the tab
1159            WebView webView = mMainView;
1160            setWebView(null);
1161            webView.destroy();
1162        }
1163    }
1164
1165    /**
1166     * Remove the tab from the parent
1167     */
1168    void removeFromTree() {
1169        // detach the children
1170        if (mChildTabs != null) {
1171            for(Tab t : mChildTabs) {
1172                t.setParentTab(null);
1173            }
1174        }
1175        // remove itself from the parent list
1176        if (mParentTab != null) {
1177            mParentTab.mChildTabs.remove(this);
1178        }
1179    }
1180
1181    /**
1182     * Create a new subwindow unless a subwindow already exists.
1183     * @return True if a new subwindow was created. False if one already exists.
1184     */
1185    boolean createSubWindow() {
1186        if (mSubView == null) {
1187            mSubViewContainer = mInflateService.inflate(
1188                    R.layout.browser_subwindow, null);
1189            mSubView = (WebView) mSubViewContainer.findViewById(R.id.webview);
1190            // use trackball directly
1191            mSubView.setMapTrackballToArrowKeys(false);
1192            mSubView.setWebViewClient(new SubWindowClient(mWebViewClient));
1193            mSubView.setWebChromeClient(new SubWindowChromeClient(
1194                    mWebChromeClient));
1195            mSubView.setDownloadListener(mActivity);
1196            mSubView.setOnCreateContextMenuListener(mActivity);
1197            final BrowserSettings s = BrowserSettings.getInstance();
1198            s.addObserver(mSubView.getSettings()).update(s, null);
1199            final ImageButton cancel = (ImageButton) mSubViewContainer
1200                    .findViewById(R.id.subwindow_close);
1201            cancel.setOnClickListener(new OnClickListener() {
1202                public void onClick(View v) {
1203                    mSubView.getWebChromeClient().onCloseWindow(mSubView);
1204                }
1205            });
1206            return true;
1207        }
1208        return false;
1209    }
1210
1211    /**
1212     * Dismiss the subWindow for the tab.
1213     */
1214    void dismissSubWindow() {
1215        if (mSubView != null) {
1216            BrowserSettings.getInstance().deleteObserver(
1217                    mSubView.getSettings());
1218            mSubView.destroy();
1219            mSubView = null;
1220            mSubViewContainer = null;
1221        }
1222    }
1223
1224    /**
1225     * Attach the sub window to the content view.
1226     */
1227    void attachSubWindow(ViewGroup content) {
1228        if (mSubView != null) {
1229            content.addView(mSubViewContainer,
1230                    BrowserActivity.COVER_SCREEN_PARAMS);
1231        }
1232    }
1233
1234    /**
1235     * Remove the sub window from the content view.
1236     */
1237    void removeSubWindow(ViewGroup content) {
1238        if (mSubView != null) {
1239            content.removeView(mSubViewContainer);
1240        }
1241    }
1242
1243    /**
1244     * This method attaches both the WebView and any sub window to the
1245     * given content view.
1246     */
1247    void attachTabToContentView(ViewGroup content) {
1248        if (mMainView == null) {
1249            return;
1250        }
1251
1252        // Attach the WebView to the container and then attach the
1253        // container to the content view.
1254        FrameLayout wrapper =
1255                (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
1256        wrapper.addView(mMainView);
1257        content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS);
1258        attachSubWindow(content);
1259    }
1260
1261    /**
1262     * Remove the WebView and any sub window from the given content view.
1263     */
1264    void removeTabFromContentView(ViewGroup content) {
1265        if (mMainView == null) {
1266            return;
1267        }
1268
1269        // Remove the container from the content and then remove the
1270        // WebView from the container. This will trigger a focus change
1271        // needed by WebView.
1272        FrameLayout wrapper =
1273                (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
1274        wrapper.removeView(mMainView);
1275        content.removeView(mContainer);
1276        removeSubWindow(content);
1277    }
1278
1279    /**
1280     * Set the parent tab of this tab.
1281     */
1282    void setParentTab(Tab parent) {
1283        mParentTab = parent;
1284        // This tab may have been freed due to low memory. If that is the case,
1285        // the parent tab index is already saved. If we are changing that index
1286        // (most likely due to removing the parent tab) we must update the
1287        // parent tab index in the saved Bundle.
1288        if (mSavedState != null) {
1289            if (parent == null) {
1290                mSavedState.remove(PARENTTAB);
1291            } else {
1292                mSavedState.putInt(PARENTTAB, mActivity.getTabControl()
1293                        .getTabIndex(parent));
1294            }
1295        }
1296    }
1297
1298    /**
1299     * When a Tab is created through the content of another Tab, then we
1300     * associate the Tabs.
1301     * @param child the Tab that was created from this Tab
1302     */
1303    void addChildTab(Tab child) {
1304        if (mChildTabs == null) {
1305            mChildTabs = new Vector<Tab>();
1306        }
1307        mChildTabs.add(child);
1308        child.setParentTab(this);
1309    }
1310
1311    Vector<Tab> getChildTabs() {
1312        return mChildTabs;
1313    }
1314
1315    void resume() {
1316        if (mMainView != null) {
1317            mMainView.onResume();
1318            if (mSubView != null) {
1319                mSubView.onResume();
1320            }
1321        }
1322    }
1323
1324    void pause() {
1325        if (mMainView != null) {
1326            mMainView.onPause();
1327            if (mSubView != null) {
1328                mSubView.onPause();
1329            }
1330        }
1331    }
1332
1333    void putInForeground() {
1334        mInForeground = true;
1335        resume();
1336        mMainView.setOnCreateContextMenuListener(mActivity);
1337        if (mSubView != null) {
1338            mSubView.setOnCreateContextMenuListener(mActivity);
1339        }
1340        // Show the pending error dialog if the queue is not empty
1341        if (mQueuedErrors != null && mQueuedErrors.size() >  0) {
1342            showError(mQueuedErrors.getFirst());
1343        }
1344    }
1345
1346    void putInBackground() {
1347        mInForeground = false;
1348        pause();
1349        mMainView.setOnCreateContextMenuListener(null);
1350        if (mSubView != null) {
1351            mSubView.setOnCreateContextMenuListener(null);
1352        }
1353    }
1354
1355    /**
1356     * Return the top window of this tab; either the subwindow if it is not
1357     * null or the main window.
1358     * @return The top window of this tab.
1359     */
1360    WebView getTopWindow() {
1361        if (mSubView != null) {
1362            return mSubView;
1363        }
1364        return mMainView;
1365    }
1366
1367    /**
1368     * Return the main window of this tab. Note: if a tab is freed in the
1369     * background, this can return null. It is only guaranteed to be
1370     * non-null for the current tab.
1371     * @return The main WebView of this tab.
1372     */
1373    WebView getWebView() {
1374        return mMainView;
1375    }
1376
1377    /**
1378     * Return the subwindow of this tab or null if there is no subwindow.
1379     * @return The subwindow of this tab or null.
1380     */
1381    WebView getSubWebView() {
1382        return mSubView;
1383    }
1384
1385    /**
1386     * @return The geolocation permissions prompt for this tab.
1387     */
1388    GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() {
1389        return mGeolocationPermissionsPrompt;
1390    }
1391
1392    /**
1393     * @return The application id string
1394     */
1395    String getAppId() {
1396        return mAppId;
1397    }
1398
1399    /**
1400     * Set the application id string
1401     * @param id
1402     */
1403    void setAppId(String id) {
1404        mAppId = id;
1405    }
1406
1407    /**
1408     * @return The original url associated with this Tab
1409     */
1410    String getOriginalUrl() {
1411        return mOriginalUrl;
1412    }
1413
1414    /**
1415     * Set the original url associated with this tab
1416     */
1417    void setOriginalUrl(String url) {
1418        mOriginalUrl = url;
1419    }
1420
1421    /**
1422     * Get the url of this tab. Valid after calling populatePickerData, but
1423     * before calling wipePickerData, or if the webview has been destroyed.
1424     * @return The WebView's url or null.
1425     */
1426    String getUrl() {
1427        if (mPickerData != null) {
1428            return mPickerData.mUrl;
1429        }
1430        return null;
1431    }
1432
1433    /**
1434     * Get the title of this tab. Valid after calling populatePickerData, but
1435     * before calling wipePickerData, or if the webview has been destroyed. If
1436     * the url has no title, use the url instead.
1437     * @return The WebView's title (or url) or null.
1438     */
1439    String getTitle() {
1440        if (mPickerData != null) {
1441            return mPickerData.mTitle;
1442        }
1443        return null;
1444    }
1445
1446    /**
1447     * Get the favicon of this tab. Valid after calling populatePickerData, but
1448     * before calling wipePickerData, or if the webview has been destroyed.
1449     * @return The WebView's favicon or null.
1450     */
1451    Bitmap getFavicon() {
1452        if (mPickerData != null) {
1453            return mPickerData.mFavicon;
1454        }
1455        return null;
1456    }
1457
1458    /**
1459     * Return the tab's error console. Creates the console if createIfNEcessary
1460     * is true and we haven't already created the console.
1461     * @param createIfNecessary Flag to indicate if the console should be
1462     *            created if it has not been already.
1463     * @return The tab's error console, or null if one has not been created and
1464     *         createIfNecessary is false.
1465     */
1466    ErrorConsoleView getErrorConsole(boolean createIfNecessary) {
1467        if (createIfNecessary && mErrorConsole == null) {
1468            mErrorConsole = new ErrorConsoleView(mActivity);
1469            mErrorConsole.setWebView(mMainView);
1470        }
1471        return mErrorConsole;
1472    }
1473
1474    /**
1475     * If this Tab was created through another Tab, then this method returns
1476     * that Tab.
1477     * @return the Tab parent or null
1478     */
1479    public Tab getParentTab() {
1480        return mParentTab;
1481    }
1482
1483    /**
1484     * Return whether this tab should be closed when it is backing out of the
1485     * first page.
1486     * @return TRUE if this tab should be closed when exit.
1487     */
1488    boolean closeOnExit() {
1489        return mCloseOnExit;
1490    }
1491
1492    /**
1493     * Saves the current lock-icon state before resetting the lock icon. If we
1494     * have an error, we may need to roll back to the previous state.
1495     */
1496    void resetLockIcon(String url) {
1497        mPrevLockIconType = mLockIconType;
1498        mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
1499        if (URLUtil.isHttpsUrl(url)) {
1500            mLockIconType = BrowserActivity.LOCK_ICON_SECURE;
1501        }
1502    }
1503
1504    /**
1505     * Reverts the lock-icon state to the last saved state, for example, if we
1506     * had an error, and need to cancel the load.
1507     */
1508    void revertLockIcon() {
1509        mLockIconType = mPrevLockIconType;
1510    }
1511
1512    /**
1513     * @return The tab's lock icon type.
1514     */
1515    int getLockIconType() {
1516        return mLockIconType;
1517    }
1518
1519    /**
1520     * @return TRUE if onPageStarted is called while onPageFinished is not
1521     *         called yet.
1522     */
1523    boolean inLoad() {
1524        return mInLoad;
1525    }
1526
1527    // force mInLoad to be false. This should only be called before closing the
1528    // tab to ensure BrowserActivity's pauseWebViewTimers() is called correctly.
1529    void clearInLoad() {
1530        mInLoad = false;
1531    }
1532
1533    void populatePickerData() {
1534        if (mMainView == null) {
1535            populatePickerDataFromSavedState();
1536            return;
1537        }
1538
1539        // FIXME: The only place we cared about subwindow was for
1540        // bookmarking (i.e. not when saving state). Was this deliberate?
1541        final WebBackForwardList list = mMainView.copyBackForwardList();
1542        final WebHistoryItem item = list != null ? list.getCurrentItem() : null;
1543        populatePickerData(item);
1544    }
1545
1546    // Populate the picker data using the given history item and the current top
1547    // WebView.
1548    private void populatePickerData(WebHistoryItem item) {
1549        mPickerData = new PickerData();
1550        if (item != null) {
1551            mPickerData.mUrl = item.getUrl();
1552            mPickerData.mTitle = item.getTitle();
1553            mPickerData.mFavicon = item.getFavicon();
1554            if (mPickerData.mTitle == null) {
1555                mPickerData.mTitle = mPickerData.mUrl;
1556            }
1557        }
1558    }
1559
1560    // Create the PickerData and populate it using the saved state of the tab.
1561    void populatePickerDataFromSavedState() {
1562        if (mSavedState == null) {
1563            return;
1564        }
1565        mPickerData = new PickerData();
1566        mPickerData.mUrl = mSavedState.getString(CURRURL);
1567        mPickerData.mTitle = mSavedState.getString(CURRTITLE);
1568    }
1569
1570    void clearPickerData() {
1571        mPickerData = null;
1572    }
1573
1574    /**
1575     * Get the saved state bundle.
1576     * @return
1577     */
1578    Bundle getSavedState() {
1579        return mSavedState;
1580    }
1581
1582    /**
1583     * Set the saved state.
1584     */
1585    void setSavedState(Bundle state) {
1586        mSavedState = state;
1587    }
1588
1589    /**
1590     * @return TRUE if succeed in saving the state.
1591     */
1592    boolean saveState() {
1593        // If the WebView is null it means we ran low on memory and we already
1594        // stored the saved state in mSavedState.
1595        if (mMainView == null) {
1596            return mSavedState != null;
1597        }
1598
1599        mSavedState = new Bundle();
1600        final WebBackForwardList list = mMainView.saveState(mSavedState);
1601        if (list != null) {
1602            final File f = new File(mActivity.getTabControl().getThumbnailDir(),
1603                    mMainView.hashCode() + "_pic.save");
1604            if (mMainView.savePicture(mSavedState, f)) {
1605                mSavedState.putString(CURRPICTURE, f.getPath());
1606            }
1607        }
1608
1609        // Store some extra info for displaying the tab in the picker.
1610        final WebHistoryItem item = list != null ? list.getCurrentItem() : null;
1611        populatePickerData(item);
1612
1613        if (mPickerData.mUrl != null) {
1614            mSavedState.putString(CURRURL, mPickerData.mUrl);
1615        }
1616        if (mPickerData.mTitle != null) {
1617            mSavedState.putString(CURRTITLE, mPickerData.mTitle);
1618        }
1619        mSavedState.putBoolean(CLOSEONEXIT, mCloseOnExit);
1620        if (mAppId != null) {
1621            mSavedState.putString(APPID, mAppId);
1622        }
1623        if (mOriginalUrl != null) {
1624            mSavedState.putString(ORIGINALURL, mOriginalUrl);
1625        }
1626        // Remember the parent tab so the relationship can be restored.
1627        if (mParentTab != null) {
1628            mSavedState.putInt(PARENTTAB, mActivity.getTabControl().getTabIndex(
1629                    mParentTab));
1630        }
1631        return true;
1632    }
1633
1634    /*
1635     * Restore the state of the tab.
1636     */
1637    boolean restoreState(Bundle b) {
1638        if (b == null) {
1639            return false;
1640        }
1641        // Restore the internal state even if the WebView fails to restore.
1642        // This will maintain the app id, original url and close-on-exit values.
1643        mSavedState = null;
1644        mPickerData = null;
1645        mCloseOnExit = b.getBoolean(CLOSEONEXIT);
1646        mAppId = b.getString(APPID);
1647        mOriginalUrl = b.getString(ORIGINALURL);
1648
1649        final WebBackForwardList list = mMainView.restoreState(b);
1650        if (list == null) {
1651            return false;
1652        }
1653        if (b.containsKey(CURRPICTURE)) {
1654            final File f = new File(b.getString(CURRPICTURE));
1655            mMainView.restorePicture(b, f);
1656            f.delete();
1657        }
1658        return true;
1659    }
1660}
1661