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.os.AsyncTask;
import android.os.Bundle;
import android.os.Message;
+import android.os.SystemClock;
import android.provider.Browser;
+import android.speech.RecognizerResultsIntent;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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.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 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;
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;
// -------------------------------------------------------------------------
+ /**
+ * Private information regarding the latest voice search. If the Tab is not
+ * in voice search mode, this will be null.
+ */
+ private VoiceSearchData mVoiceSearchData;
+ /**
+ * Return whether the tab is in voice search mode.
+ */
+ public boolean isInVoiceSearchMode() {
+ return mVoiceSearchData != null;
+ }
+ /**
+ * Return true if 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;
// -------------------------------------------------------------------------
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);
+ }
+ mVoiceSearchData = null;
+ if (mInForeground) {
+ mActivity.revertVoiceTitleBar();
+ }
+ }
// 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
@Override
public void onPageFinished(WebView view, String url) {
+ LogTag.logPageFinishedLoading(
+ url, SystemClock.uptimeMillis() - mLoadStartTime);
mInLoad = false;
if (mInForeground && !mActivity.didUserStopLoading()
}
/**
- * Show the dialog if it is in the foreground, asking the user if they
- * would like to continue after an excessive number of HTTP redirects.
- * Cancel if it is in the background.
- */
- @Override
- public void onTooManyRedirects(WebView view, final Message cancelMsg,
- final Message continueMsg) {
- if (!mInForeground) {
- cancelMsg.sendToTarget();
- return;
- }
- new AlertDialog.Builder(mActivity).setTitle(
- R.string.browserFrameRedirect).setMessage(
- R.string.browserFrame307Post).setPositiveButton(
- R.string.ok, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- continueMsg.sendToTarget();
- }
- }).setNegativeButton(R.string.cancel,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- cancelMsg.sendToTarget();
- }
- }).setOnCancelListener(new OnCancelListener() {
- public void onCancel(DialogInterface dialog) {
- cancelMsg.sendToTarget();
- }
- }).show();
- }
-
- /**
* Show a dialog informing the user of the network error reported by
* WebCore if it is in the foreground.
*/
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)
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
- resend.sendToTarget();
+ if (mResend != null) {
+ mResend.sendToTarget();
+ mResend = null;
+ mDontResend = null;
+ }
}
}).setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
- dontResend.sendToTarget();
+ if (mDontResend != null) {
+ mDontResend.sendToTarget();
+ mResend = null;
+ mDontResend = null;
+ }
}
}).setOnCancelListener(new OnCancelListener() {
public void onCancel(DialogInterface dialog) {
- dontResend.sendToTarget();
+ if (mDontResend != null) {
+ mDontResend.sendToTarget();
+ mResend = null;
+ mDontResend = null;
+ }
}
}).show();
}
}
@Override
- public void onReceivedTitle(WebView view, String title) {
- String url = view.getUrl();
+ 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(url, title);
+ mActivity.setUrlTitle(pageUrl, title);
}
- if (url == null ||
- url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
+ if (pageUrl == null || pageUrl.length()
+ >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
return;
}
- // See if we can find the current url in our history database and
- // add the new title to it.
- if (url.startsWith("http://www.")) {
- url = url.substring(11);
- } else if (url.startsWith("http://")) {
- url = url.substring(4);
- }
- try {
- final ContentResolver cr = mActivity.getContentResolver();
- url = "%" + url;
- String [] selArgs = new String[] { url };
- String where = Browser.BookmarkColumns.URL + " LIKE ? AND "
- + Browser.BookmarkColumns.BOOKMARK + " = 0";
- Cursor c = cr.query(Browser.BOOKMARKS_URI,
- Browser.HISTORY_PROJECTION, 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);
- cr.update(Browser.BOOKMARKS_URI, map, "_id = "
- + c.getInt(0), null);
+ 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);
+ }
+ Cursor c = null;
+ try {
+ final ContentResolver cr
+ = mActivity.getContentResolver();
+ url = "%" + url;
+ String [] selArgs = new String[] { url };
+ String where = Browser.BookmarkColumns.URL
+ + " LIKE ? 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;
}
- c.close();
- } catch (IllegalStateException e) {
- Log.e(LOGTAG, "Tab onReceived title", e);
- } catch (SQLiteException ex) {
- Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex);
- }
+ }.execute();
}
@Override
mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr,
c, view);
mTouchIconLoader.execute(url);
+ } else {
+ c.close();
}
} else {
c.close();
}
}
- /* Adds a JavaScript error message to the system log.
- * @param message The error message to report.
- * @param lineNumber The line number of the error.
- * @param sourceID The name of the source file that caused the error.
+ /* 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 void addMessageToConsole(String message, int lineNumber,
- String sourceID) {
+ public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
if (mInForeground) {
// call getErrorConsole(true) so it will create one if needed
ErrorConsoleView errorConsole = getErrorConsole(true);
- errorConsole.addErrorMessage(message, sourceID, lineNumber);
+ errorConsole.addErrorMessage(consoleMessage);
if (mActivity.shouldShowErrorConsole()
&& errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) {
errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
}
}
- Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":"
- + lineNumber);
+
+ 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;
}
/**
(GeolocationPermissionsPrompt) mContainer.findViewById(
R.id.geolocation_permissions_prompt);
+ 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);
}
// set the new one
mMainView = w;
- // attached the WebViewClient and WebChromeClient
+ // 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);
}
}
mSubView.setWebViewClient(new SubWindowClient(mWebViewClient));
mSubView.setWebChromeClient(new SubWindowChromeClient(
mWebChromeClient));
- mSubView.setDownloadListener(mActivity);
+ // 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.
+ dismissSubWindow();
+ }
+ }
+ });
mSubView.setOnCreateContextMenuListener(mActivity);
final BrowserSettings s = BrowserSettings.getInstance();
s.addObserver(mSubView.getSettings()).update(s, null);
mMainView.hashCode() + "_pic.save");
if (mMainView.savePicture(mSavedState, f)) {
mSavedState.putString(CURRPICTURE, f.getPath());
+ } else {
+ // if savePicture returned false, we can't trust the contents,
+ // and it may be large, so we delete it right away
+ f.delete();
}
}