OSDN Git Service

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