OSDN Git Service

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