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