// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview;
import android.annotation.SuppressLint;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Picture;
import android.graphics.Rect;
import android.net.http.SslCertificate;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.webkit.GeolocationPermissions;
import android.webkit.ValueCallback;
import com.google.common.annotations.VisibleForTesting;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.ThreadUtils;
import org.chromium.content.browser.ContentSettings;
import org.chromium.content.browser.ContentVideoView;
import org.chromium.content.browser.ContentViewClient;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.ContentViewStatics;
import org.chromium.content.browser.LoadUrlParams;
import org.chromium.content.browser.NavigationHistory;
import org.chromium.content.browser.PageTransitionTypes;
import org.chromium.content.common.CleanupReference;
import org.chromium.components.navigation_interception.InterceptNavigationDelegate;
import org.chromium.components.navigation_interception.NavigationParams;
import org.chromium.net.GURLUtils;
import org.chromium.ui.gfx.DeviceDisplayInfo;
import java.io.File;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URL;
/**
* Exposes the native AwContents class, and together these classes wrap the ContentViewCore
* and Browser components that are required to implement Android WebView API. This is the
* primary entry point for the WebViewProvider implementation; it holds a 1:1 object
* relationship with application WebView instances.
* (We define this class independent of the hidden WebViewProvider interfaces, to allow
* continuous build & test in the open source SDK-based tree).
*/
@JNINamespace("android_webview")
public class AwContents {
private static final String TAG = AwContents.class.getSimpleName();
private static final String WEB_ARCHIVE_EXTENSION = ".mht";
/**
* WebKit hit test related data strcutre. These are used to implement
* getHitTestResult, requestFocusNodeHref, requestImageRef methods in WebView.
* All values should be updated together. The native counterpart is
* AwHitTestData.
*/
public static class HitTestData {
// Used in getHitTestResult.
public int hitTestResultType;
public String hitTestResultExtraData;
// Used in requestFocusNodeHref (all three) and requestImageRef (only imgSrc).
public String href;
public String anchorText;
public String imgSrc;
}
/**
* Interface that consumers of {@link AwContents} must implement to allow the proper
* dispatching of view methods through the containing view.
*/
public interface InternalAccessDelegate extends ContentViewCore.InternalAccessDelegate {
/**
* @see View#setMeasuredDimension(int, int)
*/
void setMeasuredDimension(int measuredWidth, int measuredHeight);
/**
* Requests a callback on the native DrawGL method (see getAwDrawGLFunction)
* if called from within onDraw, |canvas| will be non-null and hardware accelerated.
* otherwise, |canvas| will be null, and the container view itself will be hardware
* accelerated.
*
* @return false indicates the GL draw request was not accepted, and the caller
* should fallback to the SW path.
*/
boolean requestDrawGL(Canvas canvas);
}
private int mNativeAwContents;
private AwBrowserContext mBrowserContext;
private ViewGroup mContainerView;
private ContentViewCore mContentViewCore;
private AwContentsClient mContentsClient;
private AwContentsClientBridge mContentsClientBridge;
private AwWebContentsDelegate mWebContentsDelegate;
private AwContentsIoThreadClient mIoThreadClient;
private InterceptNavigationDelegateImpl mInterceptNavigationDelegate;
private InternalAccessDelegate mInternalAccessAdapter;
private final AwLayoutSizer mLayoutSizer;
private AwZoomControls mZoomControls;
// This can be accessed on any thread after construction. See AwContentsIoThreadClient.
private final AwSettings mSettings;
private boolean mIsPaused;
private Bitmap mFavicon;
private boolean mHasRequestedVisitedHistoryFromClient;
// TODO(boliu): This should be in a global context, not per webview.
private final double mDIPScale;
// Must call nativeUpdateLastHitTestData first to update this before use.
private final HitTestData mPossiblyStaleHitTestData;
private DefaultVideoPosterRequestHandler mDefaultVideoPosterRequestHandler;
private boolean mNewPictureInvalidationOnly;
private Rect mGlobalVisibleBounds;
private int mLastGlobalVisibleWidth;
private int mLastGlobalVisibleHeight;
private boolean mContainerViewFocused;
private boolean mWindowFocused;
private static final class DestroyRunnable implements Runnable {
private int mNativeAwContents;
private DestroyRunnable(int nativeAwContents) {
mNativeAwContents = nativeAwContents;
}
@Override
public void run() {
nativeDestroy(mNativeAwContents);
}
}
private CleanupReference mCleanupReference;
//--------------------------------------------------------------------------------------------
private class IoThreadClientImpl implements AwContentsIoThreadClient {
// All methods are called on the IO thread.
@Override
public int getCacheMode() {
return mSettings.getCacheMode();
}
@Override
public InterceptedRequestData shouldInterceptRequest(final String url,
boolean isMainFrame) {
InterceptedRequestData interceptedRequestData;
// Return the response directly if the url is default video poster url.
interceptedRequestData = mDefaultVideoPosterRequestHandler.shouldInterceptRequest(url);
if (interceptedRequestData != null) return interceptedRequestData;
interceptedRequestData = mContentsClient.shouldInterceptRequest(url);
if (interceptedRequestData == null) {
mContentsClient.getCallbackHelper().postOnLoadResource(url);
}
if (isMainFrame && interceptedRequestData != null &&
interceptedRequestData.getData() == null) {
// In this case the intercepted URLRequest job will simulate an empty response
// which doesn't trigger the onReceivedError callback. For WebViewClassic
// compatibility we synthesize that callback. http://crbug.com/180950
mContentsClient.getCallbackHelper().postOnReceivedError(
ErrorCodeConversionHelper.ERROR_UNKNOWN,
null /* filled in by the glue layer */, url);
}
return interceptedRequestData;
}
@Override
public boolean shouldBlockContentUrls() {
return !mSettings.getAllowContentAccess();
}
@Override
public boolean shouldBlockFileUrls() {
return !mSettings.getAllowFileAccess();
}
@Override
public boolean shouldBlockNetworkLoads() {
return mSettings.getBlockNetworkLoads();
}
@Override
public void onDownloadStart(String url,
String userAgent,
String contentDisposition,
String mimeType,
long contentLength) {
mContentsClient.getCallbackHelper().postOnDownloadStart(url, userAgent,
contentDisposition, mimeType, contentLength);
}
@Override
public void newLoginRequest(String realm, String account, String args) {
mContentsClient.getCallbackHelper().postOnReceivedLoginRequest(realm, account, args);
}
}
//--------------------------------------------------------------------------------------------
private class InterceptNavigationDelegateImpl implements InterceptNavigationDelegate {
private String mLastLoadUrlAddress;
public void onUrlLoadRequested(String url) {
mLastLoadUrlAddress = url;
}
@Override
public boolean shouldIgnoreNavigation(NavigationParams navigationParams) {
final String url = navigationParams.url;
boolean ignoreNavigation = false;
if (mLastLoadUrlAddress != null && mLastLoadUrlAddress.equals(url)) {
// Support the case where the user clicks on a link that takes them back to the
// same page.
mLastLoadUrlAddress = null;
// If the embedder requested the load of a certain URL via the loadUrl API, then we
// do not offer it to AwContentsClient.shouldOverrideUrlLoading.
// The embedder is also not allowed to intercept POST requests because of
// crbug.com/155250.
} else if (!navigationParams.isPost) {
ignoreNavigation = mContentsClient.shouldOverrideUrlLoading(url);
}
// The existing contract is that shouldOverrideUrlLoading callbacks are delivered before
// onPageStarted callbacks; third party apps depend on this behavior.
// Using a ResouceThrottle to implement the navigation interception feature results in
// the WebContentsObserver.didStartLoading callback happening before the
// ResourceThrottle has a chance to run.
// To preserve the ordering the onPageStarted callback is synthesized from the
// shouldOverrideUrlLoading, and only if the navigation was not ignored (this
// balances out with the onPageFinished callback, which is suppressed in the
// AwContentsClient if the navigation was ignored).
if (!ignoreNavigation) {
// The shouldOverrideUrlLoading call might have resulted in posting messages to the
// UI thread. Using sendMessage here (instead of calling onPageStarted directly)
// will allow those to run in order.
mContentsClient.getCallbackHelper().postOnPageStarted(url);
}
return ignoreNavigation;
}
}
//--------------------------------------------------------------------------------------------
private class AwLayoutSizerDelegate implements AwLayoutSizer.Delegate {
@Override
public void requestLayout() {
mContainerView.requestLayout();
}
@Override
public void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mInternalAccessAdapter.setMeasuredDimension(measuredWidth, measuredHeight);
}
}
//--------------------------------------------------------------------------------------------
private class AwPinchGestureStateListener implements ContentViewCore.PinchGestureStateListener {
@Override
public void onPinchGestureStart() {
// While it's possible to re-layout the view during a pinch gesture, the effect is very
// janky (especially that the page scale update notification comes from the renderer
// main thread, not from the impl thread, so it's usually out of sync with what's on
// screen). It's also quite expensive to do a re-layout, so we simply postpone
// re-layout for the duration of the gesture. This is compatible with what
// WebViewClassic does.
mLayoutSizer.freezeLayoutRequests();
}
public void onPinchGestureEnd() {
mLayoutSizer.unfreezeLayoutRequests();
}
}
//--------------------------------------------------------------------------------------------
private class ScrollChangeListener implements ViewTreeObserver.OnScrollChangedListener {
@Override
public void onScrollChanged() {
// We do this to cover the case that when the view hierarchy is scrolled,
// more of the containing view becomes visible (i.e. a containing view
// with a width/height of "wrap_content" and dimensions greater than
// that of the screen).
AwContents.this.updatePhysicalBackingSizeIfNeeded();
}
};
private ScrollChangeListener mScrollChangeListener;
/**
* @param browserContext the browsing context to associate this view contents with.
* @param containerView the view-hierarchy item this object will be bound to.
* @param internalAccessAdapter to access private methods on containerView.
* @param contentsClient will receive API callbacks from this WebView Contents
* @param isAccessFromFileURLsGrantedByDefault passed to AwSettings.
*
* This constructor uses the default view sizing policy.
*/
public AwContents(AwBrowserContext browserContext, ViewGroup containerView,
InternalAccessDelegate internalAccessAdapter, AwContentsClient contentsClient,
boolean isAccessFromFileURLsGrantedByDefault) {
this(browserContext, containerView, internalAccessAdapter, contentsClient,
isAccessFromFileURLsGrantedByDefault, new AwLayoutSizer());
}
private static ContentViewCore createAndInitializeContentViewCore(ViewGroup containerView,
InternalAccessDelegate internalDispatcher, int nativeWebContents,
ContentViewCore.PinchGestureStateListener pinchGestureStateListener,
ContentViewClient contentViewClient,
ContentViewCore.ZoomControlsDelegate zoomControlsDelegate) {
ContentViewCore contentViewCore = new ContentViewCore(containerView.getContext());
// Note INPUT_EVENTS_DELIVERED_IMMEDIATELY is passed to avoid triggering vsync in the
// compositor, not because input events are delivered immediately.
contentViewCore.initialize(containerView, internalDispatcher, nativeWebContents, null,
ContentViewCore.INPUT_EVENTS_DELIVERED_IMMEDIATELY);
contentViewCore.setPinchGestureStateListener(pinchGestureStateListener);
contentViewCore.setContentViewClient(contentViewClient);
contentViewCore.setZoomControlsDelegate(zoomControlsDelegate);
return contentViewCore;
}
/**
* @param layoutSizer the AwLayoutSizer instance implementing the sizing policy for the view.
*
* This version of the constructor is used in test code to inject test versions of the above
* documented classes
*/
public AwContents(AwBrowserContext browserContext, ViewGroup containerView,
InternalAccessDelegate internalAccessAdapter, AwContentsClient contentsClient,
boolean isAccessFromFileURLsGrantedByDefault, AwLayoutSizer layoutSizer) {
mBrowserContext = browserContext;
mContainerView = containerView;
mInternalAccessAdapter = internalAccessAdapter;
mDIPScale = DeviceDisplayInfo.create(containerView.getContext()).getDIPScale();
// Note that ContentViewCore must be set up before AwContents, as ContentViewCore
// setup performs process initialisation work needed by AwContents.
mContentsClientBridge = new AwContentsClientBridge(contentsClient);
mLayoutSizer = layoutSizer;
mLayoutSizer.setDelegate(new AwLayoutSizerDelegate());
mLayoutSizer.setDIPScale(mDIPScale);
mWebContentsDelegate = new AwWebContentsDelegateAdapter(contentsClient,
mLayoutSizer.getPreferredSizeChangedListener());
mNativeAwContents = nativeInit(mWebContentsDelegate, mContentsClientBridge);
mContentsClient = contentsClient;
mCleanupReference = new CleanupReference(this, new DestroyRunnable(mNativeAwContents));
int nativeWebContents = nativeGetWebContents(mNativeAwContents);
mZoomControls = new AwZoomControls(this);
mContentViewCore = createAndInitializeContentViewCore(
containerView, internalAccessAdapter, nativeWebContents,
new AwPinchGestureStateListener(), mContentsClient.getContentViewClient(),
mZoomControls);
mContentsClient.installWebContentsObserver(mContentViewCore);
mSettings = new AwSettings(mContentViewCore.getContext(), nativeWebContents,
mContentViewCore, isAccessFromFileURLsGrantedByDefault);
setIoThreadClient(new IoThreadClientImpl());
setInterceptNavigationDelegate(new InterceptNavigationDelegateImpl());
mPossiblyStaleHitTestData = new HitTestData();
nativeDidInitializeContentViewCore(mNativeAwContents,
mContentViewCore.getNativeContentViewCore());
mContentsClient.setDIPScale(mDIPScale);
mSettings.setDIPScale(mDIPScale);
mDefaultVideoPosterRequestHandler = new DefaultVideoPosterRequestHandler(mContentsClient);
mSettings.setDefaultVideoPosterURL(
mDefaultVideoPosterRequestHandler.getDefaultVideoPosterURL());
ContentVideoView.registerContentVideoViewContextDelegate(
new AwContentVideoViewDelegate(contentsClient, containerView.getContext()));
mGlobalVisibleBounds = new Rect();
}
private void updatePhysicalBackingSizeIfNeeded() {
// We musn't let the physical backing size get too big, otherwise we
// will try to allocate a SurfaceTexture beyond what the GL driver can
// cope with. In most cases, limiting the SurfaceTexture size to that
// of the visible bounds of the WebView will be good enough i.e. the maximum
// SurfaceTexture dimensions will match the screen dimensions).
mContainerView.getGlobalVisibleRect(mGlobalVisibleBounds);
int width = mGlobalVisibleBounds.width();
int height = mGlobalVisibleBounds.height();
if (width != mLastGlobalVisibleWidth || height != mLastGlobalVisibleHeight) {
mLastGlobalVisibleWidth = width;
mLastGlobalVisibleHeight = height;
mContentViewCore.onPhysicalBackingSizeChanged(width, height);
}
}
@VisibleForTesting
public ContentViewCore getContentViewCore() {
return mContentViewCore;
}
// Can be called from any thread.
public AwSettings getSettings() {
return mSettings;
}
public void setIoThreadClient(AwContentsIoThreadClient ioThreadClient) {
mIoThreadClient = ioThreadClient;
nativeSetIoThreadClient(mNativeAwContents, mIoThreadClient);
}
private void setInterceptNavigationDelegate(InterceptNavigationDelegateImpl delegate) {
mInterceptNavigationDelegate = delegate;
nativeSetInterceptNavigationDelegate(mNativeAwContents, delegate);
}
public void destroy() {
mContentViewCore.destroy();
// The native part of AwSettings isn't needed for the IoThreadClient instance.
mSettings.destroy();
// We explicitly do not null out the mContentViewCore reference here
// because ContentViewCore already has code to deal with the case
// methods are called on it after it's been destroyed, and other
// code relies on AwContents.mContentViewCore to be non-null.
mCleanupReference.cleanupNow();
mNativeAwContents = 0;
}
public static void setAwDrawSWFunctionTable(int functionTablePointer) {
nativeSetAwDrawSWFunctionTable(functionTablePointer);
}
public static void setAwDrawGLFunctionTable(int functionTablePointer) {
nativeSetAwDrawGLFunctionTable(functionTablePointer);
}
public static int getAwDrawGLFunction() {
return nativeGetAwDrawGLFunction();
}
public int getAwDrawGLViewContext() {
// Using the native pointer as the returned viewContext. This is matched by the
// reinterpret_cast back to BrowserViewRenderer pointer in the native DrawGLFunction.
return nativeGetAwDrawGLViewContext(mNativeAwContents);
}
public void onDraw(Canvas canvas) {
if (mNativeAwContents == 0) return;
if (canvas.isHardwareAccelerated() &&
nativePrepareDrawGL(mNativeAwContents,
mContainerView.getScrollX(), mContainerView.getScrollY()) &&
mInternalAccessAdapter.requestDrawGL(canvas)) {
return;
}
Rect clip = canvas.getClipBounds();
if (!nativeDrawSW(mNativeAwContents, canvas, clip.left, clip.top,
clip.right - clip.left, clip.bottom - clip.top)) {
Log.w(TAG, "Native DrawSW failed; clearing to background color.");
int c = mContentViewCore.getBackgroundColor();
canvas.drawRGB(Color.red(c), Color.green(c), Color.blue(c));
}
}
@SuppressLint("WrongCall")
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mLayoutSizer.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public int getContentHeightCss() {
return (int) Math.ceil(mContentViewCore.getContentHeightCss());
}
public int getContentWidthCss() {
return (int) Math.ceil(mContentViewCore.getContentWidthCss());
}
public Picture capturePicture() {
return nativeCapturePicture(mNativeAwContents);
}
/**
* Enable the OnNewPicture callback.
* @param enabled Flag to enable the callback.
* @param invalidationOnly Flag to call back only on invalidation without providing a picture.
*/
public void enableOnNewPicture(boolean enabled, boolean invalidationOnly) {
mNewPictureInvalidationOnly = invalidationOnly;
nativeEnableOnNewPicture(mNativeAwContents, enabled);
}
// This is no longer synchronous and just calls the Async version and return 0.
// TODO(boliu): Remove this method.
@Deprecated
public int findAllSync(String searchString) {
findAllAsync(searchString);
return 0;
}
public void findAllAsync(String searchString) {
if (mNativeAwContents == 0) return;
nativeFindAllAsync(mNativeAwContents, searchString);
}
public void findNext(boolean forward) {
if (mNativeAwContents == 0) return;
nativeFindNext(mNativeAwContents, forward);
}
public void clearMatches() {
if (mNativeAwContents == 0) return;
nativeClearMatches(mNativeAwContents);
}
/**
* @return load progress of the WebContents.
*/
public int getMostRecentProgress() {
// WebContentsDelegateAndroid conveniently caches the most recent notified value for us.
return mWebContentsDelegate.getMostRecentProgress();
}
public Bitmap getFavicon() {
return mFavicon;
}
private void requestVisitedHistoryFromClient() {
ValueCallback<String[]> callback = new ValueCallback<String[]>() {
@Override
public void onReceiveValue(final String[] value) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mNativeAwContents == 0) return;
nativeAddVisitedLinks(mNativeAwContents, value);
}
});
}
};
mContentsClient.getVisitedHistory(callback);
}
/**
* Load url without fixing up the url string. Consumers of ContentView are responsible for
* ensuring the URL passed in is properly formatted (i.e. the scheme has been added if left
* off during user input).
*
* @param pararms Parameters for this load.
*/
public void loadUrl(LoadUrlParams params) {
if (params.getLoadUrlType() == LoadUrlParams.LOAD_TYPE_DATA &&
!params.isBaseUrlDataScheme()) {
// This allows data URLs with a non-data base URL access to file:///android_asset/ and
// file:///android_res/ URLs. If AwSettings.getAllowFileAccess permits, it will also
// allow access to file:// URLs (subject to OS level permission checks).
params.setCanLoadLocalResources(true);
}
// If we are reloading the same url, then set transition type as reload.
if (params.getUrl() != null &&
params.getUrl().equals(mContentViewCore.getUrl()) &&
params.getTransitionType() == PageTransitionTypes.PAGE_TRANSITION_LINK) {
params.setTransitionType(PageTransitionTypes.PAGE_TRANSITION_RELOAD);
}
// For WebView, always use the user agent override, which is set
// every time the user agent in AwSettings is modified.
params.setOverrideUserAgent(LoadUrlParams.UA_OVERRIDE_TRUE);
mContentViewCore.loadUrl(params);
suppressInterceptionForThisNavigation();
// The behavior of WebViewClassic uses the populateVisitedLinks callback in WebKit.
// Chromium does not use this use code path and the best emulation of this behavior to call
// request visited links once on the first URL load of the WebView.
if (!mHasRequestedVisitedHistoryFromClient) {
mHasRequestedVisitedHistoryFromClient = true;
requestVisitedHistoryFromClient();
}
}
private void suppressInterceptionForThisNavigation() {
if (mInterceptNavigationDelegate != null) {
// getUrl returns a sanitized address in the same format that will be used for
// callbacks, so it's safe to use string comparison as an equality check later on.
mInterceptNavigationDelegate.onUrlLoadRequested(mContentViewCore.getUrl());
}
}
/**
* Get the URL of the current page.
*
* @return The URL of the current page or null if it's empty.
*/
public String getUrl() {
String url = mContentViewCore.getUrl();
if (url == null || url.trim().isEmpty()) return null;
return url;
}
/**
* Called on the "source" AwContents that is opening the popup window to
* provide the AwContents to host the pop up content.
*/
public void supplyContentsForPopup(AwContents newContents) {
int popupWebContents = nativeReleasePopupWebContents(mNativeAwContents);
assert popupWebContents != 0;
newContents.setNewWebContents(popupWebContents);
}
private void setNewWebContents(int newWebContentsPtr) {
// When setting a new WebContents, we new up a ContentViewCore that will
// wrap it and then swap it.
ContentViewCore newCore = createAndInitializeContentViewCore(
mContainerView, mInternalAccessAdapter, newWebContentsPtr,
new AwPinchGestureStateListener(), mContentsClient.getContentViewClient(),
mZoomControls);
mContentsClient.installWebContentsObserver(newCore);
// Now swap the Java side reference.
mContentViewCore.destroy();
mContentViewCore = newCore;
// Now rewire native side to use the new WebContents.
nativeSetWebContents(mNativeAwContents, newWebContentsPtr);
nativeSetIoThreadClient(mNativeAwContents, mIoThreadClient);
nativeSetInterceptNavigationDelegate(mNativeAwContents, mInterceptNavigationDelegate);
// This will also apply settings to the new WebContents.
mSettings.setWebContents(newWebContentsPtr);
// Finally poke the new ContentViewCore with the size of the container view and show it.
if (mContainerView.getWidth() != 0 || mContainerView.getHeight() != 0) {
mContentViewCore.onSizeChanged(
mContainerView.getWidth(), mContainerView.getHeight(), 0, 0);
}
nativeDidInitializeContentViewCore(mNativeAwContents,
mContentViewCore.getNativeContentViewCore());
if (mContainerView.getVisibility() == View.VISIBLE) {
// The popup window was hidden when we prompted the embedder to display
// it, so show it again now we have a container.
mContentViewCore.onShow();
}
}
public void requestFocus() {
if (!mContainerView.isInTouchMode() && mSettings.shouldFocusFirstNode()) {
nativeFocusFirstNode(mNativeAwContents);
}
}
public boolean isMultiTouchZoomSupported() {
return mSettings.supportsMultiTouchZoom();
}
public View getZoomControlsForTest() {
return mZoomControls.getZoomControlsViewForTest();
}
//--------------------------------------------------------------------------------------------
// WebView[Provider] method implementations (where not provided by ContentViewCore)
//--------------------------------------------------------------------------------------------
/**
* @see ContentViewCore#getContentSettings()
*/
public ContentSettings getContentSettings() {
return mContentViewCore.getContentSettings();
}
/**
* @see ContentViewCore#computeHorizontalScrollRange()
*/
public int computeHorizontalScrollRange() {
return mContentViewCore.computeHorizontalScrollRange();
}
/**
* @see ContentViewCore#computeHorizontalScrollOffset()
*/
public int computeHorizontalScrollOffset() {
return mContentViewCore.computeHorizontalScrollOffset();
}
/**
* @see ContentViewCore#computeVerticalScrollRange()
*/
public int computeVerticalScrollRange() {
return mContentViewCore.computeVerticalScrollRange();
}
/**
* @see ContentViewCore#computeVerticalScrollOffset()
*/
public int computeVerticalScrollOffset() {
return mContentViewCore.computeVerticalScrollOffset();
}
/**
* @see ContentViewCore#computeVerticalScrollExtent()
*/
public int computeVerticalScrollExtent() {
return mContentViewCore.computeVerticalScrollExtent();
}
/**
* @see android.webkit.WebView#stopLoading()
*/
public void stopLoading() {
mContentViewCore.stopLoading();
}
/**
* @see android.webkit.WebView#reload()
*/
public void reload() {
mContentViewCore.reload();
}
/**
* @see android.webkit.WebView#canGoBack()
*/
public boolean canGoBack() {
return mContentViewCore.canGoBack();
}
/**
* @see android.webkit.WebView#goBack()
*/
public void goBack() {
mContentViewCore.goBack();
suppressInterceptionForThisNavigation();
}
/**
* @see android.webkit.WebView#canGoForward()
*/
public boolean canGoForward() {
return mContentViewCore.canGoForward();
}
/**
* @see android.webkit.WebView#goForward()
*/
public void goForward() {
mContentViewCore.goForward();
suppressInterceptionForThisNavigation();
}
/**
* @see android.webkit.WebView#canGoBackOrForward(int)
*/
public boolean canGoBackOrForward(int steps) {
return mContentViewCore.canGoToOffset(steps);
}
/**
* @see android.webkit.WebView#goBackOrForward(int)
*/
public void goBackOrForward(int steps) {
mContentViewCore.goToOffset(steps);
suppressInterceptionForThisNavigation();
}
/**
* @see android.webkit.WebView#pauseTimers()
*/
// TODO(kristianm): Remove
public void pauseTimers() {
ContentViewStatics.setWebKitSharedTimersSuspended(true);
}
/**
* @see android.webkit.WebView#resumeTimers()
*/
// TODO(kristianm): Remove
public void resumeTimers() {
ContentViewStatics.setWebKitSharedTimersSuspended(false);
}
/**
* @see android.webkit.WebView#onPause()
*/
public void onPause() {
mIsPaused = true;
mContentViewCore.onHide();
}
/**
* @see android.webkit.WebView#onResume()
*/
public void onResume() {
mContentViewCore.onShow();
mIsPaused = false;
}
/**
* @see android.webkit.WebView#isPaused()
*/
public boolean isPaused() {
return mIsPaused;
}
/**
* @see android.webkit.WebView#onCreateInputConnection(EditorInfo)
*/
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return mContentViewCore.onCreateInputConnection(outAttrs);
}
/**
* @see android.webkit.WebView#onKeyUp(int, KeyEvent)
*/
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mContentViewCore.onKeyUp(keyCode, event);
}
/**
* @see android.webkit.WebView#dispatchKeyEvent(KeyEvent)
*/
public boolean dispatchKeyEvent(KeyEvent event) {
return mContentViewCore.dispatchKeyEvent(event);
}
/**
* Clears the resource cache. Note that the cache is per-application, so this will clear the
* cache for all WebViews used.
*
* @param includeDiskFiles if false, only the RAM cache is cleared
*/
public void clearCache(boolean includeDiskFiles) {
if (mNativeAwContents == 0) return;
nativeClearCache(mNativeAwContents, includeDiskFiles);
}
public void documentHasImages(Message message) {
if (mNativeAwContents == 0) return;
nativeDocumentHasImages(mNativeAwContents, message);
}
public void saveWebArchive(
final String basename, boolean autoname, final ValueCallback<String> callback) {
if (!autoname) {
saveWebArchiveInternal(basename, callback);
return;
}
// If auto-generating the file name, handle the name generation on a background thread
// as it will require I/O access for checking whether previous files existed.
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
return generateArchiveAutoNamePath(getOriginalUrl(), basename);
}
@Override
protected void onPostExecute(String result) {
saveWebArchiveInternal(result, callback);
}
}.execute();
}
public String getOriginalUrl() {
NavigationHistory history = mContentViewCore.getNavigationHistory();
int currentIndex = history.getCurrentEntryIndex();
if (currentIndex >= 0 && currentIndex < history.getEntryCount()) {
return history.getEntryAtIndex(currentIndex).getOriginalUrl();
}
return null;
}
/**
* @see ContentViewCore#getNavigationHistory()
*/
public NavigationHistory getNavigationHistory() {
return mContentViewCore.getNavigationHistory();
}
/**
* @see android.webkit.WebView#getTitle()
*/
public String getTitle() {
return mContentViewCore.getTitle();
}
/**
* @see android.webkit.WebView#clearHistory()
*/
public void clearHistory() {
mContentViewCore.clearHistory();
}
public String[] getHttpAuthUsernamePassword(String host, String realm) {
return HttpAuthDatabase.getInstance(mContentViewCore.getContext())
.getHttpAuthUsernamePassword(host, realm);
}
public void setHttpAuthUsernamePassword(String host, String realm, String username,
String password) {
HttpAuthDatabase.getInstance(mContentViewCore.getContext())
.setHttpAuthUsernamePassword(host, realm, username, password);
}
/**
* @see android.webkit.WebView#getCertificate()
*/
public SslCertificate getCertificate() {
if (mNativeAwContents == 0) return null;
return SslUtil.getCertificateFromDerBytes(nativeGetCertificate(mNativeAwContents));
}
/**
* @see android.webkit.WebView#clearSslPreferences()
*/
public void clearSslPreferences() {
mContentViewCore.clearSslPreferences();
}
/**
* Method to return all hit test values relevant to public WebView API.
* Note that this expose more data than needed for WebView.getHitTestResult.
* Unsafely returning reference to mutable internal object to avoid excessive
* garbage allocation on repeated calls.
*/
public HitTestData getLastHitTestResult() {
if (mNativeAwContents == 0) return null;
nativeUpdateLastHitTestData(mNativeAwContents);
return mPossiblyStaleHitTestData;
}
/**
* @see android.webkit.WebView#requestFocusNodeHref()
*/
public void requestFocusNodeHref(Message msg) {
if (msg == null || mNativeAwContents == 0) return;
nativeUpdateLastHitTestData(mNativeAwContents);
Bundle data = msg.getData();
data.putString("url", mPossiblyStaleHitTestData.href);
data.putString("title", mPossiblyStaleHitTestData.anchorText);
data.putString("src", mPossiblyStaleHitTestData.imgSrc);
msg.setData(data);
msg.sendToTarget();
}
/**
* @see android.webkit.WebView#requestImageRef()
*/
public void requestImageRef(Message msg) {
if (msg == null || mNativeAwContents == 0) return;
nativeUpdateLastHitTestData(mNativeAwContents);
Bundle data = msg.getData();
data.putString("url", mPossiblyStaleHitTestData.imgSrc);
msg.setData(data);
msg.sendToTarget();
}
/**
* @see android.webkit.WebView#getScale()
*
* Please note that the scale returned is the page scale multiplied by
* the screen density factor. See CTS WebViewTest.testSetInitialScale.
*/
public float getScale() {
return (float)(mContentViewCore.getScale() * mDIPScale);
}
/**
* @see android.webkit.WebView#flingScroll(int, int)
*/
public void flingScroll(int vx, int vy) {
mContentViewCore.flingScroll(vx, vy);
}
/**
* @see android.webkit.WebView#pageUp(boolean)
*/
public boolean pageUp(boolean top) {
return mContentViewCore.pageUp(top);
}
/**
* @see android.webkit.WebView#pageDown(boolean)
*/
public boolean pageDown(boolean bottom) {
return mContentViewCore.pageDown(bottom);
}
/**
* @see android.webkit.WebView#canZoomIn()
*/
public boolean canZoomIn() {
return mContentViewCore.canZoomIn();
}
/**
* @see android.webkit.WebView#canZoomOut()
*/
public boolean canZoomOut() {
return mContentViewCore.canZoomOut();
}
/**
* @see android.webkit.WebView#zoomIn()
*/
public boolean zoomIn() {
return mContentViewCore.zoomIn();
}
/**
* @see android.webkit.WebView#zoomOut()
*/
public boolean zoomOut() {
return mContentViewCore.zoomOut();
}
/**
* @see android.webkit.WebView#invokeZoomPicker()
*/
public void invokeZoomPicker() {
mContentViewCore.invokeZoomPicker();
}
//--------------------------------------------------------------------------------------------
// View and ViewGroup method implementations
//--------------------------------------------------------------------------------------------
/**
* @see android.webkit.View#onTouchEvent()
*/
public boolean onTouchEvent(MotionEvent event) {
if (mNativeAwContents == 0) return false;
boolean rv = mContentViewCore.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
int actionIndex = event.getActionIndex();
// Note this will trigger IPC back to browser even if nothing is hit.
nativeRequestNewHitTestDataAt(mNativeAwContents,
(int)Math.round(event.getX(actionIndex) / mDIPScale),
(int)Math.round(event.getY(actionIndex) / mDIPScale));
}
return rv;
}
/**
* @see android.view.View#onHoverEvent()
*/
public boolean onHoverEvent(MotionEvent event) {
return mContentViewCore.onHoverEvent(event);
}
/**
* @see android.view.View#onGenericMotionEvent()
*/
public boolean onGenericMotionEvent(MotionEvent event) {
return mContentViewCore.onGenericMotionEvent(event);
}
/**
* @see android.view.View#onConfigurationChanged()
*/
public void onConfigurationChanged(Configuration newConfig) {
mContentViewCore.onConfigurationChanged(newConfig);
}
/**
* @see android.view.View#onAttachedToWindow()
*/
public void onAttachedToWindow() {
if (mScrollChangeListener == null) {
mScrollChangeListener = new ScrollChangeListener();
}
mContainerView.getViewTreeObserver().addOnScrollChangedListener(mScrollChangeListener);
mContentViewCore.onAttachedToWindow();
nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
mContainerView.getHeight());
// This is for the case where this is created by restoreState, which
// needs to call to NavigationController::LoadIfNecessary to actually
// load the restored page.
if (!mIsPaused) onResume();
}
/**
* @see android.view.View#onDetachedFromWindow()
*/
public void onDetachedFromWindow() {
if (mNativeAwContents != 0) {
nativeOnDetachedFromWindow(mNativeAwContents);
}
if (mScrollChangeListener != null) {
mContainerView.getViewTreeObserver().removeOnScrollChangedListener(
mScrollChangeListener);
mScrollChangeListener = null;
}
mContentViewCore.onDetachedFromWindow();
}
/**
* @see android.view.View#onWindowFocusChanged()
*/
public void onWindowFocusChanged(boolean hasWindowFocus) {
mWindowFocused = hasWindowFocus;
mContentViewCore.onFocusChanged(mContainerViewFocused && mWindowFocused);
}
/**
* @see android.view.View#onFocusChanged()
*/
public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
mContainerViewFocused = focused;
mContentViewCore.onFocusChanged(mContainerViewFocused && mWindowFocused);
}
/**
* @see android.view.View#onSizeChanged()
*/
public void onSizeChanged(int w, int h, int ow, int oh) {
if (mNativeAwContents == 0) return;
updatePhysicalBackingSizeIfNeeded();
mContentViewCore.onSizeChanged(w, h, ow, oh);
nativeOnSizeChanged(mNativeAwContents, w, h, ow, oh);
}
/**
* @see android.view.View#onVisibilityChanged()
*/
public void onVisibilityChanged(View changedView, int visibility) {
updateVisiblityState();
}
/**
* @see android.view.View#onWindowVisibilityChanged()
*/
public void onWindowVisibilityChanged(int visibility) {
updateVisiblityState();
}
private void updateVisiblityState() {
if (mNativeAwContents == 0 || mIsPaused) return;
boolean windowVisible = mContainerView.getWindowVisibility() == View.VISIBLE;
boolean viewVisible = mContainerView.getVisibility() == View.VISIBLE;
nativeSetWindowViewVisibility(mNativeAwContents, windowVisible, viewVisible);
if (viewVisible) {
mContentViewCore.onShow();
} else {
mContentViewCore.onHide();
}
}
/**
* Key for opaque state in bundle. Note this is only public for tests.
*/
public static final String SAVE_RESTORE_STATE_KEY = "WEBVIEW_CHROMIUM_STATE";
/**
* Save the state of this AwContents into provided Bundle.
* @return False if saving state failed.
*/
public boolean saveState(Bundle outState) {
if (outState == null) return false;
byte[] state = nativeGetOpaqueState(mNativeAwContents);
if (state == null) return false;
outState.putByteArray(SAVE_RESTORE_STATE_KEY, state);
return true;
}
/**
* Restore the state of this AwContents into provided Bundle.
* @param inState Must be a bundle returned by saveState.
* @return False if restoring state failed.
*/
public boolean restoreState(Bundle inState) {
if (inState == null) return false;
byte[] state = inState.getByteArray(SAVE_RESTORE_STATE_KEY);
if (state == null) return false;
boolean result = nativeRestoreFromOpaqueState(mNativeAwContents, state);
// The onUpdateTitle callback normally happens when a page is loaded,
// but is optimized out in the restoreState case because the title is
// already restored. See WebContentsImpl::UpdateTitleForEntry. So we
// call the callback explicitly here.
if (result) mContentsClient.onReceivedTitle(mContentViewCore.getTitle());
return result;
}
/**
* @see ContentViewCore#addPossiblyUnsafeJavascriptInterface(Object, String, Class)
*/
public void addPossiblyUnsafeJavascriptInterface(Object object, String name,
Class<? extends Annotation> requiredAnnotation) {
mContentViewCore.addPossiblyUnsafeJavascriptInterface(object, name, requiredAnnotation);
}
/**
* @see android.webkit.WebView#removeJavascriptInterface(String)
*/
public void removeJavascriptInterface(String interfaceName) {
mContentViewCore.removeJavascriptInterface(interfaceName);
}
/**
* @see android.webkit.WebView#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)
*/
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
mContentViewCore.onInitializeAccessibilityNodeInfo(info);
}
/**
* @see android.webkit.WebView#onInitializeAccessibilityEvent(AccessibilityEvent)
*/
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
mContentViewCore.onInitializeAccessibilityEvent(event);
}
public boolean supportsAccessibilityAction(int action) {
return mContentViewCore.supportsAccessibilityAction(action);
}
/**
* @see android.webkit.WebView#performAccessibilityAction(int, Bundle)
*/
public boolean performAccessibilityAction(int action, Bundle arguments) {
return mContentViewCore.performAccessibilityAction(action, arguments);
}
//--------------------------------------------------------------------------------------------
// Methods called from native via JNI
//--------------------------------------------------------------------------------------------
@CalledByNative
private static void onDocumentHasImagesResponse(boolean result, Message message) {
message.arg1 = result ? 1 : 0;
message.sendToTarget();
}
@CalledByNative
private void onReceivedTouchIconUrl(String url, boolean precomposed) {
mContentsClient.onReceivedTouchIconUrl(url, precomposed);
}
@CalledByNative
private void onReceivedIcon(Bitmap bitmap) {
mContentsClient.onReceivedIcon(bitmap);
mFavicon = bitmap;
}
/** Callback for generateMHTML. */
@CalledByNative
private static void generateMHTMLCallback(
String path, long size, ValueCallback<String> callback) {
if (callback == null) return;
callback.onReceiveValue(size < 0 ? null : path);
}
@CalledByNative
private void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
mContentsClient.onReceivedHttpAuthRequest(handler, host, realm);
}
private class AwGeolocationCallback implements GeolocationPermissions.Callback {
@Override
public void invoke(final String origin, final boolean allow, final boolean retain) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
if (retain) {
if (allow) {
mBrowserContext.getGeolocationPermissions().allow(origin);
} else {
mBrowserContext.getGeolocationPermissions().deny(origin);
}
}
nativeInvokeGeolocationCallback(mNativeAwContents, allow, origin);
}
});
}
}
@CalledByNative
private void onGeolocationPermissionsShowPrompt(String origin) {
AwGeolocationPermissions permissions = mBrowserContext.getGeolocationPermissions();
// Reject if geoloaction is disabled, or the origin has a retained deny
if (!mSettings.getGeolocationEnabled()) {
nativeInvokeGeolocationCallback(mNativeAwContents, false, origin);
return;
}
// Allow if the origin has a retained allow
if (permissions.hasOrigin(origin)) {
nativeInvokeGeolocationCallback(mNativeAwContents, permissions.isOriginAllowed(origin),
origin);
return;
}
mContentsClient.onGeolocationPermissionsShowPrompt(
origin, new AwGeolocationCallback());
}
@CalledByNative
private void onGeolocationPermissionsHidePrompt() {
mContentsClient.onGeolocationPermissionsHidePrompt();
}
@CalledByNative
public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
boolean isDoneCounting) {
mContentsClient.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
}
@CalledByNative
public void onNewPicture() {
mContentsClient.onNewPicture(mNewPictureInvalidationOnly ? null : capturePicture());
}
// Called as a result of nativeUpdateLastHitTestData.
@CalledByNative
private void updateHitTestData(
int type, String extra, String href, String anchorText, String imgSrc) {
mPossiblyStaleHitTestData.hitTestResultType = type;
mPossiblyStaleHitTestData.hitTestResultExtraData = extra;
mPossiblyStaleHitTestData.href = href;
mPossiblyStaleHitTestData.anchorText = anchorText;
mPossiblyStaleHitTestData.imgSrc = imgSrc;
}
@CalledByNative
private void requestProcessMode() {
mInternalAccessAdapter.requestDrawGL(null);
}
@CalledByNative
private void invalidate() {
mContainerView.invalidate();
}
@CalledByNative
private boolean performLongClick() {
return mContainerView.performLongClick();
}
@CalledByNative
private int[] getLocationOnScreen() {
int[] result = new int[2];
mContainerView.getLocationOnScreen(result);
return result;
}
@CalledByNative
private void onPageScaleFactorChanged(float pageScaleFactor) {
// This change notification comes from the renderer thread, not from the cc/ impl thread.
mLayoutSizer.onPageScaleChanged(pageScaleFactor);
}
// -------------------------------------------------------------------------------------------
// Helper methods
// -------------------------------------------------------------------------------------------
private void saveWebArchiveInternal(String path, final ValueCallback<String> callback) {
if (path == null || mNativeAwContents == 0) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
callback.onReceiveValue(null);
}
});
} else {
nativeGenerateMHTML(mNativeAwContents, path, callback);
}
}
/**
* Try to generate a pathname for saving an MHTML archive. This roughly follows WebView's
* autoname logic.
*/
private static String generateArchiveAutoNamePath(String originalUrl, String baseName) {
String name = null;
if (originalUrl != null && !originalUrl.isEmpty()) {
try {
String path = new URL(originalUrl).getPath();
int lastSlash = path.lastIndexOf('/');
if (lastSlash > 0) {
name = path.substring(lastSlash + 1);
} else {
name = path;
}
} catch (MalformedURLException e) {
// If it fails parsing the URL, we'll just rely on the default name below.
}
}
if (TextUtils.isEmpty(name)) name = "index";
String testName = baseName + name + WEB_ARCHIVE_EXTENSION;
if (!new File(testName).exists()) return testName;
for (int i = 1; i < 100; i++) {
testName = baseName + name + "-" + i + WEB_ARCHIVE_EXTENSION;
if (!new File(testName).exists()) return testName;
}
Log.e(TAG, "Unable to auto generate archive name for path: " + baseName);
return null;
}
//--------------------------------------------------------------------------------------------
// Native methods
//--------------------------------------------------------------------------------------------
private native int nativeInit(AwWebContentsDelegate webViewWebContentsDelegate,
AwContentsClientBridge contentsClientBridge);
private static native void nativeDestroy(int nativeAwContents);
private static native void nativeSetAwDrawSWFunctionTable(int functionTablePointer);
private static native void nativeSetAwDrawGLFunctionTable(int functionTablePointer);
private static native int nativeGetAwDrawGLFunction();
private native int nativeGetWebContents(int nativeAwContents);
private native void nativeDidInitializeContentViewCore(int nativeAwContents,
int nativeContentViewCore);
private native void nativeDocumentHasImages(int nativeAwContents, Message message);
private native void nativeGenerateMHTML(
int nativeAwContents, String path, ValueCallback<String> callback);
private native void nativeSetIoThreadClient(int nativeAwContents,
AwContentsIoThreadClient ioThreadClient);
private native void nativeSetInterceptNavigationDelegate(int nativeAwContents,
InterceptNavigationDelegate navigationInterceptionDelegate);
private native void nativeAddVisitedLinks(int nativeAwContents, String[] visitedLinks);
private native boolean nativePrepareDrawGL(int nativeAwContents, int scrollX, int scrollY);
private native void nativeFindAllAsync(int nativeAwContents, String searchString);
private native void nativeFindNext(int nativeAwContents, boolean forward);
private native void nativeClearMatches(int nativeAwContents);
private native void nativeClearCache(int nativeAwContents, boolean includeDiskFiles);
private native byte[] nativeGetCertificate(int nativeAwContents);
// Coordinates in desity independent pixels.
private native void nativeRequestNewHitTestDataAt(int nativeAwContents, int x, int y);
private native void nativeUpdateLastHitTestData(int nativeAwContents);
private native void nativeOnSizeChanged(int nativeAwContents, int w, int h, int ow, int oh);
private native void nativeSetWindowViewVisibility(int nativeAwContents, boolean windowVisible,
boolean viewVisible);
private native void nativeOnAttachedToWindow(int nativeAwContents, int w, int h);
private native void nativeOnDetachedFromWindow(int nativeAwContents);
// Returns null if save state fails.
private native byte[] nativeGetOpaqueState(int nativeAwContents);
// Returns false if restore state fails.
private native boolean nativeRestoreFromOpaqueState(int nativeAwContents, byte[] state);
private native int nativeReleasePopupWebContents(int nativeAwContents);
private native void nativeSetWebContents(int nativeAwContents, int nativeNewWebContents);
private native void nativeFocusFirstNode(int nativeAwContents);
private native boolean nativeDrawSW(int nativeAwContents, Canvas canvas, int clipX, int clipY,
int clipW, int clipH);
private native int nativeGetAwDrawGLViewContext(int nativeAwContents);
private native Picture nativeCapturePicture(int nativeAwContents);
private native void nativeEnableOnNewPicture(int nativeAwContents, boolean enabled);
private native void nativeInvokeGeolocationCallback(
int nativeAwContents, boolean value, String requestingFrame);
}