blob: bde2fa7390584fbd653184361c0e7ca693c18eac [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.browser;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Vector;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Browser;
import android.speech.RecognizerResultsIntent;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.View.OnClickListener;
import android.webkit.ConsoleMessage;
import android.webkit.CookieSyncManager;
import android.webkit.DownloadListener;
import android.webkit.GeolocationPermissions;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.URLUtil;
import android.webkit.ValueCallback;
import android.webkit.WebBackForwardList;
import android.webkit.WebBackForwardListClient;
import android.webkit.WebChromeClient;
import android.webkit.WebHistoryItem;
import android.webkit.WebIconDatabase;
import android.webkit.WebStorage;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.common.speech.LoggingEvents;
/**
* Class for maintaining Tabs with a main WebView and a subwindow.
*/
class Tab {
// Log Tag
private static final String LOGTAG = "Tab";
// Special case the logtag for messages for the Console to make it easier to
// filter them and match the logtag used for these messages in older versions
// of the browser.
private static final String CONSOLE_LOGTAG = "browser";
// The Geolocation permissions prompt
private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt;
// Main WebView wrapper
private LinearLayout mContainer;
// Main WebView
private WebView mMainView;
// Subwindow container
private View mSubViewContainer;
// Subwindow WebView
private WebView mSubView;
// Saved bundle for when we are running low on memory. It contains the
// information needed to restore the WebView if the user goes back to the
// tab.
private Bundle mSavedState;
// Data used when displaying the tab in the picker.
private PickerData mPickerData;
// Parent Tab. This is the Tab that created this Tab, or null if the Tab was
// created by the UI
private Tab mParentTab;
// Tab that constructed by this Tab. This is used when this Tab is
// destroyed, it clears all mParentTab values in the children.
private Vector<Tab> mChildTabs;
// If true, the tab will be removed when back out of the first page.
private boolean mCloseOnExit;
// If true, the tab is in the foreground of the current activity.
private boolean mInForeground;
// If true, the tab is in loading state.
private boolean mInLoad;
// The time the load started, used to find load page time
private long mLoadStartTime;
// Application identifier used to find tabs that another application wants
// to reuse.
private String mAppId;
// Keep the original url around to avoid killing the old WebView if the url
// has not changed.
private String mOriginalUrl;
// Error console for the tab
private ErrorConsoleView mErrorConsole;
// the lock icon type and previous lock icon type for the tab
private int mLockIconType;
private int mPrevLockIconType;
// Inflation service for making subwindows.
private final LayoutInflater mInflateService;
// The BrowserActivity which owners the Tab
private final BrowserActivity mActivity;
// The listener that gets invoked when a download is started from the
// mMainView
private final DownloadListener mDownloadListener;
// Listener used to know when we move forward or back in the history list.
private final WebBackForwardListClient mWebBackForwardListClient;
// AsyncTask for downloading touch icons
DownloadTouchIcon mTouchIconLoader;
// Extra saved information for displaying the tab in the picker.
private static class PickerData {
String mUrl;
String mTitle;
Bitmap mFavicon;
}
// Used for saving and restoring each Tab
static final String WEBVIEW = "webview";
static final String NUMTABS = "numTabs";
static final String CURRTAB = "currentTab";
static final String CURRURL = "currentUrl";
static final String CURRTITLE = "currentTitle";
static final String CLOSEONEXIT = "closeonexit";
static final String PARENTTAB = "parentTab";
static final String APPID = "appid";
static final String ORIGINALURL = "originalUrl";
// -------------------------------------------------------------------------
/**
* Private information regarding the latest voice search. If the Tab is not
* in voice search mode, this will be null.
*/
private VoiceSearchData mVoiceSearchData;
/**
* Remove voice search mode from this tab.
*/
public void revertVoiceSearchMode() {
if (mVoiceSearchData != null) {
mVoiceSearchData = null;
if (mInForeground) {
mActivity.revertVoiceTitleBar();
}
}
}
/**
* Return whether the tab is in voice search mode.
*/
public boolean isInVoiceSearchMode() {
return mVoiceSearchData != null;
}
/**
* Return true if the Tab is in voice search mode and the voice search
* Intent came with a String identifying that Google provided the Intent.
*/
public boolean voiceSearchSourceIsGoogle() {
return mVoiceSearchData != null && mVoiceSearchData.mSourceIsGoogle;
}
/**
* Get the title to display for the current voice search page. If the Tab
* is not in voice search mode, return null.
*/
public String getVoiceDisplayTitle() {
if (mVoiceSearchData == null) return null;
return mVoiceSearchData.mLastVoiceSearchTitle;
}
/**
* Get the latest array of voice search results, to be passed to the
* BrowserProvider. If the Tab is not in voice search mode, return null.
*/
public ArrayList<String> getVoiceSearchResults() {
if (mVoiceSearchData == null) return null;
return mVoiceSearchData.mVoiceSearchResults;
}
/**
* Activate voice search mode.
* @param intent Intent which has the results to use, or an index into the
* results when reusing the old results.
*/
/* package */ void activateVoiceSearchMode(Intent intent) {
int index = 0;
ArrayList<String> results = intent.getStringArrayListExtra(
RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS);
if (results != null) {
ArrayList<String> urls = intent.getStringArrayListExtra(
RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS);
ArrayList<String> htmls = intent.getStringArrayListExtra(
RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_HTML);
ArrayList<String> baseUrls = intent.getStringArrayListExtra(
RecognizerResultsIntent
.EXTRA_VOICE_SEARCH_RESULT_HTML_BASE_URLS);
// This tab is now entering voice search mode for the first time, or
// a new voice search was done.
int size = results.size();
if (urls == null || size != urls.size()) {
throw new AssertionError("improper extras passed in Intent");
}
if (htmls == null || htmls.size() != size || baseUrls == null ||
(baseUrls.size() != size && baseUrls.size() != 1)) {
// If either of these arrays are empty/incorrectly sized, ignore
// them.
htmls = null;
baseUrls = null;
}
mVoiceSearchData = new VoiceSearchData(results, urls, htmls,
baseUrls);
mVoiceSearchData.mHeaders = intent.getParcelableArrayListExtra(
RecognizerResultsIntent
.EXTRA_VOICE_SEARCH_RESULT_HTTP_HEADERS);
mVoiceSearchData.mSourceIsGoogle = intent.getBooleanExtra(
VoiceSearchData.SOURCE_IS_GOOGLE, false);
mVoiceSearchData.mVoiceSearchIntent = new Intent(intent);
}
String extraData = intent.getStringExtra(
SearchManager.EXTRA_DATA_KEY);
if (extraData != null) {
index = Integer.parseInt(extraData);
if (index >= mVoiceSearchData.mVoiceSearchResults.size()) {
throw new AssertionError("index must be less than "
+ "size of mVoiceSearchResults");
}
if (mVoiceSearchData.mSourceIsGoogle) {
Intent logIntent = new Intent(
LoggingEvents.ACTION_LOG_EVENT);
logIntent.putExtra(LoggingEvents.EXTRA_EVENT,
LoggingEvents.VoiceSearch.N_BEST_CHOOSE);
logIntent.putExtra(
LoggingEvents.VoiceSearch.EXTRA_N_BEST_CHOOSE_INDEX,
index);
mActivity.sendBroadcast(logIntent);
}
if (mVoiceSearchData.mVoiceSearchIntent != null) {
// Copy the Intent, so that each history item will have its own
// Intent, with different (or none) extra data.
Intent latest = new Intent(mVoiceSearchData.mVoiceSearchIntent);
latest.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
mVoiceSearchData.mVoiceSearchIntent = latest;
}
}
mVoiceSearchData.mLastVoiceSearchTitle
= mVoiceSearchData.mVoiceSearchResults.get(index);
if (mInForeground) {
mActivity.showVoiceTitleBar(mVoiceSearchData.mLastVoiceSearchTitle);
}
if (mVoiceSearchData.mVoiceSearchHtmls != null) {
// When index was found it was already ensured that it was valid
String uriString = mVoiceSearchData.mVoiceSearchHtmls.get(index);
if (uriString != null) {
Uri dataUri = Uri.parse(uriString);
if (RecognizerResultsIntent.URI_SCHEME_INLINE.equals(
dataUri.getScheme())) {
// If there is only one base URL, use it. If there are
// more, there will be one for each index, so use the base
// URL corresponding to the index.
String baseUrl = mVoiceSearchData.mVoiceSearchBaseUrls.get(
mVoiceSearchData.mVoiceSearchBaseUrls.size() > 1 ?
index : 0);
mVoiceSearchData.mLastVoiceSearchUrl = baseUrl;
mMainView.loadDataWithBaseURL(baseUrl,
uriString.substring(RecognizerResultsIntent
.URI_SCHEME_INLINE.length() + 1), "text/html",
"utf-8", baseUrl);
return;
}
}
}
mVoiceSearchData.mLastVoiceSearchUrl
= mVoiceSearchData.mVoiceSearchUrls.get(index);
if (null == mVoiceSearchData.mLastVoiceSearchUrl) {
mVoiceSearchData.mLastVoiceSearchUrl = mActivity.smartUrlFilter(
mVoiceSearchData.mLastVoiceSearchTitle);
}
Map<String, String> headers = null;
if (mVoiceSearchData.mHeaders != null) {
int bundleIndex = mVoiceSearchData.mHeaders.size() == 1 ? 0
: index;
Bundle bundle = mVoiceSearchData.mHeaders.get(bundleIndex);
if (bundle != null && !bundle.isEmpty()) {
Iterator<String> iter = bundle.keySet().iterator();
headers = new HashMap<String, String>();
while (iter.hasNext()) {
String key = iter.next();
headers.put(key, bundle.getString(key));
}
}
}
mMainView.loadUrl(mVoiceSearchData.mLastVoiceSearchUrl, headers);
}
/* package */ static class VoiceSearchData {
public VoiceSearchData(ArrayList<String> results,
ArrayList<String> urls, ArrayList<String> htmls,
ArrayList<String> baseUrls) {
mVoiceSearchResults = results;
mVoiceSearchUrls = urls;
mVoiceSearchHtmls = htmls;
mVoiceSearchBaseUrls = baseUrls;
}
/*
* ArrayList of suggestions to be displayed when opening the
* SearchManager
*/
public ArrayList<String> mVoiceSearchResults;
/*
* ArrayList of urls, associated with the suggestions in
* mVoiceSearchResults.
*/
public ArrayList<String> mVoiceSearchUrls;
/*
* ArrayList holding content to load for each item in
* mVoiceSearchResults.
*/
public ArrayList<String> mVoiceSearchHtmls;
/*
* ArrayList holding base urls for the items in mVoiceSearchResults.
* If non null, this will either have the same size as
* mVoiceSearchResults or have a size of 1, in which case all will use
* the same base url
*/
public ArrayList<String> mVoiceSearchBaseUrls;
/*
* The last url provided by voice search. Used for comparison to see if
* we are going to a page by some method besides voice search.
*/
public String mLastVoiceSearchUrl;
/**
* The last title used for voice search. Needed to update the title bar
* when switching tabs.
*/
public String mLastVoiceSearchTitle;
/**
* Whether the Intent which turned on voice search mode contained the
* String signifying that Google was the source.
*/
public boolean mSourceIsGoogle;
/**
* List of headers to be passed into the WebView containing location
* information
*/
public ArrayList<Bundle> mHeaders;
/**
* The Intent used to invoke voice search. Placed on the
* WebHistoryItem so that when coming back to a previous voice search
* page we can again activate voice search.
*/
public Intent mVoiceSearchIntent;
/**
* String used to identify Google as the source of voice search.
*/
public static String SOURCE_IS_GOOGLE
= "android.speech.extras.SOURCE_IS_GOOGLE";
}
// Container class for the next error dialog that needs to be displayed
private class ErrorDialog {
public final int mTitle;
public final String mDescription;
public final int mError;
ErrorDialog(int title, String desc, int error) {
mTitle = title;
mDescription = desc;
mError = error;
}
};
private void processNextError() {
if (mQueuedErrors == null) {
return;
}
// The first one is currently displayed so just remove it.
mQueuedErrors.removeFirst();
if (mQueuedErrors.size() == 0) {
mQueuedErrors = null;
return;
}
showError(mQueuedErrors.getFirst());
}
private DialogInterface.OnDismissListener mDialogListener =
new DialogInterface.OnDismissListener() {
public void onDismiss(DialogInterface d) {
processNextError();
}
};
private LinkedList<ErrorDialog> mQueuedErrors;
private void queueError(int err, String desc) {
if (mQueuedErrors == null) {
mQueuedErrors = new LinkedList<ErrorDialog>();
}
for (ErrorDialog d : mQueuedErrors) {
if (d.mError == err) {
// Already saw a similar error, ignore the new one.
return;
}
}
ErrorDialog errDialog = new ErrorDialog(
err == WebViewClient.ERROR_FILE_NOT_FOUND ?
R.string.browserFrameFileErrorLabel :
R.string.browserFrameNetworkErrorLabel,
desc, err);
mQueuedErrors.addLast(errDialog);
// Show the dialog now if the queue was empty and it is in foreground
if (mQueuedErrors.size() == 1 && mInForeground) {
showError(errDialog);
}
}
private void showError(ErrorDialog errDialog) {
if (mInForeground) {
AlertDialog d = new AlertDialog.Builder(mActivity)
.setTitle(errDialog.mTitle)
.setMessage(errDialog.mDescription)
.setPositiveButton(R.string.ok, null)
.create();
d.setOnDismissListener(mDialogListener);
d.show();
}
}
// -------------------------------------------------------------------------
// WebViewClient implementation for the main WebView
// -------------------------------------------------------------------------
private final WebViewClient mWebViewClient = new WebViewClient() {
private Message mDontResend;
private Message mResend;
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
mInLoad = true;
mLoadStartTime = SystemClock.uptimeMillis();
if (mVoiceSearchData != null
&& !url.equals(mVoiceSearchData.mLastVoiceSearchUrl)) {
if (mVoiceSearchData.mSourceIsGoogle) {
Intent i = new Intent(LoggingEvents.ACTION_LOG_EVENT);
i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
mActivity.sendBroadcast(i);
}
revertVoiceSearchMode();
}
// We've started to load a new page. If there was a pending message
// to save a screenshot then we will now take the new page and save
// an incorrect screenshot. Therefore, remove any pending thumbnail
// messages from the queue.
mActivity.removeMessages(BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL,
view);
// If we start a touch icon load and then load a new page, we don't
// want to cancel the current touch icon loader. But, we do want to
// create a new one when the touch icon url is known.
if (mTouchIconLoader != null) {
mTouchIconLoader.mTab = null;
mTouchIconLoader = null;
}
// reset the error console
if (mErrorConsole != null) {
mErrorConsole.clearErrorMessages();
if (mActivity.shouldShowErrorConsole()) {
mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
}
}
// update the bookmark database for favicon
if (favicon != null) {
BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity
.getContentResolver(), null, url, favicon);
}
// reset sync timer to avoid sync starts during loading a page
CookieSyncManager.getInstance().resetSync();
if (!mActivity.isNetworkUp()) {
view.setNetworkAvailable(false);
}
// finally update the UI in the activity if it is in the foreground
if (mInForeground) {
mActivity.onPageStarted(view, url, favicon);
}
}
@Override
public void onPageFinished(WebView view, String url) {
LogTag.logPageFinishedLoading(
url, SystemClock.uptimeMillis() - mLoadStartTime);
mInLoad = false;
if (mInForeground && !mActivity.didUserStopLoading()
|| !mInForeground) {
// Only update the bookmark screenshot if the user did not
// cancel the load early.
mActivity.postMessage(
BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, 0, 0, view,
500);
}
// finally update the UI in the activity if it is in the foreground
if (mInForeground) {
mActivity.onPageFinished(view, url);
}
}
// return true if want to hijack the url to let another app to handle it
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (voiceSearchSourceIsGoogle()) {
// This method is called when the user clicks on a link.
// VoiceSearchMode is turned off when the user leaves the
// Google results page, so at this point the user must be on
// that page. If the user clicked a link on that page, assume
// that the voice search was effective, and broadcast an Intent
// so a receiver can take note of that fact.
Intent logIntent = new Intent(LoggingEvents.ACTION_LOG_EVENT);
logIntent.putExtra(LoggingEvents.EXTRA_EVENT,
LoggingEvents.VoiceSearch.RESULT_CLICKED);
mActivity.sendBroadcast(logIntent);
}
if (mInForeground) {
return mActivity.shouldOverrideUrlLoading(view, url);
} else {
return false;
}
}
/**
* Updates the lock icon. This method is called when we discover another
* resource to be loaded for this page (for example, javascript). While
* we update the icon type, we do not update the lock icon itself until
* we are done loading, it is slightly more secure this way.
*/
@Override
public void onLoadResource(WebView view, String url) {
if (url != null && url.length() > 0) {
// It is only if the page claims to be secure that we may have
// to update the lock:
if (mLockIconType == BrowserActivity.LOCK_ICON_SECURE) {
// If NOT a 'safe' url, change the lock to mixed content!
if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url)
|| URLUtil.isAboutUrl(url))) {
mLockIconType = BrowserActivity.LOCK_ICON_MIXED;
}
}
}
}
/**
* Show a dialog informing the user of the network error reported by
* WebCore if it is in the foreground.
*/
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
if (errorCode != WebViewClient.ERROR_HOST_LOOKUP &&
errorCode != WebViewClient.ERROR_CONNECT &&
errorCode != WebViewClient.ERROR_BAD_URL &&
errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME &&
errorCode != WebViewClient.ERROR_FILE) {
queueError(errorCode, description);
}
Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl
+ " " + description);
// We need to reset the title after an error if it is in foreground.
if (mInForeground) {
mActivity.resetTitleAndRevertLockIcon();
}
}
/**
* Check with the user if it is ok to resend POST data as the page they
* are trying to navigate to is the result of a POST.
*/
@Override
public void onFormResubmission(WebView view, final Message dontResend,
final Message resend) {
if (!mInForeground) {
dontResend.sendToTarget();
return;
}
if (mDontResend != null) {
Log.w(LOGTAG, "onFormResubmission should not be called again "
+ "while dialog is still up");
dontResend.sendToTarget();
return;
}
mDontResend = dontResend;
mResend = resend;
new AlertDialog.Builder(mActivity).setTitle(
R.string.browserFrameFormResubmitLabel).setMessage(
R.string.browserFrameFormResubmitMessage)
.setPositiveButton(R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
if (mResend != null) {
mResend.sendToTarget();
mResend = null;
mDontResend = null;
}
}
}).setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
if (mDontResend != null) {
mDontResend.sendToTarget();
mResend = null;
mDontResend = null;
}
}
}).setOnCancelListener(new OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (mDontResend != null) {
mDontResend.sendToTarget();
mResend = null;
mDontResend = null;
}
}
}).show();
}
/**
* Insert the url into the visited history database.
* @param url The url to be inserted.
* @param isReload True if this url is being reloaded.
* FIXME: Not sure what to do when reloading the page.
*/
@Override
public void doUpdateVisitedHistory(WebView view, String url,
boolean isReload) {
if (url.regionMatches(true, 0, "about:", 0, 6)) {
return;
}
// remove "client" before updating it to the history so that it wont
// show up in the auto-complete list.
int index = url.indexOf("client=ms-");
if (index > 0 && url.contains(".google.")) {
int end = url.indexOf('&', index);
if (end > 0) {
url = url.substring(0, index)
.concat(url.substring(end + 1));
} else {
// the url.charAt(index-1) should be either '?' or '&'
url = url.substring(0, index-1);
}
}
final ContentResolver cr = mActivity.getContentResolver();
final String newUrl = url;
new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... unused) {
Browser.updateVisitedHistory(cr, newUrl, true);
return null;
}
}.execute();
WebIconDatabase.getInstance().retainIconForPageUrl(url);
}
/**
* Displays SSL error(s) dialog to the user.
*/
@Override
public void onReceivedSslError(final WebView view,
final SslErrorHandler handler, final SslError error) {
if (!mInForeground) {
handler.cancel();
return;
}
if (BrowserSettings.getInstance().showSecurityWarnings()) {
final LayoutInflater factory =
LayoutInflater.from(mActivity);
final View warningsView =
factory.inflate(R.layout.ssl_warnings, null);
final LinearLayout placeholder =
(LinearLayout)warningsView.findViewById(R.id.placeholder);
if (error.hasError(SslError.SSL_UNTRUSTED)) {
LinearLayout ll = (LinearLayout)factory
.inflate(R.layout.ssl_warning, null);
((TextView)ll.findViewById(R.id.warning))
.setText(R.string.ssl_untrusted);
placeholder.addView(ll);
}
if (error.hasError(SslError.SSL_IDMISMATCH)) {
LinearLayout ll = (LinearLayout)factory
.inflate(R.layout.ssl_warning, null);
((TextView)ll.findViewById(R.id.warning))
.setText(R.string.ssl_mismatch);
placeholder.addView(ll);
}
if (error.hasError(SslError.SSL_EXPIRED)) {
LinearLayout ll = (LinearLayout)factory
.inflate(R.layout.ssl_warning, null);
((TextView)ll.findViewById(R.id.warning))
.setText(R.string.ssl_expired);
placeholder.addView(ll);
}
if (error.hasError(SslError.SSL_NOTYETVALID)) {
LinearLayout ll = (LinearLayout)factory
.inflate(R.layout.ssl_warning, null);
((TextView)ll.findViewById(R.id.warning))
.setText(R.string.ssl_not_yet_valid);
placeholder.addView(ll);
}
new AlertDialog.Builder(mActivity).setTitle(
R.string.security_warning).setIcon(
android.R.drawable.ic_dialog_alert).setView(
warningsView).setPositiveButton(R.string.ssl_continue,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
handler.proceed();
}
}).setNeutralButton(R.string.view_certificate,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
mActivity.showSSLCertificateOnError(view,
handler, error);
}
}).setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
handler.cancel();
mActivity.resetTitleAndRevertLockIcon();
}
}).setOnCancelListener(
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
handler.cancel();
mActivity.resetTitleAndRevertLockIcon();
}
}).show();
} else {
handler.proceed();
}
}
/**
* Handles an HTTP authentication request.
*
* @param handler The authentication handler
* @param host The host
* @param realm The realm
*/
@Override
public void onReceivedHttpAuthRequest(WebView view,
final HttpAuthHandler handler, final String host,
final String realm) {
String username = null;
String password = null;
boolean reuseHttpAuthUsernamePassword = handler
.useHttpAuthUsernamePassword();
if (reuseHttpAuthUsernamePassword && view != null) {
String[] credentials = view.getHttpAuthUsernamePassword(
host, realm);
if (credentials != null && credentials.length == 2) {
username = credentials[0];
password = credentials[1];
}
}
if (username != null && password != null) {
handler.proceed(username, password);
} else {
if (mInForeground) {
mActivity.showHttpAuthentication(handler, host, realm,
null, null, null, 0);
} else {
handler.cancel();
}
}
}
@Override
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
if (!mInForeground) {
return false;
}
if (mActivity.isMenuDown()) {
// only check shortcut key when MENU is held
return mActivity.getWindow().isShortcutKey(event.getKeyCode(),
event);
} else {
return false;
}
}
@Override
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
if (!mInForeground || mActivity.mActivityInPause) {
return;
}
if (event.isDown()) {
mActivity.onKeyDown(event.getKeyCode(), event);
} else {
mActivity.onKeyUp(event.getKeyCode(), event);
}
}
};
// -------------------------------------------------------------------------
// WebChromeClient implementation for the main WebView
// -------------------------------------------------------------------------
private final WebChromeClient mWebChromeClient = new WebChromeClient() {
// Helper method to create a new tab or sub window.
private void createWindow(final boolean dialog, final Message msg) {
WebView.WebViewTransport transport =
(WebView.WebViewTransport) msg.obj;
if (dialog) {
createSubWindow();
mActivity.attachSubWindow(Tab.this);
transport.setWebView(mSubView);
} else {
final Tab newTab = mActivity.openTabAndShow(
BrowserActivity.EMPTY_URL_DATA, false, null);
if (newTab != Tab.this) {
Tab.this.addChildTab(newTab);
}
transport.setWebView(newTab.getWebView());
}
msg.sendToTarget();
}
@Override
public boolean onCreateWindow(WebView view, final boolean dialog,
final boolean userGesture, final Message resultMsg) {
// only allow new window or sub window for the foreground case
if (!mInForeground) {
return false;
}
// Short-circuit if we can't create any more tabs or sub windows.
if (dialog && mSubView != null) {
new AlertDialog.Builder(mActivity)
.setTitle(R.string.too_many_subwindows_dialog_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.too_many_subwindows_dialog_message)
.setPositiveButton(R.string.ok, null)
.show();
return false;
} else if (!mActivity.getTabControl().canCreateNewTab()) {
new AlertDialog.Builder(mActivity)
.setTitle(R.string.too_many_windows_dialog_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.too_many_windows_dialog_message)
.setPositiveButton(R.string.ok, null)
.show();
return false;
}
// Short-circuit if this was a user gesture.
if (userGesture) {
createWindow(dialog, resultMsg);
return true;
}
// Allow the popup and create the appropriate window.
final AlertDialog.OnClickListener allowListener =
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface d,
int which) {
createWindow(dialog, resultMsg);
}
};
// Block the popup by returning a null WebView.
final AlertDialog.OnClickListener blockListener =
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface d, int which) {
resultMsg.sendToTarget();
}
};
// Build a confirmation dialog to display to the user.
final AlertDialog d =
new AlertDialog.Builder(mActivity)
.setTitle(R.string.attention)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.popup_window_attempt)
.setPositiveButton(R.string.allow, allowListener)
.setNegativeButton(R.string.block, blockListener)
.setCancelable(false)
.create();
// Show the confirmation dialog.
d.show();
return true;
}
@Override
public void onRequestFocus(WebView view) {
if (!mInForeground) {
mActivity.switchToTab(mActivity.getTabControl().getTabIndex(
Tab.this));
}
}
@Override
public void onCloseWindow(WebView window) {
if (mParentTab != null) {
// JavaScript can only close popup window.
if (mInForeground) {
mActivity.switchToTab(mActivity.getTabControl()
.getTabIndex(mParentTab));
}
mActivity.closeTab(Tab.this);
}
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (newProgress == 100) {
// sync cookies and cache promptly here.
CookieSyncManager.getInstance().sync();
}
if (mInForeground) {
mActivity.onProgressChanged(view, newProgress);
}
}
@Override
public void onReceivedTitle(WebView view, final String title) {
final String pageUrl = view.getUrl();
if (mInForeground) {
// here, if url is null, we want to reset the title
mActivity.setUrlTitle(pageUrl, title);
}
if (pageUrl == null || pageUrl.length()
>= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
return;
}
new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... unused) {
// See if we can find the current url in our history
// database and add the new title to it.
String url = pageUrl;
if (url.startsWith("http://www.")) {
url = url.substring(11);
} else if (url.startsWith("http://")) {
url = url.substring(4);
}
// Escape wildcards for LIKE operator.
url = url.replace("\\", "\\\\").replace("%", "\\%")
.replace("_", "\\_");
Cursor c = null;
try {
final ContentResolver cr
= mActivity.getContentResolver();
url = "%" + url;
String [] selArgs = new String[] { url };
String where = Browser.BookmarkColumns.URL
+ " LIKE ? ESCAPE '\\' AND "
+ Browser.BookmarkColumns.BOOKMARK + " = 0";
c = cr.query(Browser.BOOKMARKS_URI, new String[]
{ Browser.BookmarkColumns._ID }, where, selArgs,
null);
if (c.moveToFirst()) {
// Current implementation of database only has one
// entry per url.
ContentValues map = new ContentValues();
map.put(Browser.BookmarkColumns.TITLE, title);
String[] projection = new String[]
{ Integer.valueOf(c.getInt(0)).toString() };
cr.update(Browser.BOOKMARKS_URI, map, "_id = ?",
projection);
}
} catch (IllegalStateException e) {
Log.e(LOGTAG, "Tab onReceived title", e);
} catch (SQLiteException ex) {
Log.e(LOGTAG,
"onReceivedTitle() caught SQLiteException: ",
ex);
} finally {
if (c != null) c.close();
}
return null;
}
}.execute();
}
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
if (icon != null) {
BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity
.getContentResolver(), view.getOriginalUrl(), view
.getUrl(), icon);
}
if (mInForeground) {
mActivity.setFavicon(icon);
}
}
@Override
public void onReceivedTouchIconUrl(WebView view, String url,
boolean precomposed) {
final ContentResolver cr = mActivity.getContentResolver();
// Let precomposed icons take precedence over non-composed
// icons.
if (precomposed && mTouchIconLoader != null) {
mTouchIconLoader.cancel(false);
mTouchIconLoader = null;
}
// Have only one async task at a time.
if (mTouchIconLoader == null) {
mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr, view);
mTouchIconLoader.execute(url);
}
}
@Override
public void onSelectionDone(WebView view) {
if (mInForeground) mActivity.closeDialogs();
}
@Override
public void onSelectionStart(WebView view) {
if (false && mInForeground) mActivity.showSelectDialog();
}
@Override
public void onShowCustomView(View view,
WebChromeClient.CustomViewCallback callback) {
if (mInForeground) mActivity.onShowCustomView(view, callback);
}
@Override
public void onHideCustomView() {
if (mInForeground) mActivity.onHideCustomView();
}
/**
* The origin has exceeded its database quota.
* @param url the URL that exceeded the quota
* @param databaseIdentifier the identifier of the database on which the
* transaction that caused the quota overflow was run
* @param currentQuota the current quota for the origin.
* @param estimatedSize the estimated size of the database.
* @param totalUsedQuota is the sum of all origins' quota.
* @param quotaUpdater The callback to run when a decision to allow or
* deny quota has been made. Don't forget to call this!
*/
@Override
public void onExceededDatabaseQuota(String url,
String databaseIdentifier, long currentQuota, long estimatedSize,
long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
BrowserSettings.getInstance().getWebStorageSizeManager()
.onExceededDatabaseQuota(url, databaseIdentifier,
currentQuota, estimatedSize, totalUsedQuota,
quotaUpdater);
}
/**
* The Application Cache has exceeded its max size.
* @param spaceNeeded is the amount of disk space that would be needed
* in order for the last appcache operation to succeed.
* @param totalUsedQuota is the sum of all origins' quota.
* @param quotaUpdater A callback to inform the WebCore thread that a
* new app cache size is available. This callback must always
* be executed at some point to ensure that the sleeping
* WebCore thread is woken up.
*/
@Override
public void onReachedMaxAppCacheSize(long spaceNeeded,
long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
BrowserSettings.getInstance().getWebStorageSizeManager()
.onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota,
quotaUpdater);
}
/**
* Instructs the browser to show a prompt to ask the user to set the
* Geolocation permission state for the specified origin.
* @param origin The origin for which Geolocation permissions are
* requested.
* @param callback The callback to call once the user has set the
* Geolocation permission state.
*/
@Override
public void onGeolocationPermissionsShowPrompt(String origin,
GeolocationPermissions.Callback callback) {
if (mInForeground) {
getGeolocationPermissionsPrompt().show(origin, callback);
}
}
/**
* Instructs the browser to hide the Geolocation permissions prompt.
*/
@Override
public void onGeolocationPermissionsHidePrompt() {
if (mInForeground && mGeolocationPermissionsPrompt != null) {
mGeolocationPermissionsPrompt.hide();
}
}
/* Adds a JavaScript error message to the system log and if the JS
* console is enabled in the about:debug options, to that console
* also.
* @param consoleMessage the message object.
*/
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
if (mInForeground) {
// call getErrorConsole(true) so it will create one if needed
ErrorConsoleView errorConsole = getErrorConsole(true);
errorConsole.addErrorMessage(consoleMessage);
if (mActivity.shouldShowErrorConsole()
&& errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) {
errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
}
}
String message = "Console: " + consoleMessage.message() + " "
+ consoleMessage.sourceId() + ":"
+ consoleMessage.lineNumber();
switch (consoleMessage.messageLevel()) {
case TIP:
Log.v(CONSOLE_LOGTAG, message);
break;
case LOG:
Log.i(CONSOLE_LOGTAG, message);
break;
case WARNING:
Log.w(CONSOLE_LOGTAG, message);
break;
case ERROR:
Log.e(CONSOLE_LOGTAG, message);
break;
case DEBUG:
Log.d(CONSOLE_LOGTAG, message);
break;
}
return true;
}
/**
* Ask the browser for an icon to represent a <video> element.
* This icon will be used if the Web page did not specify a poster attribute.
* @return Bitmap The icon or null if no such icon is available.
*/
@Override
public Bitmap getDefaultVideoPoster() {
if (mInForeground) {
return mActivity.getDefaultVideoPoster();
}
return null;
}
/**
* Ask the host application for a custom progress view to show while
* a <video> is loading.
* @return View The progress view.
*/
@Override
public View getVideoLoadingProgressView() {
if (mInForeground) {
return mActivity.getVideoLoadingProgressView();
}
return null;
}
@Override
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
if (mInForeground) {
mActivity.openFileChooser(uploadMsg);
} else {
uploadMsg.onReceiveValue(null);
}
}
/**
* Deliver a list of already-visited URLs
*/
@Override
public void getVisitedHistory(final ValueCallback<String[]> callback) {
AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() {
public String[] doInBackground(Void... unused) {
return Browser.getVisitedHistory(mActivity
.getContentResolver());
}
public void onPostExecute(String[] result) {
callback.onReceiveValue(result);
};
};
task.execute();
};
};
// -------------------------------------------------------------------------
// WebViewClient implementation for the sub window
// -------------------------------------------------------------------------
// Subclass of WebViewClient used in subwindows to notify the main
// WebViewClient of certain WebView activities.
private static class SubWindowClient extends WebViewClient {
// The main WebViewClient.
private final WebViewClient mClient;
private final BrowserActivity mBrowserActivity;
SubWindowClient(WebViewClient client, BrowserActivity activity) {
mClient = client;
mBrowserActivity = activity;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// Unlike the others, do not call mClient's version, which would
// change the progress bar. However, we do want to remove the
// find or select dialog.
mBrowserActivity.closeDialogs();
}
@Override
public void doUpdateVisitedHistory(WebView view, String url,
boolean isReload) {
mClient.doUpdateVisitedHistory(view, url, isReload);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return mClient.shouldOverrideUrlLoading(view, url);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler,
SslError error) {
mClient.onReceivedSslError(view, handler, error);
}
@Override
public void onReceivedHttpAuthRequest(WebView view,
HttpAuthHandler handler, String host, String realm) {
mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
}
@Override
public void onFormResubmission(WebView view, Message dontResend,
Message resend) {
mClient.onFormResubmission(view, dontResend, resend);
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
mClient.onReceivedError(view, errorCode, description, failingUrl);
}
@Override
public boolean shouldOverrideKeyEvent(WebView view,
android.view.KeyEvent event) {
return mClient.shouldOverrideKeyEvent(view, event);
}
@Override
public void onUnhandledKeyEvent(WebView view,
android.view.KeyEvent event) {
mClient.onUnhandledKeyEvent(view, event);
}
}
// -------------------------------------------------------------------------
// WebChromeClient implementation for the sub window
// -------------------------------------------------------------------------
private class SubWindowChromeClient extends WebChromeClient {
// The main WebChromeClient.
private final WebChromeClient mClient;
SubWindowChromeClient(WebChromeClient client) {
mClient = client;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
mClient.onProgressChanged(view, newProgress);
}
@Override
public boolean onCreateWindow(WebView view, boolean dialog,
boolean userGesture, android.os.Message resultMsg) {
return mClient.onCreateWindow(view, dialog, userGesture, resultMsg);
}
@Override
public void onCloseWindow(WebView window) {
if (window != mSubView) {
Log.e(LOGTAG, "Can't close the window");
}
mActivity.dismissSubWindow(Tab.this);
}
}
// -------------------------------------------------------------------------
// Construct a new tab
Tab(BrowserActivity activity, WebView w, boolean closeOnExit, String appId,
String url) {
mActivity = activity;
mCloseOnExit = closeOnExit;
mAppId = appId;
mOriginalUrl = url;
mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
mPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
mInLoad = false;
mInForeground = false;
mInflateService = LayoutInflater.from(activity);
// The tab consists of a container view, which contains the main
// WebView, as well as any other UI elements associated with the tab.
mContainer = (LinearLayout) mInflateService.inflate(R.layout.tab, null);
mDownloadListener = new DownloadListener() {
public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimetype,
long contentLength) {
mActivity.onDownloadStart(url, userAgent, contentDisposition,
mimetype, contentLength);
if (mMainView.copyBackForwardList().getSize() == 0) {
// This Tab was opened for the sole purpose of downloading a
// file. Remove it.
if (mActivity.getTabControl().getCurrentWebView()
== mMainView) {
// In this case, the Tab is still on top.
mActivity.goBackOnePageOrQuit();
} else {
// In this case, it is not.
mActivity.closeTab(Tab.this);
}
}
}
};
mWebBackForwardListClient = new WebBackForwardListClient() {
@Override
public void onNewHistoryItem(WebHistoryItem item) {
if (isInVoiceSearchMode()) {
item.setCustomData(mVoiceSearchData.mVoiceSearchIntent);
}
}
@Override
public void onIndexChanged(WebHistoryItem item, int index) {
Object data = item.getCustomData();
if (data != null && data instanceof Intent) {
activateVoiceSearchMode((Intent) data);
}
}
};
setWebView(w);
}
/**
* Sets the WebView for this tab, correctly removing the old WebView from
* the container view.
*/
void setWebView(WebView w) {
if (mMainView == w) {
return;
}
// If the WebView is changing, the page will be reloaded, so any ongoing
// Geolocation permission requests are void.
if (mGeolocationPermissionsPrompt != null) {
mGeolocationPermissionsPrompt.hide();
}
// Just remove the old one.
FrameLayout wrapper =
(FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
wrapper.removeView(mMainView);
// set the new one
mMainView = w;
// attach the WebViewClient, WebChromeClient and DownloadListener
if (mMainView != null) {
mMainView.setWebViewClient(mWebViewClient);
mMainView.setWebChromeClient(mWebChromeClient);
// Attach DownloadManager so that downloads can start in an active
// or a non-active window. This can happen when going to a site that
// does a redirect after a period of time. The user could have
// switched to another tab while waiting for the download to start.
mMainView.setDownloadListener(mDownloadListener);
mMainView.setWebBackForwardListClient(mWebBackForwardListClient);
}
}
/**
* Destroy the tab's main WebView and subWindow if any
*/
void destroy() {
if (mMainView != null) {
dismissSubWindow();
BrowserSettings.getInstance().deleteObserver(mMainView.getSettings());
// save the WebView to call destroy() after detach it from the tab
WebView webView = mMainView;
setWebView(null);
webView.destroy();
}
}
/**
* Remove the tab from the parent
*/
void removeFromTree() {
// detach the children
if (mChildTabs != null) {
for(Tab t : mChildTabs) {
t.setParentTab(null);
}
}
// remove itself from the parent list
if (mParentTab != null) {
mParentTab.mChildTabs.remove(this);
}
}
/**
* Create a new subwindow unless a subwindow already exists.
* @return True if a new subwindow was created. False if one already exists.
*/
boolean createSubWindow() {
if (mSubView == null) {
mActivity.closeDialogs();
mSubViewContainer = mInflateService.inflate(
R.layout.browser_subwindow, null);
mSubView = (WebView) mSubViewContainer.findViewById(R.id.webview);
mSubView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
// use trackball directly
mSubView.setMapTrackballToArrowKeys(false);
// Enable the built-in zoom
mSubView.getSettings().setBuiltInZoomControls(true);
mSubView.setWebViewClient(new SubWindowClient(mWebViewClient,
mActivity));
mSubView.setWebChromeClient(new SubWindowChromeClient(
mWebChromeClient));
// Set a different DownloadListener for the mSubView, since it will
// just need to dismiss the mSubView, rather than close the Tab
mSubView.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimetype,
long contentLength) {
mActivity.onDownloadStart(url, userAgent,
contentDisposition, mimetype, contentLength);
if (mSubView.copyBackForwardList().getSize() == 0) {
// This subwindow was opened for the sole purpose of
// downloading a file. Remove it.
mActivity.dismissSubWindow(Tab.this);
}
}
});
mSubView.setOnCreateContextMenuListener(mActivity);
final BrowserSettings s = BrowserSettings.getInstance();
s.addObserver(mSubView.getSettings()).update(s, null);
final ImageButton cancel = (ImageButton) mSubViewContainer
.findViewById(R.id.subwindow_close);
cancel.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mSubView.getWebChromeClient().onCloseWindow(mSubView);
}
});
return true;
}
return false;
}
/**
* Dismiss the subWindow for the tab.
*/
void dismissSubWindow() {
if (mSubView != null) {
mActivity.closeDialogs();
BrowserSettings.getInstance().deleteObserver(
mSubView.getSettings());
mSubView.destroy();
mSubView = null;
mSubViewContainer = null;
}
}
/**
* Attach the sub window to the content view.
*/
void attachSubWindow(ViewGroup content) {
if (mSubView != null) {
content.addView(mSubViewContainer,
BrowserActivity.COVER_SCREEN_PARAMS);
}
}
/**
* Remove the sub window from the content view.
*/
void removeSubWindow(ViewGroup content) {
if (mSubView != null) {
content.removeView(mSubViewContainer);
mActivity.closeDialogs();
}
}
/**
* This method attaches both the WebView and any sub window to the
* given content view.
*/
void attachTabToContentView(ViewGroup content) {
if (mMainView == null) {
return;
}
// Attach the WebView to the container and then attach the
// container to the content view.
FrameLayout wrapper =
(FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
ViewGroup parent = (ViewGroup) mMainView.getParent();
if (parent != wrapper) {
if (parent != null) {
Log.w(LOGTAG, "mMainView already has a parent in"
+ " attachTabToContentView!");
parent.removeView(mMainView);
}
wrapper.addView(mMainView);
} else {
Log.w(LOGTAG, "mMainView is already attached to wrapper in"
+ " attachTabToContentView!");
}
parent = (ViewGroup) mContainer.getParent();
if (parent != content) {
if (parent != null) {
Log.w(LOGTAG, "mContainer already has a parent in"
+ " attachTabToContentView!");
parent.removeView(mContainer);
}
content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS);
} else {
Log.w(LOGTAG, "mContainer is already attached to content in"
+ " attachTabToContentView!");
}
attachSubWindow(content);
}
/**
* Remove the WebView and any sub window from the given content view.
*/
void removeTabFromContentView(ViewGroup content) {
if (mMainView == null) {
return;
}
// Remove the container from the content and then remove the
// WebView from the container. This will trigger a focus change
// needed by WebView.
FrameLayout wrapper =
(FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
wrapper.removeView(mMainView);
content.removeView(mContainer);
mActivity.closeDialogs();
removeSubWindow(content);
}
/**
* Set the parent tab of this tab.
*/
void setParentTab(Tab parent) {
mParentTab = parent;
// This tab may have been freed due to low memory. If that is the case,
// the parent tab index is already saved. If we are changing that index
// (most likely due to removing the parent tab) we must update the
// parent tab index in the saved Bundle.
if (mSavedState != null) {
if (parent == null) {
mSavedState.remove(PARENTTAB);
} else {
mSavedState.putInt(PARENTTAB, mActivity.getTabControl()
.getTabIndex(parent));
}
}
}
/**
* When a Tab is created through the content of another Tab, then we
* associate the Tabs.
* @param child the Tab that was created from this Tab
*/
void addChildTab(Tab child) {
if (mChildTabs == null) {
mChildTabs = new Vector<Tab>();
}
mChildTabs.add(child);
child.setParentTab(this);
}
Vector<Tab> getChildTabs() {
return mChildTabs;
}
void resume() {
if (mMainView != null) {
mMainView.onResume();
if (mSubView != null) {
mSubView.onResume();
}
}
}
void pause() {
if (mMainView != null) {
mMainView.onPause();
if (mSubView != null) {
mSubView.onPause();
}
}
}
void putInForeground() {
mInForeground = true;
resume();
mMainView.setOnCreateContextMenuListener(mActivity);
if (mSubView != null) {
mSubView.setOnCreateContextMenuListener(mActivity);
}
// Show the pending error dialog if the queue is not empty
if (mQueuedErrors != null && mQueuedErrors.size() > 0) {
showError(mQueuedErrors.getFirst());
}
}
void putInBackground() {
mInForeground = false;
pause();
mMainView.setOnCreateContextMenuListener(null);
if (mSubView != null) {
mSubView.setOnCreateContextMenuListener(null);
}
}
/**
* Return the top window of this tab; either the subwindow if it is not
* null or the main window.
* @return The top window of this tab.
*/
WebView getTopWindow() {
if (mSubView != null) {
return mSubView;
}
return mMainView;
}
/**
* Return the main window of this tab. Note: if a tab is freed in the
* background, this can return null. It is only guaranteed to be
* non-null for the current tab.
* @return The main WebView of this tab.
*/
WebView getWebView() {
return mMainView;
}
/**
* Return the subwindow of this tab or null if there is no subwindow.
* @return The subwindow of this tab or null.
*/
WebView getSubWebView() {
return mSubView;
}
/**
* @return The geolocation permissions prompt for this tab.
*/
GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() {
if (mGeolocationPermissionsPrompt == null) {
ViewStub stub = (ViewStub) mContainer
.findViewById(R.id.geolocation_permissions_prompt);
mGeolocationPermissionsPrompt = (GeolocationPermissionsPrompt) stub
.inflate();
mGeolocationPermissionsPrompt.init();
}
return mGeolocationPermissionsPrompt;
}
/**
* @return The application id string
*/
String getAppId() {
return mAppId;
}
/**
* Set the application id string
* @param id
*/
void setAppId(String id) {
mAppId = id;
}
/**
* @return The original url associated with this Tab
*/
String getOriginalUrl() {
return mOriginalUrl;
}
/**
* Set the original url associated with this tab
*/
void setOriginalUrl(String url) {
mOriginalUrl = url;
}
/**
* Get the url of this tab. Valid after calling populatePickerData, but
* before calling wipePickerData, or if the webview has been destroyed.
* @return The WebView's url or null.
*/
String getUrl() {
if (mPickerData != null) {
return mPickerData.mUrl;
}
return null;
}
/**
* Get the title of this tab. Valid after calling populatePickerData, but
* before calling wipePickerData, or if the webview has been destroyed. If
* the url has no title, use the url instead.
* @return The WebView's title (or url) or null.
*/
String getTitle() {
if (mPickerData != null) {
return mPickerData.mTitle;
}
return null;
}
/**
* Get the favicon of this tab. Valid after calling populatePickerData, but
* before calling wipePickerData, or if the webview has been destroyed.
* @return The WebView's favicon or null.
*/
Bitmap getFavicon() {
if (mPickerData != null) {
return mPickerData.mFavicon;
}
return null;
}
/**
* Return the tab's error console. Creates the console if createIfNEcessary
* is true and we haven't already created the console.
* @param createIfNecessary Flag to indicate if the console should be
* created if it has not been already.
* @return The tab's error console, or null if one has not been created and
* createIfNecessary is false.
*/
ErrorConsoleView getErrorConsole(boolean createIfNecessary) {
if (createIfNecessary && mErrorConsole == null) {
mErrorConsole = new ErrorConsoleView(mActivity);
mErrorConsole.setWebView(mMainView);
}
return mErrorConsole;
}
/**
* If this Tab was created through another Tab, then this method returns
* that Tab.
* @return the Tab parent or null
*/
public Tab getParentTab() {
return mParentTab;
}
/**
* Return whether this tab should be closed when it is backing out of the
* first page.
* @return TRUE if this tab should be closed when exit.
*/
boolean closeOnExit() {
return mCloseOnExit;
}
/**
* Saves the current lock-icon state before resetting the lock icon. If we
* have an error, we may need to roll back to the previous state.
*/
void resetLockIcon(String url) {
mPrevLockIconType = mLockIconType;
mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE;
if (URLUtil.isHttpsUrl(url)) {
mLockIconType = BrowserActivity.LOCK_ICON_SECURE;
}
}
/**
* Reverts the lock-icon state to the last saved state, for example, if we
* had an error, and need to cancel the load.
*/
void revertLockIcon() {
mLockIconType = mPrevLockIconType;
}
/**
* @return The tab's lock icon type.
*/
int getLockIconType() {
return mLockIconType;
}
/**
* @return TRUE if onPageStarted is called while onPageFinished is not
* called yet.
*/
boolean inLoad() {
return mInLoad;
}
// force mInLoad to be false. This should only be called before closing the
// tab to ensure BrowserActivity's pauseWebViewTimers() is called correctly.
void clearInLoad() {
mInLoad = false;
}
void populatePickerData() {
if (mMainView == null) {
populatePickerDataFromSavedState();
return;
}
// FIXME: The only place we cared about subwindow was for
// bookmarking (i.e. not when saving state). Was this deliberate?
final WebBackForwardList list = mMainView.copyBackForwardList();
final WebHistoryItem item = list != null ? list.getCurrentItem() : null;
populatePickerData(item);
}
// Populate the picker data using the given history item and the current top
// WebView.
private void populatePickerData(WebHistoryItem item) {
if (item != null && !TextUtils.isEmpty(item.getUrl())) {
mPickerData = new PickerData();
mPickerData.mUrl = item.getUrl();
mPickerData.mTitle = item.getTitle();
mPickerData.mFavicon = item.getFavicon();
if (mPickerData.mTitle == null) {
mPickerData.mTitle = mPickerData.mUrl;
}
}
}
// Create the PickerData and populate it using the saved state of the tab.
void populatePickerDataFromSavedState() {
if (mSavedState == null) {
return;
}
mPickerData = new PickerData();
mPickerData.mUrl = mSavedState.getString(CURRURL);
mPickerData.mTitle = mSavedState.getString(CURRTITLE);
}
void clearPickerData() {
mPickerData = null;
}
/**
* Get the saved state bundle.
* @return
*/
Bundle getSavedState() {
return mSavedState;
}
/**
* Set the saved state.
*/
void setSavedState(Bundle state) {
mSavedState = state;
}
/**
* @return TRUE if succeed in saving the state.
*/
boolean saveState() {
// If the WebView is null it means we ran low on memory and we already
// stored the saved state in mSavedState.
if (mMainView == null) {
return mSavedState != null;
}
mSavedState = new Bundle();
final WebBackForwardList list = mMainView.saveState(mSavedState);
// Store some extra info for displaying the tab in the picker.
final WebHistoryItem item = list != null ? list.getCurrentItem() : null;
populatePickerData(item);
if (mPickerData != null && mPickerData.mUrl != null) {
mSavedState.putString(CURRURL, mPickerData.mUrl);
}
if (mPickerData != null && mPickerData.mTitle != null) {
mSavedState.putString(CURRTITLE, mPickerData.mTitle);
}
mSavedState.putBoolean(CLOSEONEXIT, mCloseOnExit);
if (mAppId != null) {
mSavedState.putString(APPID, mAppId);
}
if (mOriginalUrl != null) {
mSavedState.putString(ORIGINALURL, mOriginalUrl);
}
// Remember the parent tab so the relationship can be restored.
if (mParentTab != null) {
mSavedState.putInt(PARENTTAB, mActivity.getTabControl().getTabIndex(
mParentTab));
}
return true;
}
/*
* Restore the state of the tab.
*/
boolean restoreState(Bundle b) {
if (b == null) {
return false;
}
// Restore the internal state even if the WebView fails to restore.
// This will maintain the app id, original url and close-on-exit values.
mSavedState = null;
mCloseOnExit = b.getBoolean(CLOSEONEXIT);
mAppId = b.getString(APPID);
mOriginalUrl = b.getString(ORIGINALURL);
final WebBackForwardList list = mMainView.restoreState(b);
if (list == null) {
return false;
}
return true;
}
/*
* Opens the find and select text dialogs. Called by BrowserActivity.
*/
WebView showDialog(WebDialog dialog) {
LinearLayout container;
WebView view;
if (mSubView != null) {
view = mSubView;
container = (LinearLayout) mSubViewContainer.findViewById(
R.id.inner_container);
} else {
view = mMainView;
container = mContainer;
}
dialog.show();
container.addView(dialog, 0, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
dialog.setWebView(view);
return view;
}
/*
* Close the find or select dialog. Called by BrowserActivity.closeDialog.
*/
void closeDialog(WebDialog dialog) {
// The dialog may be attached to the subwindow. Ensure that the
// correct parent has it removed.
LinearLayout parent = (LinearLayout) dialog.getParent();
if (parent != null) parent.removeView(dialog);
}
}