OSDN Git Service

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