| /* |
| * 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 android.app.Activity; |
| import android.app.AlertDialog; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.DialogInterface.OnCancelListener; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.CompressFormat; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Picture; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.net.Uri; |
| import android.net.http.SslError; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.SystemClock; |
| import android.security.KeyChain; |
| import android.security.KeyChainAliasCallback; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewStub; |
| import android.webkit.BrowserDownloadListener; |
| import android.webkit.ClientCertRequestHandler; |
| import android.webkit.ConsoleMessage; |
| 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.WebResourceResponse; |
| import android.webkit.WebStorage; |
| import android.webkit.WebView; |
| import android.webkit.WebView.PictureListener; |
| import android.webkit.WebViewClassic; |
| import android.webkit.WebViewClient; |
| import android.webkit.WebViewClientClassicExt; |
| import android.widget.CheckBox; |
| import android.widget.Toast; |
| |
| import com.android.browser.TabControl.OnThumbnailUpdatedListener; |
| import com.android.browser.homepages.HomeProvider; |
| import com.android.browser.provider.SnapshotProvider.Snapshots; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.util.Vector; |
| import java.util.regex.Pattern; |
| import java.util.zip.GZIPOutputStream; |
| |
| /** |
| * Class for maintaining Tabs with a main WebView and a subwindow. |
| */ |
| class Tab implements PictureListener { |
| |
| // Log Tag |
| private static final String LOGTAG = "Tab"; |
| private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; |
| // 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"; |
| |
| private static final int MSG_CAPTURE = 42; |
| private static final int CAPTURE_DELAY = 100; |
| private static final int INITIAL_PROGRESS = 5; |
| |
| private static Bitmap sDefaultFavicon; |
| |
| private static Paint sAlphaPaint = new Paint(); |
| static { |
| sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); |
| sAlphaPaint.setColor(Color.TRANSPARENT); |
| } |
| |
| public enum SecurityState { |
| // The page's main resource does not use SSL. Note that we use this |
| // state irrespective of the SSL authentication state of sub-resources. |
| SECURITY_STATE_NOT_SECURE, |
| // The page's main resource uses SSL and the certificate is good. The |
| // same is true of all sub-resources. |
| SECURITY_STATE_SECURE, |
| // The page's main resource uses SSL and the certificate is good, but |
| // some sub-resources either do not use SSL or have problems with their |
| // certificates. |
| SECURITY_STATE_MIXED, |
| // The page's main resource uses SSL but there is a problem with its |
| // certificate. |
| SECURITY_STATE_BAD_CERTIFICATE, |
| } |
| |
| Context mContext; |
| protected WebViewController mWebViewController; |
| |
| // The tab ID |
| private long mId = -1; |
| |
| // The Geolocation permissions prompt |
| private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; |
| // Main WebView wrapper |
| private View 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; |
| // Parent Tab. This is the Tab that created this Tab, or null if the Tab was |
| // created by the UI |
| private Tab mParent; |
| // 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> mChildren; |
| // If true, the tab is in the foreground of the current activity. |
| private boolean mInForeground; |
| // If true, the tab is in page loading state (after onPageStarted, |
| // before onPageFinsihed) |
| private boolean mInPageLoad; |
| private boolean mDisableOverrideUrlLoading; |
| // The last reported progress of the current page |
| private int mPageLoadProgress; |
| // 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; |
| // flag to indicate if tab should be closed on back |
| private boolean mCloseOnBack; |
| // Keep the original url around to avoid killing the old WebView if the url |
| // has not changed. |
| // Error console for the tab |
| private ErrorConsoleView mErrorConsole; |
| // The listener that gets invoked when a download is started from the |
| // mMainView |
| private final BrowserDownloadListener mDownloadListener; |
| // Listener used to know when we move forward or back in the history list. |
| private final WebBackForwardListClient mWebBackForwardListClient; |
| private DataController mDataController; |
| // State of the auto-login request. |
| private DeviceAccountLogin mDeviceAccountLogin; |
| |
| // AsyncTask for downloading touch icons |
| DownloadTouchIcon mTouchIconLoader; |
| |
| private BrowserSettings mSettings; |
| private int mCaptureWidth; |
| private int mCaptureHeight; |
| private Bitmap mCapture; |
| private Handler mHandler; |
| private boolean mUpdateThumbnail; |
| |
| /** |
| * See {@link #clearBackStackWhenItemAdded(String)}. |
| */ |
| private Pattern mClearHistoryUrlPattern; |
| |
| private static synchronized Bitmap getDefaultFavicon(Context context) { |
| if (sDefaultFavicon == null) { |
| sDefaultFavicon = BitmapFactory.decodeResource( |
| context.getResources(), R.drawable.app_web_browser_sm); |
| } |
| return sDefaultFavicon; |
| } |
| |
| // All the state needed for a page |
| protected static class PageState { |
| String mUrl; |
| String mOriginalUrl; |
| String mTitle; |
| SecurityState mSecurityState; |
| // This is non-null only when mSecurityState is SECURITY_STATE_BAD_CERTIFICATE. |
| SslError mSslCertificateError; |
| Bitmap mFavicon; |
| boolean mIsBookmarkedSite; |
| boolean mIncognito; |
| |
| PageState(Context c, boolean incognito) { |
| mIncognito = incognito; |
| if (mIncognito) { |
| mOriginalUrl = mUrl = "browser:incognito"; |
| mTitle = c.getString(R.string.new_incognito_tab); |
| } else { |
| mOriginalUrl = mUrl = ""; |
| mTitle = c.getString(R.string.new_tab); |
| } |
| mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; |
| } |
| |
| PageState(Context c, boolean incognito, String url, Bitmap favicon) { |
| mIncognito = incognito; |
| mOriginalUrl = mUrl = url; |
| if (URLUtil.isHttpsUrl(url)) { |
| mSecurityState = SecurityState.SECURITY_STATE_SECURE; |
| } else { |
| mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; |
| } |
| mFavicon = favicon; |
| } |
| |
| } |
| |
| // The current/loading page's state |
| protected PageState mCurrentState; |
| |
| // Used for saving and restoring each Tab |
| static final String ID = "ID"; |
| static final String CURRURL = "currentUrl"; |
| static final String CURRTITLE = "currentTitle"; |
| static final String PARENTTAB = "parentTab"; |
| static final String APPID = "appid"; |
| static final String INCOGNITO = "privateBrowsingEnabled"; |
| static final String USERAGENT = "useragent"; |
| static final String CLOSEFLAG = "closeOnBack"; |
| |
| // 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(mContext) |
| .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 WebViewClientClassicExt mWebViewClient = new WebViewClientClassicExt() { |
| private Message mDontResend; |
| private Message mResend; |
| |
| private boolean providersDiffer(String url, String otherUrl) { |
| Uri uri1 = Uri.parse(url); |
| Uri uri2 = Uri.parse(otherUrl); |
| return !uri1.getEncodedAuthority().equals(uri2.getEncodedAuthority()); |
| } |
| |
| @Override |
| public void onPageStarted(WebView view, String url, Bitmap favicon) { |
| mInPageLoad = true; |
| mUpdateThumbnail = true; |
| mPageLoadProgress = INITIAL_PROGRESS; |
| mCurrentState = new PageState(mContext, |
| view.isPrivateBrowsingEnabled(), url, favicon); |
| mLoadStartTime = SystemClock.uptimeMillis(); |
| |
| // 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 (mWebViewController.shouldShowErrorConsole()) { |
| mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE); |
| } |
| } |
| |
| // Cancel the auto-login process. |
| if (mDeviceAccountLogin != null) { |
| mDeviceAccountLogin.cancel(); |
| mDeviceAccountLogin = null; |
| mWebViewController.hideAutoLogin(Tab.this); |
| } |
| |
| // finally update the UI in the activity if it is in the foreground |
| mWebViewController.onPageStarted(Tab.this, view, favicon); |
| |
| updateBookmarkedStatus(); |
| } |
| |
| @Override |
| public void onPageFinished(WebView view, String url) { |
| mDisableOverrideUrlLoading = false; |
| if (!isPrivateBrowsingEnabled()) { |
| LogTag.logPageFinishedLoading( |
| url, SystemClock.uptimeMillis() - mLoadStartTime); |
| } |
| syncCurrentState(view, url); |
| mWebViewController.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 (!mDisableOverrideUrlLoading && mInForeground) { |
| return mWebViewController.shouldOverrideUrlLoading(Tab.this, |
| view, url); |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Updates the security state. This method is called when we discover |
| * another resource to be loaded for this page (for example, |
| * javascript). While we update the security state, we do not update |
| * the lock icon until we are done loading, as 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 security state: |
| if (mCurrentState.mSecurityState == SecurityState.SECURITY_STATE_SECURE) { |
| // If NOT a 'safe' url, change the state to mixed content! |
| if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) |
| || URLUtil.isAboutUrl(url))) { |
| mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_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); |
| } |
| } |
| } |
| |
| /** |
| * 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(mContext).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) { |
| mWebViewController.doUpdateVisitedHistory(Tab.this, isReload); |
| } |
| |
| /** |
| * 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(); |
| setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE); |
| return; |
| } |
| if (mSettings.showSecurityWarnings()) { |
| new AlertDialog.Builder(mContext) |
| .setTitle(R.string.security_warning) |
| .setMessage(R.string.ssl_warnings_header) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .setPositiveButton(R.string.ssl_continue, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| handler.proceed(); |
| handleProceededAfterSslError(error); |
| } |
| }) |
| .setNeutralButton(R.string.view_certificate, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| mWebViewController.showSslCertificateOnError( |
| view, handler, error); |
| } |
| }) |
| .setNegativeButton(R.string.ssl_go_back, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| dialog.cancel(); |
| } |
| }) |
| .setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| handler.cancel(); |
| setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE); |
| mWebViewController.onUserCanceledSsl(Tab.this); |
| } |
| }) |
| .show(); |
| } else { |
| handler.proceed(); |
| } |
| } |
| |
| /** |
| * Called when an SSL error occurred while loading a resource, but the |
| * WebView but chose to proceed anyway based on a decision retained |
| * from a previous response to onReceivedSslError(). We update our |
| * security state to reflect this. |
| */ |
| @Override |
| public void onProceededAfterSslError(WebView view, SslError error) { |
| handleProceededAfterSslError(error); |
| } |
| |
| /** |
| * Displays client certificate request to the user. |
| */ |
| @Override |
| public void onReceivedClientCertRequest(final WebView view, |
| final ClientCertRequestHandler handler, final String host_and_port) { |
| if (!mInForeground) { |
| handler.ignore(); |
| return; |
| } |
| int colon = host_and_port.lastIndexOf(':'); |
| String host; |
| int port; |
| if (colon == -1) { |
| host = host_and_port; |
| port = -1; |
| } else { |
| String portString = host_and_port.substring(colon + 1); |
| try { |
| port = Integer.parseInt(portString); |
| host = host_and_port.substring(0, colon); |
| } catch (NumberFormatException e) { |
| host = host_and_port; |
| port = -1; |
| } |
| } |
| KeyChain.choosePrivateKeyAlias( |
| mWebViewController.getActivity(), new KeyChainAliasCallback() { |
| @Override public void alias(String alias) { |
| if (alias == null) { |
| handler.cancel(); |
| return; |
| } |
| new KeyChainLookup(mContext, handler, alias).execute(); |
| } |
| }, null, null, host, port, null); |
| } |
| |
| /** |
| * 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) { |
| mWebViewController.onReceivedHttpAuthRequest(Tab.this, view, handler, host, realm); |
| } |
| |
| @Override |
| public WebResourceResponse shouldInterceptRequest(WebView view, |
| String url) { |
| WebResourceResponse res = HomeProvider.shouldInterceptRequest( |
| mContext, url); |
| return res; |
| } |
| |
| @Override |
| public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { |
| if (!mInForeground) { |
| return false; |
| } |
| return mWebViewController.shouldOverrideKeyEvent(event); |
| } |
| |
| @Override |
| public void onUnhandledKeyEvent(WebView view, KeyEvent event) { |
| if (!mInForeground) { |
| return; |
| } |
| if (!mWebViewController.onUnhandledKeyEvent(event)) { |
| super.onUnhandledKeyEvent(view, event); |
| } |
| } |
| |
| @Override |
| public void onReceivedLoginRequest(WebView view, String realm, |
| String account, String args) { |
| new DeviceAccountLogin(mWebViewController.getActivity(), view, Tab.this, mWebViewController) |
| .handleLogin(realm, account, args); |
| } |
| |
| }; |
| |
| private void syncCurrentState(WebView view, String url) { |
| // Sync state (in case of stop/timeout) |
| mCurrentState.mUrl = view.getUrl(); |
| if (mCurrentState.mUrl == null) { |
| mCurrentState.mUrl = ""; |
| } |
| mCurrentState.mOriginalUrl = view.getOriginalUrl(); |
| mCurrentState.mTitle = view.getTitle(); |
| mCurrentState.mFavicon = view.getFavicon(); |
| if (!URLUtil.isHttpsUrl(mCurrentState.mUrl)) { |
| // In case we stop when loading an HTTPS page from an HTTP page |
| // but before a provisional load occurred |
| mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; |
| mCurrentState.mSslCertificateError = null; |
| } |
| mCurrentState.mIncognito = view.isPrivateBrowsingEnabled(); |
| } |
| |
| // Called by DeviceAccountLogin when the Tab needs to have the auto-login UI |
| // displayed. |
| void setDeviceAccountLogin(DeviceAccountLogin login) { |
| mDeviceAccountLogin = login; |
| } |
| |
| // Returns non-null if the title bar should display the auto-login UI. |
| DeviceAccountLogin getDeviceAccountLogin() { |
| return mDeviceAccountLogin; |
| } |
| |
| // ------------------------------------------------------------------------- |
| // 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(); |
| mWebViewController.attachSubWindow(Tab.this); |
| transport.setWebView(mSubView); |
| } else { |
| final Tab newTab = mWebViewController.openTab(null, |
| Tab.this, true, true); |
| 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(mContext) |
| .setTitle(R.string.too_many_subwindows_dialog_title) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .setMessage(R.string.too_many_subwindows_dialog_message) |
| .setPositiveButton(R.string.ok, null) |
| .show(); |
| return false; |
| } else if (!mWebViewController.getTabControl().canCreateNewTab()) { |
| new AlertDialog.Builder(mContext) |
| .setTitle(R.string.too_many_windows_dialog_title) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .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(mContext) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .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) { |
| mWebViewController.switchToTab(Tab.this); |
| } |
| } |
| |
| @Override |
| public void onCloseWindow(WebView window) { |
| if (mParent != null) { |
| // JavaScript can only close popup window. |
| if (mInForeground) { |
| mWebViewController.switchToTab(mParent); |
| } |
| mWebViewController.closeTab(Tab.this); |
| } |
| } |
| |
| @Override |
| public void onProgressChanged(WebView view, int newProgress) { |
| mPageLoadProgress = newProgress; |
| if (newProgress == 100) { |
| mInPageLoad = false; |
| } |
| mWebViewController.onProgressChanged(Tab.this); |
| if (mUpdateThumbnail && newProgress == 100) { |
| mUpdateThumbnail = false; |
| } |
| } |
| |
| @Override |
| public void onReceivedTitle(WebView view, final String title) { |
| mCurrentState.mTitle = title; |
| mWebViewController.onReceivedTitle(Tab.this, title); |
| } |
| |
| @Override |
| public void onReceivedIcon(WebView view, Bitmap icon) { |
| mCurrentState.mFavicon = icon; |
| mWebViewController.onFavicon(Tab.this, view, icon); |
| } |
| |
| @Override |
| public void onReceivedTouchIconUrl(WebView view, String url, |
| boolean precomposed) { |
| final ContentResolver cr = mContext.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, |
| mContext, cr, view); |
| mTouchIconLoader.execute(url); |
| } |
| } |
| |
| @Override |
| public void onShowCustomView(View view, |
| WebChromeClient.CustomViewCallback callback) { |
| Activity activity = mWebViewController.getActivity(); |
| if (activity != null) { |
| onShowCustomView(view, activity.getRequestedOrientation(), callback); |
| } |
| } |
| |
| @Override |
| public void onShowCustomView(View view, int requestedOrientation, |
| WebChromeClient.CustomViewCallback callback) { |
| if (mInForeground) mWebViewController.showCustomView(Tab.this, view, |
| requestedOrientation, callback); |
| } |
| |
| @Override |
| public void onHideCustomView() { |
| if (mInForeground) mWebViewController.hideCustomView(); |
| } |
| |
| /** |
| * 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) { |
| mSettings.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) { |
| mSettings.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 (mWebViewController.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 <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 mWebViewController.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 mWebViewController.getVideoLoadingProgressView(); |
| } |
| return null; |
| } |
| |
| @Override |
| public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { |
| if (mInForeground) { |
| mWebViewController.openFileChooser(uploadMsg, acceptType, capture); |
| } else { |
| uploadMsg.onReceiveValue(null); |
| } |
| } |
| |
| /** |
| * Deliver a list of already-visited URLs |
| */ |
| @Override |
| public void getVisitedHistory(final ValueCallback<String[]> callback) { |
| mWebViewController.getVisitedHistory(callback); |
| } |
| |
| @Override |
| public void setupAutoFill(Message message) { |
| // Prompt the user to set up their profile. |
| final Message msg = message; |
| AlertDialog.Builder builder = new AlertDialog.Builder(mContext); |
| LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| final View layout = inflater.inflate(R.layout.setup_autofill_dialog, null); |
| |
| builder.setView(layout) |
| .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int id) { |
| CheckBox disableAutoFill = (CheckBox) layout.findViewById( |
| R.id.setup_autofill_dialog_disable_autofill); |
| |
| if (disableAutoFill.isChecked()) { |
| // Disable autofill and show a toast with how to turn it on again. |
| mSettings.setAutofillEnabled(false); |
| Toast.makeText(mContext, |
| R.string.autofill_setup_dialog_negative_toast, |
| Toast.LENGTH_LONG).show(); |
| } else { |
| // Take user to the AutoFill profile editor. When they return, |
| // we will send the message that we pass here which will trigger |
| // the form to get filled out with their new profile. |
| mWebViewController.setupAutoFill(msg); |
| } |
| } |
| }) |
| .setNegativeButton(R.string.cancel, null) |
| .show(); |
| } |
| }; |
| |
| // ------------------------------------------------------------------------- |
| // 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 WebViewClientClassicExt { |
| // The main WebViewClient. |
| private final WebViewClientClassicExt mClient; |
| private final WebViewController mController; |
| |
| SubWindowClient(WebViewClientClassicExt client, WebViewController controller) { |
| mClient = client; |
| mController = controller; |
| } |
| @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. |
| mController.endActionMode(); |
| } |
| @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 onReceivedClientCertRequest(WebView view, |
| ClientCertRequestHandler handler, String host_and_port) { |
| mClient.onReceivedClientCertRequest(view, handler, host_and_port); |
| } |
| @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"); |
| } |
| mWebViewController.dismissSubWindow(Tab.this); |
| } |
| } |
| |
| // ------------------------------------------------------------------------- |
| |
| // Construct a new tab |
| Tab(WebViewController wvcontroller, WebView w) { |
| this(wvcontroller, w, null); |
| } |
| |
| Tab(WebViewController wvcontroller, Bundle state) { |
| this(wvcontroller, null, state); |
| } |
| |
| Tab(WebViewController wvcontroller, WebView w, Bundle state) { |
| mWebViewController = wvcontroller; |
| mContext = mWebViewController.getContext(); |
| mSettings = BrowserSettings.getInstance(); |
| mDataController = DataController.getInstance(mContext); |
| mCurrentState = new PageState(mContext, w != null |
| ? w.isPrivateBrowsingEnabled() : false); |
| mInPageLoad = false; |
| mInForeground = false; |
| |
| mDownloadListener = new BrowserDownloadListener() { |
| public void onDownloadStart(String url, String userAgent, |
| String contentDisposition, String mimetype, String referer, |
| long contentLength) { |
| mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition, |
| mimetype, referer, contentLength); |
| } |
| }; |
| mWebBackForwardListClient = new WebBackForwardListClient() { |
| @Override |
| public void onNewHistoryItem(WebHistoryItem item) { |
| if (mClearHistoryUrlPattern != null) { |
| boolean match = |
| mClearHistoryUrlPattern.matcher(item.getOriginalUrl()).matches(); |
| if (LOGD_ENABLED) { |
| Log.d(LOGTAG, "onNewHistoryItem: match=" + match + "\n\t" |
| + item.getUrl() + "\n\t" |
| + mClearHistoryUrlPattern); |
| } |
| if (match) { |
| if (mMainView != null) { |
| mMainView.clearHistory(); |
| } |
| } |
| mClearHistoryUrlPattern = null; |
| } |
| } |
| }; |
| |
| mCaptureWidth = mContext.getResources().getDimensionPixelSize( |
| R.dimen.tab_thumbnail_width); |
| mCaptureHeight = mContext.getResources().getDimensionPixelSize( |
| R.dimen.tab_thumbnail_height); |
| updateShouldCaptureThumbnails(); |
| restoreState(state); |
| if (getId() == -1) { |
| mId = TabControl.getNextId(); |
| } |
| setWebView(w); |
| mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message m) { |
| switch (m.what) { |
| case MSG_CAPTURE: |
| capture(); |
| break; |
| } |
| } |
| }; |
| } |
| |
| public boolean shouldUpdateThumbnail() { |
| return mUpdateThumbnail; |
| } |
| |
| /** |
| * This is used to get a new ID when the tab has been preloaded, before it is displayed and |
| * added to TabControl. Preloaded tabs can be created before restoreInstanceState, leading |
| * to overlapping IDs between the preloaded and restored tabs. |
| */ |
| public void refreshIdAfterPreload() { |
| mId = TabControl.getNextId(); |
| } |
| |
| public void updateShouldCaptureThumbnails() { |
| if (mWebViewController.shouldCaptureThumbnails()) { |
| synchronized (Tab.this) { |
| if (mCapture == null) { |
| mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight, |
| Bitmap.Config.RGB_565); |
| mCapture.eraseColor(Color.WHITE); |
| if (mInForeground) { |
| postCapture(); |
| } |
| } |
| } |
| } else { |
| synchronized (Tab.this) { |
| mCapture = null; |
| deleteThumbnail(); |
| } |
| } |
| } |
| |
| public void setController(WebViewController ctl) { |
| mWebViewController = ctl; |
| updateShouldCaptureThumbnails(); |
| } |
| |
| public long getId() { |
| return mId; |
| } |
| |
| void setWebView(WebView w) { |
| setWebView(w, true); |
| } |
| |
| /** |
| * Sets the WebView for this tab, correctly removing the old WebView from |
| * the container view. |
| */ |
| void setWebView(WebView w, boolean restore) { |
| 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(); |
| } |
| |
| mWebViewController.onSetWebView(this, w); |
| |
| if (mMainView != null) { |
| mMainView.setPictureListener(null); |
| if (w != null) { |
| syncCurrentState(w, null); |
| } else { |
| mCurrentState = new PageState(mContext, false); |
| } |
| } |
| // 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); |
| getWebViewClassic().setWebBackForwardListClient(mWebBackForwardListClient); |
| TabControl tc = mWebViewController.getTabControl(); |
| if (tc != null && tc.getOnThumbnailUpdatedListener() != null) { |
| mMainView.setPictureListener(this); |
| } |
| if (restore && (mSavedState != null)) { |
| restoreUserAgent(); |
| WebBackForwardList restoredState |
| = mMainView.restoreState(mSavedState); |
| if (restoredState == null || restoredState.getSize() == 0) { |
| Log.w(LOGTAG, "Failed to restore WebView state!"); |
| loadUrl(mCurrentState.mOriginalUrl, null); |
| } |
| mSavedState = null; |
| } |
| } |
| } |
| |
| /** |
| * Destroy the tab's main WebView and subWindow if any |
| */ |
| void destroy() { |
| if (mMainView != null) { |
| dismissSubWindow(); |
| // 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 (mChildren != null) { |
| for(Tab t : mChildren) { |
| t.setParent(null); |
| } |
| } |
| // remove itself from the parent list |
| if (mParent != null) { |
| mParent.mChildren.remove(this); |
| } |
| deleteThumbnail(); |
| } |
| |
| /** |
| * 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) { |
| mWebViewController.createSubWindow(this); |
| mSubView.setWebViewClient(new SubWindowClient(mWebViewClient, |
| mWebViewController)); |
| 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 BrowserDownloadListener() { |
| public void onDownloadStart(String url, String userAgent, |
| String contentDisposition, String mimetype, String referer, |
| long contentLength) { |
| mWebViewController.onDownloadStart(Tab.this, url, userAgent, |
| contentDisposition, mimetype, referer, contentLength); |
| if (mSubView.copyBackForwardList().getSize() == 0) { |
| // This subwindow was opened for the sole purpose of |
| // downloading a file. Remove it. |
| mWebViewController.dismissSubWindow(Tab.this); |
| } |
| } |
| }); |
| mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity()); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Dismiss the subWindow for the tab. |
| */ |
| void dismissSubWindow() { |
| if (mSubView != null) { |
| mWebViewController.endActionMode(); |
| mSubView.destroy(); |
| mSubView = null; |
| mSubViewContainer = null; |
| } |
| } |
| |
| |
| /** |
| * Set the parent tab of this tab. |
| */ |
| void setParent(Tab parent) { |
| if (parent == this) { |
| throw new IllegalStateException("Cannot set parent to self!"); |
| } |
| mParent = parent; |
| // This tab may have been freed due to low memory. If that is the case, |
| // the parent tab id is already saved. If we are changing that id |
| // (most likely due to removing the parent tab) we must update the |
| // parent tab id in the saved Bundle. |
| if (mSavedState != null) { |
| if (parent == null) { |
| mSavedState.remove(PARENTTAB); |
| } else { |
| mSavedState.putLong(PARENTTAB, parent.getId()); |
| } |
| } |
| |
| // Sync the WebView useragent with the parent |
| if (parent != null && mSettings.hasDesktopUseragent(parent.getWebView()) |
| != mSettings.hasDesktopUseragent(getWebView())) { |
| mSettings.toggleDesktopUseragent(getWebView()); |
| } |
| |
| if (parent != null && parent.getId() == getId()) { |
| throw new IllegalStateException("Parent has same ID as child!"); |
| } |
| } |
| |
| /** |
| * If this Tab was created through another Tab, then this method returns |
| * that Tab. |
| * @return the Tab parent or null |
| */ |
| public Tab getParent() { |
| return mParent; |
| } |
| |
| /** |
| * 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 (mChildren == null) { |
| mChildren = new Vector<Tab>(); |
| } |
| mChildren.add(child); |
| child.setParent(this); |
| } |
| |
| Vector<Tab> getChildren() { |
| return mChildren; |
| } |
| |
| void resume() { |
| if (mMainView != null) { |
| setupHwAcceleration(mMainView); |
| mMainView.onResume(); |
| if (mSubView != null) { |
| mSubView.onResume(); |
| } |
| } |
| } |
| |
| private void setupHwAcceleration(View web) { |
| if (web == null) return; |
| BrowserSettings settings = BrowserSettings.getInstance(); |
| if (settings.isHardwareAccelerated()) { |
| web.setLayerType(View.LAYER_TYPE_NONE, null); |
| } else { |
| web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); |
| } |
| } |
| |
| void pause() { |
| if (mMainView != null) { |
| mMainView.onPause(); |
| if (mSubView != null) { |
| mSubView.onPause(); |
| } |
| } |
| } |
| |
| void putInForeground() { |
| if (mInForeground) { |
| return; |
| } |
| mInForeground = true; |
| resume(); |
| Activity activity = mWebViewController.getActivity(); |
| mMainView.setOnCreateContextMenuListener(activity); |
| if (mSubView != null) { |
| mSubView.setOnCreateContextMenuListener(activity); |
| } |
| // Show the pending error dialog if the queue is not empty |
| if (mQueuedErrors != null && mQueuedErrors.size() > 0) { |
| showError(mQueuedErrors.getFirst()); |
| } |
| mWebViewController.bookmarkedStatusHasChanged(this); |
| } |
| |
| void putInBackground() { |
| if (!mInForeground) { |
| return; |
| } |
| capture(); |
| mInForeground = false; |
| pause(); |
| mMainView.setOnCreateContextMenuListener(null); |
| if (mSubView != null) { |
| mSubView.setOnCreateContextMenuListener(null); |
| } |
| } |
| |
| boolean inForeground() { |
| return mInForeground; |
| } |
| |
| /** |
| * 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 underlying WebViewClassic implementation. As with getWebView, |
| * this maybe null for background tabs. |
| * @return The main WebView of this tab. |
| */ |
| WebViewClassic getWebViewClassic() { |
| return WebViewClassic.fromWebView(mMainView); |
| } |
| |
| void setViewContainer(View container) { |
| mContainer = container; |
| } |
| |
| View getViewContainer() { |
| return mContainer; |
| } |
| |
| /** |
| * Return whether private browsing is enabled for the main window of |
| * this tab. |
| * @return True if private browsing is enabled. |
| */ |
| boolean isPrivateBrowsingEnabled() { |
| return mCurrentState.mIncognito; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| void setSubWebView(WebView subView) { |
| mSubView = subView; |
| } |
| |
| View getSubViewContainer() { |
| return mSubViewContainer; |
| } |
| |
| void setSubViewContainer(View subViewContainer) { |
| mSubViewContainer = subViewContainer; |
| } |
| |
| /** |
| * @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(); |
| } |
| return mGeolocationPermissionsPrompt; |
| } |
| |
| /** |
| * @return The application id string |
| */ |
| String getAppId() { |
| return mAppId; |
| } |
| |
| /** |
| * Set the application id string |
| * @param id |
| */ |
| void setAppId(String id) { |
| mAppId = id; |
| } |
| |
| boolean closeOnBack() { |
| return mCloseOnBack; |
| } |
| |
| void setCloseOnBack(boolean close) { |
| mCloseOnBack = close; |
| } |
| |
| String getUrl() { |
| return UrlUtils.filteredUrl(mCurrentState.mUrl); |
| } |
| |
| String getOriginalUrl() { |
| if (mCurrentState.mOriginalUrl == null) { |
| return getUrl(); |
| } |
| return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl); |
| } |
| |
| /** |
| * Get the title of this tab. |
| */ |
| String getTitle() { |
| if (mCurrentState.mTitle == null && mInPageLoad) { |
| return mContext.getString(R.string.title_bar_loading); |
| } |
| return mCurrentState.mTitle; |
| } |
| |
| /** |
| * Get the favicon of this tab. |
| */ |
| Bitmap getFavicon() { |
| if (mCurrentState.mFavicon != null) { |
| return mCurrentState.mFavicon; |
| } |
| return getDefaultFavicon(mContext); |
| } |
| |
| public boolean isBookmarkedSite() { |
| return mCurrentState.mIsBookmarkedSite; |
| } |
| |
| /** |
| * 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(mContext); |
| mErrorConsole.setWebView(mMainView); |
| } |
| return mErrorConsole; |
| } |
| |
| /** |
| * Sets the security state, clears the SSL certificate error and informs |
| * the controller. |
| */ |
| private void setSecurityState(SecurityState securityState) { |
| mCurrentState.mSecurityState = securityState; |
| mCurrentState.mSslCertificateError = null; |
| mWebViewController.onUpdatedSecurityState(this); |
| } |
| |
| /** |
| * @return The tab's security state. |
| */ |
| SecurityState getSecurityState() { |
| return mCurrentState.mSecurityState; |
| } |
| |
| /** |
| * Gets the SSL certificate error, if any, for the page's main resource. |
| * This is only non-null when the security state is |
| * SECURITY_STATE_BAD_CERTIFICATE. |
| */ |
| SslError getSslCertificateError() { |
| return mCurrentState.mSslCertificateError; |
| } |
| |
| int getLoadProgress() { |
| if (mInPageLoad) { |
| return mPageLoadProgress; |
| } |
| return 100; |
| } |
| |
| /** |
| * @return TRUE if onPageStarted is called while onPageFinished is not |
| * called yet. |
| */ |
| boolean inPageLoad() { |
| return mInPageLoad; |
| } |
| |
| /** |
| * @return The Bundle with the tab's state if it can be saved, otherwise null |
| */ |
| public Bundle 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; |
| } |
| |
| if (TextUtils.isEmpty(mCurrentState.mUrl)) { |
| return null; |
| } |
| |
| mSavedState = new Bundle(); |
| WebBackForwardList savedList = mMainView.saveState(mSavedState); |
| if (savedList == null || savedList.getSize() == 0) { |
| Log.w(LOGTAG, "Failed to save back/forward list for " |
| + mCurrentState.mUrl); |
| } |
| |
| mSavedState.putLong(ID, mId); |
| mSavedState.putString(CURRURL, mCurrentState.mUrl); |
| mSavedState.putString(CURRTITLE, mCurrentState.mTitle); |
| mSavedState.putBoolean(INCOGNITO, mMainView.isPrivateBrowsingEnabled()); |
| if (mAppId != null) { |
| mSavedState.putString(APPID, mAppId); |
| } |
| mSavedState.putBoolean(CLOSEFLAG, mCloseOnBack); |
| // Remember the parent tab so the relationship can be restored. |
| if (mParent != null) { |
| mSavedState.putLong(PARENTTAB, mParent.mId); |
| } |
| mSavedState.putBoolean(USERAGENT, |
| mSettings.hasDesktopUseragent(getWebView())); |
| return mSavedState; |
| } |
| |
| /* |
| * Restore the state of the tab. |
| */ |
| private void restoreState(Bundle b) { |
| mSavedState = b; |
| if (mSavedState == null) { |
| return; |
| } |
| // Restore the internal state even if the WebView fails to restore. |
| // This will maintain the app id, original url and close-on-exit values. |
| mId = b.getLong(ID); |
| mAppId = b.getString(APPID); |
| mCloseOnBack = b.getBoolean(CLOSEFLAG); |
| restoreUserAgent(); |
| String url = b.getString(CURRURL); |
| String title = b.getString(CURRTITLE); |
| boolean incognito = b.getBoolean(INCOGNITO); |
| mCurrentState = new PageState(mContext, incognito, url, null); |
| mCurrentState.mTitle = title; |
| synchronized (Tab.this) { |
| if (mCapture != null) { |
| DataController.getInstance(mContext).loadThumbnail(this); |
| } |
| } |
| } |
| |
| private void restoreUserAgent() { |
| if (mMainView == null || mSavedState == null) { |
| return; |
| } |
| if (mSavedState.getBoolean(USERAGENT) |
| != mSettings.hasDesktopUseragent(mMainView)) { |
| mSettings.toggleDesktopUseragent(mMainView); |
| } |
| } |
| |
| public void updateBookmarkedStatus() { |
| mDataController.queryBookmarkStatus(getUrl(), mIsBookmarkCallback); |
| } |
| |
| private DataController.OnQueryUrlIsBookmark mIsBookmarkCallback |
| = new DataController.OnQueryUrlIsBookmark() { |
| @Override |
| public void onQueryUrlIsBookmark(String url, boolean isBookmark) { |
| if (mCurrentState.mUrl.equals(url)) { |
| mCurrentState.mIsBookmarkedSite = isBookmark; |
| mWebViewController.bookmarkedStatusHasChanged(Tab.this); |
| } |
| } |
| }; |
| |
| public Bitmap getScreenshot() { |
| synchronized (Tab.this) { |
| return mCapture; |
| } |
| } |
| |
| public boolean isSnapshot() { |
| return false; |
| } |
| |
| private static class SaveCallback implements ValueCallback<Boolean> { |
| boolean mResult; |
| |
| @Override |
| public void onReceiveValue(Boolean value) { |
| mResult = value; |
| synchronized (this) { |
| notifyAll(); |
| } |
| } |
| |
| } |
| |
| /** |
| * Must be called on the UI thread |
| */ |
| public ContentValues createSnapshotValues() { |
| WebViewClassic web = getWebViewClassic(); |
| if (web == null) return null; |
| ContentValues values = new ContentValues(); |
| values.put(Snapshots.TITLE, mCurrentState.mTitle); |
| values.put(Snapshots.URL, mCurrentState.mUrl); |
| values.put(Snapshots.BACKGROUND, web.getPageBackgroundColor()); |
| values.put(Snapshots.DATE_CREATED, System.currentTimeMillis()); |
| values.put(Snapshots.FAVICON, compressBitmap(getFavicon())); |
| Bitmap screenshot = Controller.createScreenshot(mMainView, |
| Controller.getDesiredThumbnailWidth(mContext), |
| Controller.getDesiredThumbnailHeight(mContext)); |
| values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot)); |
| return values; |
| } |
| |
| /** |
| * Probably want to call this on a background thread |
| */ |
| public boolean saveViewState(ContentValues values) { |
| WebViewClassic web = getWebViewClassic(); |
| if (web == null) return false; |
| String path = UUID.randomUUID().toString(); |
| SaveCallback callback = new SaveCallback(); |
| OutputStream outs = null; |
| try { |
| outs = mContext.openFileOutput(path, Context.MODE_PRIVATE); |
| GZIPOutputStream stream = new GZIPOutputStream(outs); |
| synchronized (callback) { |
| web.saveViewState(stream, callback); |
| callback.wait(); |
| } |
| stream.flush(); |
| stream.close(); |
| } catch (Exception e) { |
| Log.w(LOGTAG, "Failed to save view state", e); |
| if (outs != null) { |
| try { |
| outs.close(); |
| } catch (IOException ignore) {} |
| } |
| File file = mContext.getFileStreamPath(path); |
| if (file.exists() && !file.delete()) { |
| file.deleteOnExit(); |
| } |
| return false; |
| } |
| File savedFile = mContext.getFileStreamPath(path); |
| if (!callback.mResult) { |
| if (!savedFile.delete()) { |
| savedFile.deleteOnExit(); |
| } |
| return false; |
| } |
| long size = savedFile.length(); |
| values.put(Snapshots.VIEWSTATE_PATH, path); |
| values.put(Snapshots.VIEWSTATE_SIZE, size); |
| return true; |
| } |
| |
| public byte[] compressBitmap(Bitmap bitmap) { |
| if (bitmap == null) { |
| return null; |
| } |
| ByteArrayOutputStream stream = new ByteArrayOutputStream(); |
| bitmap.compress(CompressFormat.PNG, 100, stream); |
| return stream.toByteArray(); |
| } |
| |
| public void loadUrl(String url, Map<String, String> headers) { |
| if (mMainView != null) { |
| mPageLoadProgress = INITIAL_PROGRESS; |
| mInPageLoad = true; |
| mCurrentState = new PageState(mContext, false, url, null); |
| mWebViewController.onPageStarted(this, mMainView, null); |
| mMainView.loadUrl(url, headers); |
| } |
| } |
| |
| public void disableUrlOverridingForLoad() { |
| mDisableOverrideUrlLoading = true; |
| } |
| |
| protected void capture() { |
| if (mMainView == null || mCapture == null) return; |
| if (mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0) { |
| return; |
| } |
| Canvas c = new Canvas(mCapture); |
| final int left = mMainView.getScrollX(); |
| final int top = mMainView.getScrollY() + mMainView.getVisibleTitleHeight(); |
| int state = c.save(); |
| c.translate(-left, -top); |
| float scale = mCaptureWidth / (float) mMainView.getWidth(); |
| c.scale(scale, scale, left, top); |
| if (mMainView instanceof BrowserWebView) { |
| ((BrowserWebView)mMainView).drawContent(c); |
| } else { |
| mMainView.draw(c); |
| } |
| c.restoreToCount(state); |
| // manually anti-alias the edges for the tilt |
| c.drawRect(0, 0, 1, mCapture.getHeight(), sAlphaPaint); |
| c.drawRect(mCapture.getWidth() - 1, 0, mCapture.getWidth(), |
| mCapture.getHeight(), sAlphaPaint); |
| c.drawRect(0, 0, mCapture.getWidth(), 1, sAlphaPaint); |
| c.drawRect(0, mCapture.getHeight() - 1, mCapture.getWidth(), |
| mCapture.getHeight(), sAlphaPaint); |
| c.setBitmap(null); |
| mHandler.removeMessages(MSG_CAPTURE); |
| persistThumbnail(); |
| TabControl tc = mWebViewController.getTabControl(); |
| if (tc != null) { |
| OnThumbnailUpdatedListener updateListener |
| = tc.getOnThumbnailUpdatedListener(); |
| if (updateListener != null) { |
| updateListener.onThumbnailUpdated(this); |
| } |
| } |
| } |
| |
| @Override |
| public void onNewPicture(WebView view, Picture picture) { |
| postCapture(); |
| } |
| |
| private void postCapture() { |
| if (!mHandler.hasMessages(MSG_CAPTURE)) { |
| mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY); |
| } |
| } |
| |
| public boolean canGoBack() { |
| return mMainView != null ? mMainView.canGoBack() : false; |
| } |
| |
| public boolean canGoForward() { |
| return mMainView != null ? mMainView.canGoForward() : false; |
| } |
| |
| public void goBack() { |
| if (mMainView != null) { |
| mMainView.goBack(); |
| } |
| } |
| |
| public void goForward() { |
| if (mMainView != null) { |
| mMainView.goForward(); |
| } |
| } |
| |
| /** |
| * Causes the tab back/forward stack to be cleared once, if the given URL is the next URL |
| * to be added to the stack. |
| * |
| * This is used to ensure that preloaded URLs that are not subsequently seen by the user do |
| * not appear in the back stack. |
| */ |
| public void clearBackStackWhenItemAdded(Pattern urlPattern) { |
| mClearHistoryUrlPattern = urlPattern; |
| } |
| |
| protected void persistThumbnail() { |
| DataController.getInstance(mContext).saveThumbnail(this); |
| } |
| |
| protected void deleteThumbnail() { |
| DataController.getInstance(mContext).deleteThumbnail(this); |
| } |
| |
| void updateCaptureFromBlob(byte[] blob) { |
| synchronized (Tab.this) { |
| if (mCapture == null) { |
| return; |
| } |
| ByteBuffer buffer = ByteBuffer.wrap(blob); |
| try { |
| mCapture.copyPixelsFromBuffer(buffer); |
| } catch (RuntimeException rex) { |
| Log.e(LOGTAG, "Load capture has mismatched sizes; buffer: " |
| + buffer.capacity() + " blob: " + blob.length |
| + "capture: " + mCapture.getByteCount()); |
| throw rex; |
| } |
| } |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(100); |
| builder.append(mId); |
| builder.append(") has parent: "); |
| if (getParent() != null) { |
| builder.append("true["); |
| builder.append(getParent().getId()); |
| builder.append("]"); |
| } else { |
| builder.append("false"); |
| } |
| builder.append(", incog: "); |
| builder.append(isPrivateBrowsingEnabled()); |
| if (!isPrivateBrowsingEnabled()) { |
| builder.append(", title: "); |
| builder.append(getTitle()); |
| builder.append(", url: "); |
| builder.append(getUrl()); |
| } |
| return builder.toString(); |
| } |
| |
| private void handleProceededAfterSslError(SslError error) { |
| if (error.getUrl().equals(mCurrentState.mUrl)) { |
| // The security state should currently be SECURITY_STATE_SECURE. |
| setSecurityState(SecurityState.SECURITY_STATE_BAD_CERTIFICATE); |
| mCurrentState.mSslCertificateError = error; |
| } else if (getSecurityState() == SecurityState.SECURITY_STATE_SECURE) { |
| // The page's main resource is secure and this error is for a |
| // sub-resource. |
| setSecurityState(SecurityState.SECURITY_STATE_MIXED); |
| } |
| } |
| } |