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