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