X-Git-Url: http://git.osdn.net/view?a=blobdiff_plain;f=src%2Fcom%2Fandroid%2Fbrowser%2FTab.java;h=535e8e7fa370f23157e129ada4266ef886c381d3;hb=2ee4a5acc5c1ef87afa02830b31770cb1359b626;hp=447e6becbb64fbdc2307bc69d769684ef430ed0b;hpb=7ab41fa8f2a0c8abf159279f9ea3cd8d61047893;p=android-x86%2Fpackages-apps-Browser.git diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java index 447e6be..535e8e7 100644 --- a/src/com/android/browser/Tab.java +++ b/src/com/android/browser/Tab.java @@ -17,14 +17,20 @@ 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; @@ -34,20 +40,25 @@ 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.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; @@ -59,12 +70,19 @@ 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 @@ -93,6 +111,8 @@ class Tab { 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; @@ -108,6 +128,11 @@ class Tab { 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; @@ -133,6 +158,218 @@ class Tab { // ------------------------------------------------------------------------- + /** + * 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 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 results = intent.getStringArrayListExtra( + RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS); + if (results != null) { + ArrayList urls = intent.getStringArrayListExtra( + RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS); + ArrayList htmls = intent.getStringArrayListExtra( + RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_HTML); + ArrayList 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 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 iter = bundle.keySet().iterator(); + headers = new HashMap(); + 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 results, + ArrayList urls, ArrayList htmls, + ArrayList baseUrls) { + mVoiceSearchResults = results; + mVoiceSearchUrls = urls; + mVoiceSearchHtmls = htmls; + mVoiceSearchBaseUrls = baseUrls; + } + /* + * ArrayList of suggestions to be displayed when opening the + * SearchManager + */ + public ArrayList mVoiceSearchResults; + /* + * ArrayList of urls, associated with the suggestions in + * mVoiceSearchResults. + */ + public ArrayList mVoiceSearchUrls; + /* + * ArrayList holding content to load for each item in + * mVoiceSearchResults. + */ + public ArrayList 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 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 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; @@ -206,9 +443,24 @@ class Tab { // ------------------------------------------------------------------------- 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 @@ -255,6 +507,8 @@ class Tab { @Override public void onPageFinished(WebView view, String url) { + LogTag.logPageFinishedLoading( + url, SystemClock.uptimeMillis() - mLoadStartTime); mInLoad = false; if (mInForeground && !mActivity.didUserStopLoading() @@ -304,37 +558,6 @@ class Tab { } /** - * 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. */ @@ -368,6 +591,14 @@ class Tab { 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) @@ -375,17 +606,29 @@ class Tab { 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(); } @@ -660,6 +903,14 @@ class Tab { } @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. @@ -683,45 +934,60 @@ class Tab { } @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() { + 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 @@ -755,6 +1021,8 @@ class Tab { mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr, c, view); mTouchIconLoader.execute(url); + } else { + c.close(); } } else { c.close(); @@ -838,25 +1106,46 @@ class Tab { } } - /* 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; } /** @@ -1019,6 +1308,42 @@ class Tab { (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); } @@ -1041,10 +1366,16 @@ class Tab { // 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); } } @@ -1092,7 +1423,21 @@ class Tab { 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); @@ -1503,6 +1848,10 @@ class Tab { 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(); } }