/* * 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 com.android.browser.TabControl.TabChangeListener; import com.android.common.speech.LoggingEvents; 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.provider.BrowserContract.History; import android.speech.RecognizerResultsIntent; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewStub; 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 java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Vector; /** * 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 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"; static final String INCOGNITO = "privateBrowsingEnabled"; // ------------------------------------------------------------------------- /** * 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 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; 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 mQueuedErrors; private void queueError(int err, String desc) { if (mQueuedErrors == null) { mQueuedErrors = new LinkedList(); } 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 maybeUpdateFavicon(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); } if (getTabChangeListener() != null) { getTabChangeListener().onPageStarted(Tab.this, url, favicon); } } @Override public void onPageFinished(WebView view, String url) { LogTag.logPageFinishedLoading( url, SystemClock.uptimeMillis() - mLoadStartTime); mInLoad = false; if (!isPrivateBrowsingEnabled()) { 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); } if (getTabChangeListener() != null) { getTabChangeListener().onPageFinished(Tab.this); } } // 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); } // Don't log URLs when in private browsing mode if (!isPrivateBrowsingEnabled()) { 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) { // Don't save anything in private browsing mode if (isPrivateBrowsingEnabled()) return; 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() { @Override 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); } 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.getAction() == KeyEvent.ACTION_DOWN) { 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); } if (getTabChangeListener() != null) { getTabChangeListener().onProgress(Tab.this, 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); } TabChangeListener tcl = getTabChangeListener(); if (tcl != null) { tcl.onUrlAndTitle(Tab.this, pageUrl,title); } if (pageUrl == null || pageUrl.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { return; } // Update the title in the history database if not in private browsing mode if (!isPrivateBrowsingEnabled()) { new AsyncTask() { @Override 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(); String selection = History.URL + " LIKE ? ESCAPE '\\'"; String [] selectionArgs = new String[] { "%" + url }; ContentValues values = new ContentValues(); values.put(History.TITLE, title); cr.update(History.CONTENT_URI, values, selection, selectionArgs); } 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) { maybeUpdateFavicon(view.getOriginalUrl(), view.getUrl(), icon); if (mInForeground) { mActivity.setFavicon(icon); } if (getTabChangeListener() != null) { getTabChangeListener().onFavicon(Tab.this, 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, mActivity, cr, view); mTouchIconLoader.execute(url); } } @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); } } // Don't log console messages in private browsing mode if (isPrivateBrowsingEnabled()) return true; 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