OSDN Git Service

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