/* * 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.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) { mPickerData = new PickerData(); if (item != null) { 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.mUrl != null) { mSavedState.putString(CURRURL, mPickerData.mUrl); } if (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; mPickerData = 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); } }