// Copyright 2012 The Chromium Authors
// 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.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.Rect;
import android.net.Uri;
import android.net.http.SslCertificate;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Pair;
import android.util.SparseArray;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.animation.AnimationUtils;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.textclassifier.TextClassifier;
import android.webkit.JavascriptInterface;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.jni_zero.CalledByNative;
import org.jni_zero.CalledByNativeUnchecked;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;
import org.chromium.android_webview.autofill.AndroidAutofillSafeModeAction;
import org.chromium.android_webview.common.AwFeatures;
import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.common.Lifetime;
import org.chromium.android_webview.gfx.AwDrawFnImpl;
import org.chromium.android_webview.gfx.AwFunctor;
import org.chromium.android_webview.gfx.AwGLFunctor;
import org.chromium.android_webview.gfx.AwPicture;
import org.chromium.android_webview.gfx.RectUtils;
import org.chromium.android_webview.metrics.AwOriginVisitLogger;
import org.chromium.android_webview.metrics.BackForwardCacheNotRestoredReason;
import org.chromium.android_webview.permission.AwGeolocationCallback;
import org.chromium.android_webview.permission.AwPermissionRequest;
import org.chromium.android_webview.renderer_priority.RendererPriority;
import org.chromium.android_webview.selection.AwSelectionActionMenuDelegate;
import org.chromium.base.BaseFeatures;
import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.LocaleUtils;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TimeUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.jank_tracker.FrameMetricsListener;
import org.chromium.base.jank_tracker.FrameMetricsStore;
import org.chromium.base.jank_tracker.JankReportingScheduler;
import org.chromium.base.jank_tracker.JankScenario;
import org.chromium.base.jank_tracker.JankTracker;
import org.chromium.base.jank_tracker.JankTrackerImpl;
import org.chromium.base.jank_tracker.JankTrackerStateController;
import org.chromium.base.memory.MemoryInfoBridge;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillSelectionMenuItemHelper;
import org.chromium.components.content_capture.OnscreenContentProvider;
import org.chromium.components.embedder_support.util.TouchEventFilter;
import org.chromium.components.embedder_support.util.WebResourceResponseInfo;
import org.chromium.components.navigation_interception.InterceptNavigationDelegate;
import org.chromium.components.stylus_handwriting.StylusWritingController;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.components.viz.common.VizFeatures;
import org.chromium.components.zoom.ZoomConstants;
import org.chromium.content_public.browser.ChildProcessImportance;
import org.chromium.content_public.browser.ContentViewStatics;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.GestureStateListener;
import org.chromium.content_public.browser.ImeAdapter;
import org.chromium.content_public.browser.ImeEventObserver;
import org.chromium.content_public.browser.JavaScriptCallback;
import org.chromium.content_public.browser.JavascriptInjector;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.MessagePayload;
import org.chromium.content_public.browser.MessagePort;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.NavigationHistory;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.SelectionClient;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.SmartClipProvider;
import org.chromium.content_public.browser.ViewEventSink;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.content_public.browser.WebContentsInternals;
import org.chromium.content_public.browser.navigation_controller.LoadURLType;
import org.chromium.content_public.browser.navigation_controller.UserAgentOverrideOption;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.content_public.common.Referrer;
import org.chromium.device.gamepad.GamepadList;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.Clipboard;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid.DisplayAndroidObserver;
import org.chromium.url.GURL;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Exposes the native AwContents class, and together these classes wrap the WebContents 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).
*/
@Lifetime.WebView
@JNINamespace("android_webview")
public class AwContents implements SmartClipProvider {
private static final String TAG = "AwContents";
private static final boolean TRACE = false;
private static final int NO_WARN = 0;
private static final int WARN = 1;
private static final String PRODUCT_VERSION = AwContentsStatics.getProductVersion();
private static final String WEB_ARCHIVE_EXTENSION = ".mht";
// The request code should be unique per WebView/AwContents object.
private static final int PROCESS_TEXT_REQUEST_CODE = 100;
// Used to avoid enabling zooming in / out if resulting zooming will
// produce little visible difference.
private static final float ZOOM_CONTROLS_EPSILON = 0.007f;
private static final double MIN_SCREEN_HEIGHT_PERCENTAGE_FOR_INTERSTITIAL = 0.7;
private static final String SAMSUNG_WORKAROUND_BASE_URL = "email://";
private static final int SAMSUNG_WORKAROUND_DELAY = 200;
private static int sLastId;
// Unique id given to each AwContents object, starting from 1.
private final int mId;
@VisibleForTesting
public static final String LOAD_URL_SCHEME_HISTOGRAM_NAME = "Android.WebView.LoadUrl.UrlScheme";
// Permit any number of slashes, since chromium seems to canonicalize bad values.
private static final Pattern sFileAndroidAssetPattern =
Pattern.compile("^file:/*android_(asset|res).*");
// Matches a data URL that (may) have a valid fragment selector, pulling the fragment selector
// out into a group. Such a URL must contain a single '#' character and everything after that
// must be a valid DOM id.
// DOM id grammar: https://www.w3.org/TR/1999/REC-html401-19991224/types.html#type-name
private static final Pattern sDataURLWithSelectorPattern =
Pattern.compile("^[^#]*(#[A-Za-z][A-Za-z0-9\\-_:.]*)$");
private static final String CONSTRUCTOR_HISTOGRAM_NAME =
"Android.WebView.AwContentsConstructorTime";
private static class ForceAuxiliaryBitmapRendering {
private static final boolean sResult = lazyCheck();
private static boolean lazyCheck() {
return !AwContentsJni.get().hasRequiredHardwareExtensions();
}
}
// Used to record the UMA histogram Android.WebView.LoadDataWithBaseUrl.UrlScheme. Since these
// values are persisted to logs, they should never be renumbered or reused.
@VisibleForTesting
@IntDef({
UrlScheme.EMPTY,
UrlScheme.UNKNOWN_SCHEME,
UrlScheme.HTTP_SCHEME,
UrlScheme.HTTPS_SCHEME,
UrlScheme.FILE_SCHEME,
UrlScheme.FTP_SCHEME,
UrlScheme.DATA_SCHEME,
UrlScheme.JAVASCRIPT_SCHEME,
UrlScheme.ABOUT_SCHEME,
UrlScheme.CHROME_SCHEME,
UrlScheme.BLOB_SCHEME,
UrlScheme.CONTENT_SCHEME,
UrlScheme.INTENT_SCHEME,
UrlScheme.FILE_ANDROID_ASSET_SCHEME
})
public @interface UrlScheme {
int EMPTY = 0;
int UNKNOWN_SCHEME = 1;
int HTTP_SCHEME = 2;
int HTTPS_SCHEME = 3;
int FILE_SCHEME = 4;
int FTP_SCHEME = 5;
int DATA_SCHEME = 6;
int JAVASCRIPT_SCHEME = 7;
int ABOUT_SCHEME = 8;
int CHROME_SCHEME = 9;
int BLOB_SCHEME = 10;
int CONTENT_SCHEME = 11;
int INTENT_SCHEME = 12;
int FILE_ANDROID_ASSET_SCHEME = 13; // Covers android_asset and android_res URLs
int COUNT = 14;
}
// Used to record Android.WebView.UsedInPopupWindow.
@IntDef({
UsedInPopupWindow.NOT_IN_POPUP_WINDOW,
UsedInPopupWindow.IN_POPUP_WINDOW,
UsedInPopupWindow.UNKNOWN,
})
public @interface UsedInPopupWindow {
int NOT_IN_POPUP_WINDOW = 0;
int IN_POPUP_WINDOW = 1;
int UNKNOWN = 2;
int COUNT = 3;
}
/**
* WebKit hit test related data structure. 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 ViewEventSink.InternalAccessDelegate {
/** @see View#overScrollBy(int, int, int, int, int, int, int, int, boolean); */
void overScrollBy(
int deltaX,
int deltaY,
int scrollX,
int scrollY,
int scrollRangeX,
int scrollRangeY,
int maxOverScrollX,
int maxOverScrollY,
boolean isTouchEvent);
/** @see View#scrollTo(int, int) */
void super_scrollTo(int scrollX, int scrollY);
/** @see View#setMeasuredDimension(int, int) */
void setMeasuredDimension(int measuredWidth, int measuredHeight);
/** @see View#getScrollBarStyle() */
int super_getScrollBarStyle();
/** @see View#startActivityForResult(Intent, int) */
void super_startActivityForResult(Intent intent, int requestCode);
/** @see View#onConfigurationChanged(Configuration) */
void super_onConfigurationChanged(Configuration newConfig);
}
/**
* Factory interface used for constructing functors that the Android framework uses for
* calling back into Chromium code to render the the contents of a Chromium frame into
* an Android view.
*/
public interface NativeDrawFunctorFactory {
/** Create a GL functor associated with native context |context|. */
NativeDrawGLFunctor createGLFunctor(long context);
/**
* Used for draw_fn functor. Only one of these methods need to return non-null.
* Prefer this over createGLFunctor.
*/
AwDrawFnImpl.DrawFnAccess getDrawFnAccess();
}
/**
* Interface that consumers of {@link AwContents} must implement to support
* native GL rendering.
*/
public interface NativeDrawGLFunctor {
/**
* Requests a callback on the native DrawGL method (see getAwDrawGLFunction).
*
* If called from within onDraw, |canvas| should be non-null and must be hardware
* accelerated. |releasedCallback| should be null if |canvas| is null.
*
* @return false indicates the GL draw request was not accepted, and the caller
* should fallback to the SW path.
*/
boolean requestDrawGL(Canvas canvas, Runnable releasedCallback);
/**
* Requests a callback on the native DrawGL method (see getAwDrawGLFunction).
*
* |containerView| must be hardware accelerated. If |waitForCompletion| is true, this method
* will not return until functor has returned.
*/
boolean requestInvokeGL(View containerView, boolean waitForCompletion);
/** Detaches the GLFunctor from the view tree. */
void detach(View containerView);
/**
* Destroy this functor instance and any native objects associated with it. No method is
* called after destroy.
*/
void destroy();
}
/**
* Class to facilitate dependency injection. Subclasses by test code to provide mock versions of
* certain AwContents dependencies.
*/
public static class DependencyFactory {
public AwLayoutSizer createLayoutSizer() {
return new AwLayoutSizer();
}
public AwScrollOffsetManager createScrollOffsetManager(
AwScrollOffsetManager.Delegate delegate) {
return new AwScrollOffsetManager(delegate);
}
}
/**
* Visual state callback, see {@link #insertVisualStateCallback} for details.
*
*/
@VisibleForTesting
public abstract static class VisualStateCallback {
/**
* @param requestId the id passed to {@link AwContents#insertVisualStateCallback}
* which can be used to match requests with the corresponding callbacks.
*/
public abstract void onComplete(long requestId);
}
private long mNativeAwContents;
private AwBrowserContext mBrowserContext;
private ViewGroup mContainerView;
private AwFunctor mDrawFunctor;
private final Context mContext;
private final int mAppTargetSdkVersion;
private AwViewAndroidDelegate mViewAndroidDelegate;
private WindowAndroidWrapper mWindowAndroid;
private WebContents mWebContents;
private ViewEventSink mViewEventSink;
private WebContentsInternalsHolder mWebContentsInternalsHolder;
private NavigationController mNavigationController;
private final AwContentsClient mContentsClient;
private AwWebContentsObserver mWebContentsObserver;
private final AwContentsClientBridge mContentsClientBridge;
private final AwWebContentsDelegateAdapter mWebContentsDelegate;
private final AwContentsBackgroundThreadClient mBackgroundThreadClient;
private final AwContentsIoThreadClient mIoThreadClient;
private final InterceptNavigationDelegateImpl mInterceptNavigationDelegate;
private InternalAccessDelegate mInternalAccessAdapter;
private final NativeDrawFunctorFactory mNativeDrawFunctorFactory;
private final AwLayoutSizer mLayoutSizer;
private final AwZoomControls mZoomControls;
private final AwScrollOffsetManager mScrollOffsetManager;
private OverScrollGlow mOverScrollGlow;
private final DisplayAndroidObserver mDisplayObserver;
// This can be accessed on any thread after construction. See AwContentsIoThreadClient.
private final AwSettings mSettings;
private final ScrollAccessibilityHelper mScrollAccessibilityHelper;
private final ObserverList<PopupTouchHandleDrawable> mTouchHandleDrawables =
new ObserverList<>();
private boolean mIsPaused;
private boolean mIsViewVisible;
private boolean mIsWindowVisible;
private boolean mIsAttachedToWindow;
private long mPreferredFrameIntervalNanos;
// Visibility state of |mWebContents|.
private boolean mIsContentVisible;
private boolean mIsUpdateVisibilityTaskPending;
private Runnable mUpdateVisibilityRunnable;
/**
* Set to true if there is ever a call to {@link AwContents#getBrowserContext()}.
* This flag is primarily used to prevent setting a new browser context via. {@link
* AwContents#setBrowserContext(AwBrowserContext)} after it has been retrieved externally.
*/
private boolean mBrowserContextAccessed;
/**
* Set to true if the browser context has ever been set explicitly via.
* {@link AwContents#setBrowserContext(AwBrowserContext)}.
*/
private boolean mBrowserContextSetExplicitly;
/**
* Set to true if {@link AwContents#evaluateJavaScript(String, Callback)}
* has been called.
*/
private boolean mHasEvaluatedJavascript;
@VisibleForTesting public static final long FUNCTOR_RECLAIM_DELAY_MS = 10000;
@VisibleForTesting public static final long METRICS_COLLECTION_DELAY_MS = 1000;
private static final long CURRENTLY_VISIBLE = -1;
private long mLastWindowVisibleTime = -1;
private boolean mHasPendingReclaimTask;
private BiFunction<Runnable, Long, Void> mPostDelayedTaskForTesting;
private static final long MEMORY_COLLECTION_INTERVAL_MS = 5 * 60 * 1000;
private static long sLastCollectionTime = -MEMORY_COLLECTION_INTERVAL_MS;
@VisibleForTesting
public static final String PSS_HISTOGRAM = "Android.WebView.Memory.FunctorReclaim.OtherPss";
@VisibleForTesting
public static final String PRIVATE_DIRTY_HISTOGRAM =
"Android.WebView.Memory.FunctorReclaim.OtherPrivateDirty";
private @RendererPriority int mRendererPriority;
private boolean mRendererPriorityWaivedWhenNotVisible;
private Bitmap mFavicon;
private boolean mHasRequestedVisitedHistoryFromClient;
// Whether this WebView is a popup.
private boolean mIsPopupWindow;
// The base background color, i.e. not accounting for any CSS body from the current page.
private int mBaseBackgroundColor = Color.WHITE;
// Did background set by developer, now used for dark mode.
private boolean mDidInitBackground;
// Must call AwContentsJni.get().updateLastHitTestData first to update this before use.
private final HitTestData mPossiblyStaleHitTestData = new HitTestData();
private final DefaultVideoPosterRequestHandler mDefaultVideoPosterRequestHandler;
// Bound method for suppling Picture instances to the AwContentsClient. Will be null if the
// picture listener API has not yet been enabled, or if it is using invalidation-only mode.
private Callable<Picture> mPictureListenerContentProvider;
private boolean mContainerViewFocused;
private boolean mWindowFocused;
// These come from the compositor and are updated synchronously (in contrast to the values in
// RenderCoordinates, which are updated at end of every frame).
private float mPageScaleFactor = 1.0f;
private float mMinPageScaleFactor = 1.0f;
private float mMaxPageScaleFactor = 1.0f;
private float mContentWidthDip;
private float mContentHeightDip;
private AwPdfExporter mAwPdfExporter;
private AwViewMethods mAwViewMethods;
private final FullScreenTransitionsState mFullScreenTransitionsState;
// This is a workaround for some qualcomm devices discarding buffer on
// Activity restore.
private boolean mInvalidateRootViewOnNextDraw;
// The framework may temporarily detach our container view, for example during layout if
// we are a child of a ListView. This may cause many toggles of View focus, which we suppress
// when in this state.
private boolean mTemporarilyDetached;
// True when this AwContents has been destroyed.
// Do not use directly, call isDestroyed() instead.
private boolean mIsDestroyed;
private AutofillProvider mAutofillProvider;
private static String sCurrentLocales = "";
private Paint mPaintForNWorkaround;
// A holder of objects passed from WebContents and should be owned by AwContents that may
// have direct or indirect reference back to WebView. They are used internally by
// WebContents but all the references can create a new gc root that can keep WebView
// instances from being freed when they are detached from view tree, hence lead to
// memory leak. To avoid the issue, it is possible to use |WebContents.setInternalHolder|
// to move the holder of those internal objects to AwContents. Note that they are still
// used by WebContents, and AwContents doesn't have to know what's inside the holder.
private WebContentsInternals mWebContentsInternals;
private JavascriptInjector mJavascriptInjector;
private OnscreenContentProvider mOnscreenContentProvider;
private AwDisplayCutoutController mDisplayCutoutController;
private final AwDisplayModeController mDisplayModeController;
private final Rect mCachedSafeAreaRect = new Rect();
// The current AwWindowCoverageTracker, if any. This will be non-null when the AwContents is
// attached to the Window and size tracking is enabled. It will be null otherwise.
private AwWindowCoverageTracker mAwWindowCoverageTracker;
private AwFrameMetricsListener mAwFrameMetricsListener;
private AwDarkMode mAwDarkMode;
private AwWebContentsMetricsRecorder mAwWebContentsMetricsRecorder;
private StylusWritingController mStylusWritingController;
// Permissions are requested on a drop event, and are released when another drag starts
// (drag-started event) or when the current page navigates to a new URL.
private DragAndDropPermissions mDragAndDropPermissions;
private static class WebContentsInternalsHolder implements WebContents.InternalsHolder {
private final WeakReference<AwContents> mAwContentsRef;
private WebContentsInternalsHolder(AwContents awContents) {
mAwContentsRef = new WeakReference<>(awContents);
}
@Override
public void set(WebContentsInternals internals) {
AwContents awContents = mAwContentsRef.get();
if (awContents == null) {
throw new IllegalStateException("AwContents should be available at this time");
}
awContents.mWebContentsInternals = internals;
}
@Override
public WebContentsInternals get() {
AwContents awContents = mAwContentsRef.get();
return awContents == null ? null : awContents.mWebContentsInternals;
}
public boolean weakRefCleared() {
return mAwContentsRef.get() == null;
}
}
private static final class AwContentsDestroyRunnable implements Runnable {
private final long mNativeAwContents;
// Hold onto a reference to the window (via its wrapper), so that it is not destroyed
// until we are done here.
private final WindowAndroidWrapper mWindowAndroid;
private AwContentsDestroyRunnable(
long nativeAwContents, WindowAndroidWrapper windowAndroid) {
mNativeAwContents = nativeAwContents;
mWindowAndroid = windowAndroid;
mWindowAndroid.incrementRefFromDestroyRunnable();
}
@Override
public void run() {
AwContentsJni.get().destroy(mNativeAwContents);
mWindowAndroid.decrementRefFromDestroyRunnable();
}
}
/** A class that stores the state needed to enter and exit fullscreen. */
private static class FullScreenTransitionsState {
private final ViewGroup mInitialContainerView;
private final InternalAccessDelegate mInitialInternalAccessAdapter;
private final AwViewMethods mInitialAwViewMethods;
private FullScreenView mFullScreenView;
/** Whether the initial container view was focused when we entered fullscreen */
private boolean mWasInitialContainerViewFocused;
private int mScrollX;
private int mScrollY;
private FullScreenTransitionsState(
ViewGroup initialContainerView,
InternalAccessDelegate initialInternalAccessAdapter,
AwViewMethods initialAwViewMethods) {
mInitialContainerView = initialContainerView;
mInitialInternalAccessAdapter = initialInternalAccessAdapter;
mInitialAwViewMethods = initialAwViewMethods;
}
private void enterFullScreen(
FullScreenView fullScreenView,
boolean wasInitialContainerViewFocused,
int scrollX,
int scrollY) {
mFullScreenView = fullScreenView;
mWasInitialContainerViewFocused = wasInitialContainerViewFocused;
mScrollX = scrollX;
mScrollY = scrollY;
}
private boolean wasInitialContainerViewFocused() {
return mWasInitialContainerViewFocused;
}
private int getScrollX() {
return mScrollX;
}
private int getScrollY() {
return mScrollY;
}
private void exitFullScreen() {
mFullScreenView = null;
}
private boolean isFullScreen() {
return mFullScreenView != null;
}
private ViewGroup getInitialContainerView() {
return mInitialContainerView;
}
private InternalAccessDelegate getInitialInternalAccessDelegate() {
return mInitialInternalAccessAdapter;
}
private AwViewMethods getInitialAwViewMethods() {
return mInitialAwViewMethods;
}
private FullScreenView getFullScreenView() {
return mFullScreenView;
}
}
// Reference to the active mNativeAwContents pointer while it is active use
// (ie before it is destroyed).
private CleanupReference mCleanupReference;
// --------------------------------------------------------------------------------------------
private class IoThreadClientImpl extends AwContentsIoThreadClient {
// All methods are called on the IO thread.
@Override
public int getCacheMode() {
return mSettings.getCacheMode();
}
@Override
public AwContentsBackgroundThreadClient getBackgroundThreadClient() {
return mBackgroundThreadClient;
}
@Override
public boolean shouldBlockContentUrls() {
return !mSettings.getAllowContentAccess();
}
@Override
public boolean shouldBlockFileUrls() {
return !mSettings.getAllowFileAccess();
}
@Override
public boolean shouldBlockSpecialFileUrls() {
return mSettings.getBlockSpecialFileUrls();
}
@Override
public boolean shouldBlockNetworkLoads() {
return mSettings.getBlockNetworkLoads();
}
@Override
public boolean shouldAcceptCookies() {
return mBrowserContext.getCookieManager().acceptCookie();
}
@Override
public boolean shouldAcceptThirdPartyCookies() {
return mSettings.getAcceptThirdPartyCookies();
}
@Override
public boolean getSafeBrowsingEnabled() {
return mSettings.getSafeBrowsingEnabled();
}
}
private class BackgroundThreadClientImpl extends AwContentsBackgroundThreadClient {
// All methods are called on the background thread.
@Override
public WebResourceResponseInfo shouldInterceptRequest(
AwContentsClient.AwWebResourceRequest request) {
String url = request.url;
WebResourceResponseInfo webResourceResponseInfo;
// Return the response directly if the url is default video poster url.
webResourceResponseInfo = mDefaultVideoPosterRequestHandler.shouldInterceptRequest(url);
if (webResourceResponseInfo != null) return webResourceResponseInfo;
webResourceResponseInfo = mContentsClient.shouldInterceptRequest(request);
if (webResourceResponseInfo == null) {
mContentsClient.getCallbackHelper().postOnLoadResource(url);
}
if (webResourceResponseInfo != null && webResourceResponseInfo.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(
request,
/* error description filled in by the glue layer */
new AwContentsClient.AwWebResourceError());
}
return webResourceResponseInfo;
}
}
// --------------------------------------------------------------------------------------------
// When the navigation is for a newly created WebView (i.e. a popup), intercept the navigation
// here for implementing shouldOverrideUrlLoading. This is to send the shouldOverrideUrlLoading
// callback to the correct WebViewClient that is associated with the WebView.
// Otherwise, use this delegate only to post onPageStarted messages.
//
// We are not using WebContentsObserver.didStartLoading because of stale URLs, out of order
// onPageStarted's and double onPageStarted's.
//
private class InterceptNavigationDelegateImpl extends InterceptNavigationDelegate {
@Override
public boolean shouldIgnoreNavigation(
NavigationHandle navigationHandle,
GURL escapedUrl,
boolean hiddenCrossFrame,
boolean isSandboxedFrame) {
// 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.
if (!AwComputedFlags.pageStartedOnCommitEnabled(
navigationHandle.isRendererInitiated())) {
GURL url =
navigationHandle.getBaseUrlForDataUrl().isEmpty()
? navigationHandle.getUrl()
: navigationHandle.getBaseUrlForDataUrl();
mContentsClient.getCallbackHelper().postOnPageStarted(url.getPossiblyInvalidSpec());
}
return false;
}
}
// --------------------------------------------------------------------------------------------
private class AwLayoutSizerDelegate implements AwLayoutSizer.Delegate {
@Override
public void requestLayout() {
ViewUtils.requestLayout(
mContainerView, "AwContents.AwLayoutSizerDelegate.requestLayout");
}
@Override
public void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mInternalAccessAdapter.setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
public boolean isLayoutParamsHeightWrapContent() {
return mContainerView.getLayoutParams() != null
&& (mContainerView.getLayoutParams().height
== ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void setForceZeroLayoutHeight(boolean forceZeroHeight) {
getSettings().setForceZeroLayoutHeight(forceZeroHeight);
}
}
// --------------------------------------------------------------------------------------------
private class AwScrollOffsetManagerDelegate implements AwScrollOffsetManager.Delegate {
@Override
public void overScrollContainerViewBy(
int deltaX,
int deltaY,
int scrollX,
int scrollY,
int scrollRangeX,
int scrollRangeY,
boolean isTouchEvent) {
mInternalAccessAdapter.overScrollBy(
deltaX,
deltaY,
scrollX,
scrollY,
scrollRangeX,
scrollRangeY,
0,
0,
isTouchEvent);
}
@Override
public void scrollContainerViewTo(int x, int y) {
try {
mInternalAccessAdapter.super_scrollTo(x, y);
} catch (Throwable e) {
AwThreadUtils.postToCurrentLooper(
() -> {
Log.e(
TAG,
"The following exception was raised by scrollContainerViewTo:");
throw e;
});
}
}
@Override
public void scrollNativeTo(int x, int y) {
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get().scrollTo(mNativeAwContents, x, y);
}
}
@Override
public void smoothScroll(int targetX, int targetY, long durationMs) {
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get().smoothScroll(mNativeAwContents, targetX, targetY, durationMs);
}
}
@Override
public int getContainerViewScrollX() {
return mContainerView.getScrollX();
}
@Override
public int getContainerViewScrollY() {
return mContainerView.getScrollY();
}
@Override
public void invalidate() {
mContainerView.postInvalidateOnAnimation();
}
@Override
public void cancelFling() {
mWebContents.getEventForwarder().cancelFling(SystemClock.uptimeMillis());
}
}
// --------------------------------------------------------------------------------------------
private class AwGestureStateListener extends GestureStateListener {
@Override
public void onPinchStarted() {
// 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();
}
@Override
public void onPinchEnded() {
mLayoutSizer.unfreezeLayoutRequests();
}
@Override
public void onScrollUpdateGestureConsumed() {
if (!AwFeatureMap.isEnabled(
AwFeatures.WEBVIEW_DO_NOT_SEND_ACCESSIBILITY_EVENTS_ON_GSU)) {
mScrollAccessibilityHelper.postViewScrolledAccessibilityEventCallback();
}
if (AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_INVOKE_ZOOM_PICKER_ON_GSU)) {
mZoomControls.invokeZoomPicker();
}
}
@Override
public void onScrollStarted(int scrollOffsetY, int scrollExtentY, boolean isDirectionUp) {
if (!AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_INVOKE_ZOOM_PICKER_ON_GSU)) {
// This needs to be paired with call to setAutoDismissed(true) and a call to invoke
// zoom picker, so that a delayed hide task is posted by android. This is happening
// on scroll end below.
mZoomControls.setAutoDismissed(false);
}
mZoomControls.invokeZoomPicker();
if (mAwFrameMetricsListener != null) {
mAwFrameMetricsListener.onWebContentsScrollStateUpdate(
/* isScrolling= */ true, mId);
}
if (AwFeatureMap.isEnabled(
AwFeatures.WEBVIEW_DO_NOT_SEND_ACCESSIBILITY_EVENTS_ON_GSU)) {
mScrollAccessibilityHelper.setIsInAScroll(true);
}
}
@Override
public void onScrollEnded(int scrollOffsetY, int scrollExtentY) {
if (!AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_INVOKE_ZOOM_PICKER_ON_GSU)) {
mZoomControls.setAutoDismissed(true);
// A call to invoke is required so that a delayed hide task can be posted by
// android.
mZoomControls.invokeZoomPicker();
}
if (mAwFrameMetricsListener != null) {
mAwFrameMetricsListener.onWebContentsScrollStateUpdate(
/* isScrolling= */ false, mId);
}
if (AwFeatureMap.isEnabled(
AwFeatures.WEBVIEW_DO_NOT_SEND_ACCESSIBILITY_EVENTS_ON_GSU)) {
mScrollAccessibilityHelper.setIsInAScroll(false);
}
}
@Override
public void onScaleLimitsChanged(float minPageScaleFactor, float maxPageScaleFactor) {
mZoomControls.updateZoomControls();
}
}
// --------------------------------------------------------------------------------------------
private class AwComponentCallbacks implements ComponentCallbacks2 {
@Override
public void onTrimMemory(final int level) {
AwContents.this.onTrimMemory(level);
}
@Override
public void onLowMemory() {}
@Override
public void onConfigurationChanged(Configuration configuration) {
updateDefaultLocale();
}
}
;
// --------------------------------------------------------------------------------------------
private class AwDisplayAndroidObserver implements DisplayAndroidObserver {
@Override
public void onRotationChanged(int rotation) {}
@Override
public void onDIPScaleChanged(float dipScale) {
if (TRACE) Log.i(TAG, "%s onDIPScaleChanged dipScale=%f", this, dipScale);
AwContentsJni.get().setDipScale(mNativeAwContents, dipScale);
mLayoutSizer.setDIPScale(dipScale);
mSettings.setDIPScale(dipScale);
}
}
;
/** Tracks and reports the percentage of coverage of AwContents on the root view. */
@VisibleForTesting
public static class AwWindowCoverageTracker {
private static final long RECALCULATION_DELAY_MS = 200;
@VisibleForTesting
public static final Map<View, AwWindowCoverageTracker> sWindowCoverageTrackers =
new HashMap<>();
private final View mRootView;
private List<AwContents> mAwContentsList = new ArrayList<>();
private long mRecalculationTime;
private boolean mPendingRecalculation;
private AwWindowCoverageTracker(View rootView) {
mRootView = rootView;
sWindowCoverageTrackers.put(rootView, this);
}
public static AwWindowCoverageTracker getOrCreateForRootView(
AwContents contents, View rootView) {
AwWindowCoverageTracker tracker = sWindowCoverageTrackers.get(rootView);
if (tracker == null) {
if (TRACE) {
Log.i(TAG, "%s creating WindowCoverageTracker for %s", contents, rootView);
}
tracker = new AwWindowCoverageTracker(rootView);
}
return tracker;
}
public void trackContents(AwContents contents) {
contents.mAwWindowCoverageTracker = this;
mAwContentsList.add(contents);
}
public void untrackContents(AwContents contents) {
contents.mAwWindowCoverageTracker = null;
mAwContentsList.remove(contents);
// If that was the last AwContents, remove ourselves from the static map.
if (!isTracking()) {
if (TRACE) Log.i(TAG, "%s removing " + this, contents);
sWindowCoverageTrackers.remove(mRootView);
}
}
private boolean isTracking() {
return mAwContentsList.size() > 0;
}
/**
* Notifies this object that a recalculation of the window coverage is necessary.
*
* This should be called every time any of the tracked AwContents changes its size,
* visibility, or scheme.
*
* Recalculation won't happen immediately, and will be rate limited.
*/
public void onInputsUpdated() {
long time = SystemClock.uptimeMillis();
if (mPendingRecalculation) return;
mPendingRecalculation = true;
if (time > mRecalculationTime + RECALCULATION_DELAY_MS) {
// Enough time has elapsed since the last recalculation, run it now.
mRecalculationTime = time;
} else {
// Not enough time has elapsed, run it once enough time has elapsed.
mRecalculationTime += RECALCULATION_DELAY_MS;
}
PostTask.postDelayedTask(
TaskTraits.UI_DEFAULT,
() -> {
recalculate();
mPendingRecalculation = false;
},
mRecalculationTime - time);
}
private static int[] toIntArray(List<Integer> list) {
int[] array = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i);
}
return array;
}
private void recalculate() {
if (TRACE) Log.i(TAG, "%s recalculate", this);
List<Rect> contentRects = new ArrayList<>();
Rect rootVisibleRect =
new Rect(
(int) mRootView.getX(),
(int) mRootView.getY(),
(int) mRootView.getX() + mRootView.getWidth(),
(int) mRootView.getY() + mRootView.getHeight());
int rootArea = RectUtils.getRectArea(rootVisibleRect);
int globalPercentage = 0;
// Note that a scheme could occur more than once at a time.
List<String> schemes = new ArrayList<>();
List<Integer> schemePercentages = new ArrayList<>();
// If the root view has a width or height of 0 then nothing is visible, so leave the
// lists empty and pass them on like that. Also, we don't want to divide by 0.
if (rootArea > 0) {
for (AwContents content : mAwContentsList) {
// A workaround for a deeper problem: https://crbug.com/1232765#c19
if (content.isDestroyed(NO_WARN)) continue;
if (content.mIsAttachedToWindow
&& content.mIsViewVisible
&& content.mIsWindowVisible) {
// The result of getGlobalVisibleRect can change underneath us, so take a
// protective copy.
Rect contentRect = new Rect(content.getGlobalVisibleRect());
// If the intersect method returns true then it may have modified
// contentRect. A Rect with area 0 will not intersect with anything.
if (contentRect.intersect(rootVisibleRect)) {
contentRects.add(contentRect);
schemes.add(AwContentsJni.get().getScheme(content.mNativeAwContents));
schemePercentages.add(
RectUtils.getRectArea(contentRect) * 100 / rootArea);
}
}
}
globalPercentage =
RectUtils.calculatePixelsOfCoverage(rootVisibleRect, contentRects)
* 100
/ rootArea;
}
AwContentsJni.get()
.updateScreenCoverage(
globalPercentage,
schemes.toArray(new String[schemes.size()]),
toIntArray(schemePercentages));
}
}
// A Webview class that implements the listener part of the JankTracker requirement. It mirrors
// JankActivityTracker in starting and stopping the listener and collection.
private static class AwFrameMetricsListener {
private static final WeakHashMap<Window, AwFrameMetricsListener> sWindowMap =
new WeakHashMap<>();
private boolean mAttached;
private JankTrackerStateController mController;
private JankTracker mJankTracker;
private WeakReference<Window> mWindow;
private int mAttachedWebviews;
private int mVisibleWebviews;
private static final WeakHashMap<Window, Integer> sNumActiveScrolls = new WeakHashMap<>();
private AwFrameMetricsListener() {
FrameMetricsStore metricsStore = new FrameMetricsStore();
mController =
new JankTrackerStateController(
new FrameMetricsListener(metricsStore),
new JankReportingScheduler(metricsStore));
mJankTracker = new JankTrackerImpl(mController);
mAttached = false;
}
private void attachListener(Window window) {
if (mAttached) return;
mWindow = new WeakReference<Window>(window);
mController.startMetricCollection(window);
mAttached = true;
}
private void detachListener(Window window) {
if (!mAttached || window != mWindow.get()) return;
mController.stopMetricCollection(window);
mAttached = false;
}
private void incrementAttachedWebviews() {
mAttachedWebviews++;
}
private void decrementAttachedWebviews() {
mAttachedWebviews--;
assert mAttachedWebviews >= 0;
}
private int getAttachedWebviews() {
return mAttachedWebviews;
}
public static AwFrameMetricsListener onAttachedToWindow(
Window window, AwContents awContents) {
AwFrameMetricsListener listener = sWindowMap.get(window);
if (listener == null) {
listener = new AwFrameMetricsListener();
listener.attachListener(window);
sWindowMap.put(window, listener);
}
listener.incrementAttachedWebviews();
return listener;
}
public static void onDetachedFromWindow(Window window, AwContents awContents) {
AwFrameMetricsListener listener = sWindowMap.get(window);
listener.decrementAttachedWebviews();
if (listener.getAttachedWebviews() >= 1) return;
listener.detachListener(window);
sWindowMap.remove(window);
}
public void onWebviewVisible() {
if (!mAttached) return;
mVisibleWebviews++;
if (mVisibleWebviews > 1) return;
mController.startPeriodicReporting();
mController.startMetricCollection(null);
}
public void onWebviewHidden() {
if (!mAttached) return;
mVisibleWebviews--;
assert mVisibleWebviews >= 0;
if (mVisibleWebviews == 0) {
mController.stopMetricCollection(null);
mController.stopPeriodicReporting();
}
}
public void onWebContentsScrollStateUpdate(boolean isScrolling, long scrollId) {
if (!mAttached) return;
// scrollIds are unique across multiple webviews in a window.
Window window = mWindow.get();
if (window == null) return;
int numActiveScrolls = sNumActiveScrolls.getOrDefault(window, 0);
if (isScrolling) {
numActiveScrolls += 1;
mJankTracker.startTrackingScenario(
new JankScenario(JankScenario.Type.WEBVIEW_SCROLLING, scrollId));
} else {
assert numActiveScrolls >= 1;
numActiveScrolls -= 1;
mJankTracker.finishTrackingScenario(
new JankScenario(JankScenario.Type.WEBVIEW_SCROLLING, scrollId),
TimeUtils.uptimeMillis() * TimeUtils.NANOSECONDS_PER_MILLISECOND);
}
if (numActiveScrolls == 0) {
mJankTracker.finishTrackingScenario(
JankScenario.COMBINED_WEBVIEW_SCROLLING,
TimeUtils.uptimeMillis() * TimeUtils.NANOSECONDS_PER_MILLISECOND);
sNumActiveScrolls.remove(window);
return;
}
if (numActiveScrolls == 1 && isScrolling) {
mJankTracker.startTrackingScenario(JankScenario.COMBINED_WEBVIEW_SCROLLING);
}
sNumActiveScrolls.put(window, numActiveScrolls);
}
}
// --------------------------------------------------------------------------------------------
/**
* @param browserContext the browsing context to associate this view contents with.
* @param containerView the view-hierarchy item this object will be bound to.
* @param context the context to use, usually containerView.getContext().
* @param internalAccessAdapter to access private methods on containerView.
* @param nativeDrawFunctorFactory to access the functor provided by the WebView.
* @param contentsClient will receive API callbacks from this WebView Contents.
* @param awSettings AwSettings instance used to configure the AwContents.
*
* This constructor uses the default view sizing policy.
*/
public AwContents(
AwBrowserContext browserContext,
ViewGroup containerView,
Context context,
InternalAccessDelegate internalAccessAdapter,
NativeDrawFunctorFactory nativeDrawFunctorFactory,
AwContentsClient contentsClient,
AwSettings awSettings) {
this(
browserContext,
containerView,
context,
internalAccessAdapter,
nativeDrawFunctorFactory,
contentsClient,
awSettings,
new DependencyFactory());
}
/**
* @param dependencyFactory an instance of the DependencyFactory used to provide instances of
* classes that this class depends on.
* <p>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,
Context context,
InternalAccessDelegate internalAccessAdapter,
NativeDrawFunctorFactory nativeDrawFunctorFactory,
AwContentsClient contentsClient,
AwSettings settings,
DependencyFactory dependencyFactory) {
assert browserContext != null;
long startTime = SystemClock.uptimeMillis();
sLastId += 1;
mId = sLastId;
if (!browserContext.isDefaultAwBrowserContext()) {
// The browser context has been explicitly set by the application.
mBrowserContextSetExplicitly = true;
}
try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped("AwContents.constructor")) {
mDisplayModeController =
new AwDisplayModeController(
new AwDisplayModeController.Delegate() {
@Override
public int getDisplayWidth() {
WindowAndroid windowAndroid = mWindowAndroid.getWindowAndroid();
return windowAndroid.getDisplay().getDisplayWidth();
}
@Override
public int getDisplayHeight() {
WindowAndroid windowAndroid = mWindowAndroid.getWindowAndroid();
return windowAndroid.getDisplay().getDisplayHeight();
}
},
containerView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_DISPLAY_CUTOUT)) {
mDisplayCutoutController =
new AwDisplayCutoutController(
new AwDisplayCutoutController.Delegate() {
@Override
public float getDipScale() {
WindowAndroid windowAndroid =
mWindowAndroid.getWindowAndroid();
return windowAndroid.getDisplay().getDipScale();
}
@Override
public void setDisplayCutoutSafeArea(
AwDisplayCutoutController.Insets insets) {
if (mWebContents == null) return;
mWebContents.setDisplayCutoutSafeArea(
insets.toRect(mCachedSafeAreaRect));
}
},
containerView);
}
mRendererPriority = RendererPriority.HIGH;
mSettings = settings;
updateDefaultLocale();
mBrowserContext = browserContext;
// setWillNotDraw(false) is required since WebView draws its own contents using its
// container view. If this is ever not the case we should remove this, as it removes
// Android's gatherTransparentRegion optimization for the view.
mContainerView = containerView;
mContainerView.setWillNotDraw(false);
mContext = context;
mAppTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
mInternalAccessAdapter = internalAccessAdapter;
mNativeDrawFunctorFactory = nativeDrawFunctorFactory;
mContentsClient = contentsClient;
mContentsClient
.getCallbackHelper()
.setCancelCallbackPoller(() -> AwContents.this.isDestroyed(NO_WARN));
mAwViewMethods = new AwViewMethodsImpl();
mFullScreenTransitionsState =
new FullScreenTransitionsState(
mContainerView, mInternalAccessAdapter, mAwViewMethods);
mLayoutSizer = dependencyFactory.createLayoutSizer();
mLayoutSizer.setDelegate(new AwLayoutSizerDelegate());
mWebContentsDelegate =
new AwWebContentsDelegateAdapter(
this, contentsClient, settings, mContext, mContainerView);
mContentsClientBridge =
new AwContentsClientBridge(
mContext, contentsClient, AwContentsStatics.getClientCertLookupTable());
mZoomControls = new AwZoomControls(this);
mBackgroundThreadClient = new BackgroundThreadClientImpl();
mIoThreadClient = new IoThreadClientImpl();
mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl();
mDisplayObserver = new AwDisplayAndroidObserver();
mUpdateVisibilityRunnable = () -> updateWebContentsVisibility();
AwSettings.ZoomSupportChangeListener zoomListener =
(supportsDoubleTapZoom, supportsMultiTouchZoom) -> {
if (isDestroyed(NO_WARN)) return;
GestureListenerManager gestureManager =
GestureListenerManager.fromWebContents(mWebContents);
gestureManager.updateDoubleTapSupport(supportsDoubleTapZoom);
gestureManager.updateMultiTouchZoomSupport(supportsMultiTouchZoom);
};
mSettings.setZoomListener(zoomListener);
mDefaultVideoPosterRequestHandler =
new DefaultVideoPosterRequestHandler(mContentsClient);
mSettings.setDefaultVideoPosterURL(
mDefaultVideoPosterRequestHandler.getDefaultVideoPosterURL());
mScrollOffsetManager =
dependencyFactory.createScrollOffsetManager(
new AwScrollOffsetManagerDelegate());
mScrollAccessibilityHelper = new ScrollAccessibilityHelper(mContainerView);
setOverScrollMode(mContainerView.getOverScrollMode());
setScrollBarStyle(mInternalAccessAdapter.super_getScrollBarStyle());
mAwDarkMode = new AwDarkMode(context);
mStylusWritingController = new StylusWritingController(context);
setNewAwContents(
AwContentsJni.get().init(mBrowserContext.getNativeBrowserContextPointer()));
onContainerViewChanged();
}
long delta = SystemClock.uptimeMillis() - startTime;
RecordHistogram.recordTimesHistogram(CONSTRUCTOR_HISTOGRAM_NAME, delta);
if (mId == 1) {
RecordHistogram.recordTimesHistogram(CONSTRUCTOR_HISTOGRAM_NAME + ".First", delta);
}
}
private void initWebContents(
ViewAndroidDelegate viewDelegate,
InternalAccessDelegate internalDispatcher,
WebContents webContents,
WindowAndroid windowAndroid,
WebContentsInternalsHolder internalsHolder,
AwSelectionActionMenuDelegate selectionActionMenuDelegate) {
webContents.setDelegates(
PRODUCT_VERSION, viewDelegate, internalDispatcher, windowAndroid, internalsHolder);
mViewEventSink = ViewEventSink.from(mWebContents);
mViewEventSink.setHideKeyboardOnBlur(false);
SelectionPopupController controller = SelectionPopupController.fromWebContents(webContents);
controller.setActionModeCallback(new AwActionModeCallback(mContext, this, webContents));
controller.setSelectionClient(SelectionClient.createSmartSelectionClient(webContents));
controller.setSelectionActionMenuDelegate(selectionActionMenuDelegate);
AwSelectionDropdownMenuDelegate.maybeSetWebViewDropdownSelectionMenuDelegate(controller);
// Listen for dpad events from IMEs (e.g. Samsung Cursor Control) so we know to enable
// spatial navigation mode to allow these events to move focus out of the WebView.
ImeAdapter.fromWebContents(webContents)
.addEventObserver(
new ImeEventObserver() {
@Override
public void onBeforeSendKeyEvent(KeyEvent event) {
if (AwContents.isDpadEvent(event)) {
mSettings.setSpatialNavigationEnabled(true);
}
}
});
}
private void initializeAutofillProviderIfNecessary(
AwSelectionActionMenuDelegate selectionActionMenuDelegate) {
if (AndroidAutofillSafeModeAction.isAndroidAutofillDisabled()) {
Log.i(TAG, "Android autofill is disabled by SafeMode");
return;
}
if (mAutofillProvider == null) {
mAutofillProvider =
new AutofillProvider(mContext, mContainerView, mWebContents, "Android WebView");
} else {
mAutofillProvider.setWebContents(mWebContents);
}
selectionActionMenuDelegate.setAutofillSelectionMenuItemHelper(
new AutofillSelectionMenuItemHelper(mContext, mAutofillProvider));
AwContentsJni.get().initializeAndroidAutofill(mNativeAwContents);
}
private boolean isSamsungMailApp() {
// There are 2 different Samsung mail apps exhibiting bugs related to
// http://crbug.com/781535.
String currentPackageName = mContext.getPackageName();
return "com.android.email".equals(currentPackageName)
|| "com.samsung.android.email.composer".equals(currentPackageName);
}
public boolean isFullScreen() {
return mFullScreenTransitionsState.isFullScreen();
}
/**
* For multi-profile public API. For internal access to the browser context,
* use the member variable {@link AwContents#mBrowserContext} directly. All Exception messages
* should be developer friendly and refer to the browser context as a "Profile".
*
* @throws IllegalStateException if the WebView has been destroyed via. {@link
* AwContents#destroy()}.
*/
@NonNull
public AwBrowserContext getBrowserContext() {
if (isDestroyed(NO_WARN)) {
throw new IllegalStateException("Cannot get profile for destroyed WebView.");
}
mBrowserContextAccessed = true;
return mBrowserContext;
}
/**
* For multi-profile public API. Sets a new browser context which will
* cause the web contents to reinitialize. All Exception messages should
* be developer friendly and refer to the browser context as a "Profile".
*
* @throws IllegalStateException if the WebView has been destroyed via. {@link
* AwContents#destroy()}.
* @throws IllegalStateException if the browser context has been accessed via. {@link
* AwContents#getBrowserContext()}.
* @throws IllegalStateException if the browser context has already been set explicitly via.
* {@link AwContents#setBrowserContext(AwBrowserContext)}.
* @throws IllegalStateException if the {@link AwContents#evaluateJavaScript(String, Callback)}
* has been called on the WebView.
* @throws IllegalStateException if the WebView has previously navigated to a web page.
*/
public void setBrowserContext(@NonNull AwBrowserContext browserContext) {
if (browserContext == mBrowserContext) {
return;
}
if (isDestroyed(NO_WARN)) {
throw new IllegalStateException(
"Cannot set new profile on a WebView that has been destroyed");
}
if (mBrowserContextAccessed) {
throw new IllegalStateException(
"Cannot set new profile after the current one has been retrieved via. "
+ "getProfile");
}
if (mBrowserContextSetExplicitly) {
throw new IllegalStateException(
"Cannot set new profile after one has already been set" + "via. setProfile");
}
if (mHasEvaluatedJavascript) {
throw new IllegalStateException(
"Cannot set new profile after call to evaluateJavascript");
}
final NavigationHistory navigationHistory = getNavigationHistory();
if (navigationHistory.getEntryCount() != 0
&& !navigationHistory.getEntryAtIndex(0).isInitialEntry()) {
throw new IllegalStateException(
"Cannot set new profile on a WebView that has been previously navigated.");
}
// Save existing state and reset.
StateSnapshot previousState = captureStateAndResetView();
mBrowserContext = browserContext;
mBrowserContextSetExplicitly = true;
setNewAwContents(
AwContentsJni.get().init(mBrowserContext.getNativeBrowserContextPointer()));
// Finally refresh all view state.
restoreState(previousState);
}
/**
* Transitions this {@link AwContents} to fullscreen mode and returns the
* {@link View} where the contents will be drawn while in fullscreen, or null
* if this AwContents has already been destroyed.
*/
View enterFullScreen() {
assert !isFullScreen();
if (isDestroyed(NO_WARN)) return null;
// Detach to tear down the GL functor if this is still associated with the old
// container view. It will be recreated during the next call to onDraw attached to
// the new container view.
onDetachedFromWindow();
// In fullscreen mode FullScreenView owns the AwViewMethodsImpl and AwContents
// a NullAwViewMethods.
FullScreenView fullScreenView = new FullScreenView(mContext, mAwViewMethods, this);
fullScreenView.setFocusable(true);
fullScreenView.setFocusableInTouchMode(true);
boolean wasInitialContainerViewFocused = mContainerView.isFocused();
if (wasInitialContainerViewFocused) {
fullScreenView.requestFocus();
}
mFullScreenTransitionsState.enterFullScreen(
fullScreenView,
wasInitialContainerViewFocused,
mScrollOffsetManager.getScrollX(),
mScrollOffsetManager.getScrollY());
mAwViewMethods = new NullAwViewMethods(this, mInternalAccessAdapter, mContainerView);
// Associate this AwContents with the FullScreenView.
setInternalAccessAdapter(fullScreenView.getInternalAccessAdapter());
setContainerView(fullScreenView);
return fullScreenView;
}
/** Called when the app has requested to exit fullscreen. */
public void requestExitFullscreen() {
if (!isDestroyed(NO_WARN)) mWebContents.exitFullscreen();
}
/**
* Returns this {@link AwContents} to embedded mode, where the {@link AwContents} are drawn
* in the WebView.
*/
void exitFullScreen() {
if (!isFullScreen() || isDestroyed(NO_WARN)) {
// exitFullScreen() can be called without a prior call to enterFullScreen() if a
// "misbehave" app overrides onShowCustomView but does not add the custom view to
// the window. Exiting avoids a crash.
return;
}
// Detach to tear down the GL functor if this is still associated with the old
// container view. It will be recreated during the next call to onDraw attached to
// the new container view.
// NOTE: we cannot use mAwViewMethods here because its type is NullAwViewMethods.
AwViewMethods awViewMethodsImpl = mFullScreenTransitionsState.getInitialAwViewMethods();
awViewMethodsImpl.onDetachedFromWindow();
// Swap the view delegates. In embedded mode the FullScreenView owns a
// NullAwViewMethods and AwContents the AwViewMethodsImpl.
FullScreenView fullscreenView = mFullScreenTransitionsState.getFullScreenView();
fullscreenView.setAwViewMethods(
new NullAwViewMethods(
this, fullscreenView.getInternalAccessAdapter(), fullscreenView));
mAwViewMethods = awViewMethodsImpl;
ViewGroup initialContainerView = mFullScreenTransitionsState.getInitialContainerView();
// Re-associate this AwContents with the WebView.
setInternalAccessAdapter(mFullScreenTransitionsState.getInitialInternalAccessDelegate());
setContainerView(initialContainerView);
// Return focus to the WebView.
if (mFullScreenTransitionsState.wasInitialContainerViewFocused()) {
mContainerView.requestFocus();
}
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get()
.restoreScrollAfterTransition(
mNativeAwContents,
mFullScreenTransitionsState.getScrollX(),
mFullScreenTransitionsState.getScrollY());
}
mFullScreenTransitionsState.exitFullScreen();
}
private void setInternalAccessAdapter(InternalAccessDelegate internalAccessAdapter) {
mInternalAccessAdapter = internalAccessAdapter;
mViewEventSink.setAccessDelegate(mInternalAccessAdapter);
}
private void setContainerView(ViewGroup newContainerView) {
// setWillNotDraw(false) is required since WebView draws its own contents using its
// container view. If this is ever not the case we should remove this, as it removes
// Android's gatherTransparentRegion optimization for the view.
mContainerView = newContainerView;
mContainerView.setWillNotDraw(false);
assert mDrawFunctor == null;
mViewAndroidDelegate.setContainerView(mContainerView);
if (mAwPdfExporter != null) {
mAwPdfExporter.setContainerView(mContainerView);
}
mWebContentsDelegate.setContainerView(mContainerView);
for (PopupTouchHandleDrawable drawable : mTouchHandleDrawables) {
drawable.onContainerViewChanged(newContainerView);
}
onContainerViewChanged();
}
/** Reconciles the state of this AwContents object with the state of the new container view. */
private void onContainerViewChanged() {
// NOTE: mAwViewMethods is used by the old container view, the WebView, so it might refer
// to a NullAwViewMethods when in fullscreen. To ensure that the state is reconciled with
// the new container view correctly, we bypass mAwViewMethods and use the real
// implementation directly.
AwViewMethods awViewMethodsImpl = mFullScreenTransitionsState.getInitialAwViewMethods();
awViewMethodsImpl.onVisibilityChanged(mContainerView, mContainerView.getVisibility());
awViewMethodsImpl.onWindowVisibilityChanged(mContainerView.getWindowVisibility());
boolean containerViewAttached = mContainerView.isAttachedToWindow();
if (containerViewAttached && !mIsAttachedToWindow) {
awViewMethodsImpl.onAttachedToWindow();
} else if (!containerViewAttached && mIsAttachedToWindow) {
awViewMethodsImpl.onDetachedFromWindow();
}
// Skip passing size of FullScreenView down. FullScreenView is newly created and detached
// so has initial size 0x0 before layout. Avoid this temporary resize to 0x0 which can
// cause flickers and sometimes layout problems in the web page.
if ((mContainerView instanceof FullScreenView)) {
assert !containerViewAttached;
} else {
awViewMethodsImpl.onSizeChanged(
mContainerView.getWidth(), mContainerView.getHeight(), 0, 0);
}
awViewMethodsImpl.onWindowFocusChanged(mContainerView.hasWindowFocus());
awViewMethodsImpl.onFocusChanged(mContainerView.hasFocus(), 0, null);
ViewUtils.requestLayout(mContainerView, "AwContents.onContainerViewChanged");
if (mAutofillProvider != null) mAutofillProvider.onContainerViewChanged(mContainerView);
mDisplayModeController.setCurrentContainerView(mContainerView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (mDisplayCutoutController != null) {
mDisplayCutoutController.setCurrentContainerView(mContainerView);
}
}
}
/**
* Used for saving and restoring the WebView's state during a native web contents change.
* Currently this occurs either after receiving popup contents, or after a browser context
* change via. the multi-profile public API.
*/
private static class StateSnapshot {
public final boolean wasAttached;
public final boolean wasViewVisible;
public final boolean wasWindowVisible;
public final boolean wasPaused;
public final boolean wasFocused;
public final boolean wasWindowFocused;
public final @NonNull Map<String, Pair<Object, Class>> javascriptInterfaces;
public final @Nullable WebMessageListenerInfo[] webMessageListenerInfo;
public final @Nullable StartupJavascriptInfo[] startupJavascriptInfo;
public StateSnapshot(@NonNull AwContents awContents) {
wasAttached = awContents.mIsAttachedToWindow;
wasViewVisible = awContents.mIsViewVisible;
wasWindowVisible = awContents.mIsWindowVisible;
wasPaused = awContents.mIsPaused;
wasFocused = awContents.mContainerViewFocused;
wasWindowFocused = awContents.mWindowFocused;
// Save injected JavaScript interfaces.
javascriptInterfaces = new HashMap<>();
if (awContents.mWebContents != null) {
javascriptInterfaces.putAll(awContents.getJavascriptInjector().getInterfaces());
}
// Save injected WebMessageListeners.
webMessageListenerInfo =
AwContentsJni.get().getWebMessageListenerInfos(awContents.mNativeAwContents);
startupJavascriptInfo =
AwContentsJni.get().getDocumentStartupJavascripts(awContents.mNativeAwContents);
}
}
// This class destroys the WindowAndroid when after it is gc-ed.
private static class WindowAndroidWrapper {
private final WindowAndroid mWindowAndroid;
private final CleanupReference mCleanupReference;
// This ref-counts is used only to destroy WindowAndroid eagerly
// when AwContents is destroyed. The CleanupReference is still used
// if a Wrapper is created without any AwContents.
private int mRefFromAwContentsDestroyRunnable;
private static final class DestroyRunnable implements Runnable {
private final WindowAndroid mWindowAndroid;
private DestroyRunnable(WindowAndroid windowAndroid) {
mWindowAndroid = windowAndroid;
}
@Override
public void run() {
mWindowAndroid.destroy();
}
}
public WindowAndroidWrapper(WindowAndroid windowAndroid) {
try (ScopedSysTraceEvent e =
ScopedSysTraceEvent.scoped("WindowAndroidWrapper.constructor")) {
mWindowAndroid = windowAndroid;
mCleanupReference = new CleanupReference(this, new DestroyRunnable(windowAndroid));
}
}
public WindowAndroid getWindowAndroid() {
return mWindowAndroid;
}
public void incrementRefFromDestroyRunnable() {
mRefFromAwContentsDestroyRunnable++;
}
public void decrementRefFromDestroyRunnable() {
assert mRefFromAwContentsDestroyRunnable > 0;
mRefFromAwContentsDestroyRunnable--;
maybeCleanupEarly();
}
private void maybeCleanupEarly() {
if (mRefFromAwContentsDestroyRunnable != 0) return;
Context context = mWindowAndroid.getContext().get();
if (context != null && sContextWindowMap.get(context) != this) return;
mCleanupReference.cleanupNow();
if (context != null) sContextWindowMap.remove(context);
}
}
private static WeakHashMap<Context, WindowAndroidWrapper> sContextWindowMap;
// getWindowAndroid is only called on UI thread, so there are no threading issues with lazy
// initialization.
private static WindowAndroidWrapper getWindowAndroid(Context context) {
if (sContextWindowMap == null) sContextWindowMap = new WeakHashMap<>();
WindowAndroidWrapper wrapper = sContextWindowMap.get(context);
if (wrapper != null) return wrapper;
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped("AwContents.getWindowAndroid")) {
Activity activity = ContextUtils.activityFromContext(context);
if (activity != null) {
ActivityWindowAndroid activityWindow;
try (ScopedSysTraceEvent e2 =
ScopedSysTraceEvent.scoped("AwContents.createActivityWindow")) {
final boolean listenToActivityState = false;
activityWindow =
new ActivityWindowAndroid(
context,
listenToActivityState,
IntentRequestTracker.createFromActivity(activity));
}
wrapper = new WindowAndroidWrapper(activityWindow);
} else {
wrapper = new WindowAndroidWrapper(new WindowAndroid(context));
}
sContextWindowMap.put(context, wrapper);
}
return wrapper;
}
/**
* Set current locales to native. Propagates this information to the Accept-Language header for
* subsequent requests. Note that this will affect <b>all</b> AwContents, not just this
* instance, as all WebViews share the same NetworkContext/UrlRequestContextGetter.
*/
@VisibleForTesting
public void updateDefaultLocale() {
String locales = LocaleUtils.getDefaultLocaleListString();
if (!sCurrentLocales.equals(locales)) {
sCurrentLocales = locales;
// We cannot use the first language in sCurrentLocales for the UI language even on
// Android N. LocaleUtils.getDefaultLocaleString() is capable for UI language but
// it is not guaranteed to be listed at the first of sCurrentLocales. Therefore,
// both values are passed to native.
AwContentsJni.get()
.updateDefaultLocale(LocaleUtils.getDefaultLocaleString(), sCurrentLocales);
mSettings.updateAcceptLanguages();
}
}
private void setFunctor(AwFunctor functor) {
if (mDrawFunctor == functor) return;
AwFunctor oldFunctor = mDrawFunctor;
mDrawFunctor = functor;
updateNativeAwGLFunctor();
if (oldFunctor != null) oldFunctor.destroy();
}
private void updateNativeAwGLFunctor() {
AwContentsJni.get()
.setCompositorFrameConsumer(
mNativeAwContents,
mDrawFunctor != null ? mDrawFunctor.getNativeCompositorFrameConsumer() : 0);
}
/* Common initialization routine for adopting a native AwContents instance into this
* java instance.
*
* TAKE CARE! This method can get called multiple times per java instance. Code accordingly.
* ^^^^^^^^^ See the native class declaration for more details on relative object lifetimes.
*/
private void setNewAwContents(long newAwContentsPtr) {
// Move the TextClassifier to the new WebContents.
TextClassifier textClassifier = mWebContents != null ? getTextClassifier() : null;
if (mNativeAwContents != 0) {
destroyNatives();
mWebContents = null;
mWebContentsInternalsHolder = null;
mWebContentsInternals = null;
mNavigationController = null;
mJavascriptInjector = null;
}
assert mNativeAwContents == 0 && mCleanupReference == null && mWebContents == null;
mNativeAwContents = newAwContentsPtr;
updateNativeAwGLFunctor();
mWebContents = AwContentsJni.get().getWebContents(mNativeAwContents);
if (!mBrowserContextSetExplicitly) {
mBrowserContext = AwContentsJni.get().getBrowserContext(mNativeAwContents);
}
mWindowAndroid = getWindowAndroid(mContext);
mViewAndroidDelegate =
new AwViewAndroidDelegate(mContainerView, mContentsClient, mScrollOffsetManager);
mWebContentsInternalsHolder = new WebContentsInternalsHolder(this);
AwSelectionActionMenuDelegate selectionActionMenuDelegate =
new AwSelectionActionMenuDelegate();
initWebContents(
mViewAndroidDelegate,
mInternalAccessAdapter,
mWebContents,
mWindowAndroid.getWindowAndroid(),
mWebContentsInternalsHolder,
selectionActionMenuDelegate);
AwContentsJni.get()
.setJavaPeers(
mNativeAwContents,
this,
mWebContentsDelegate,
mContentsClientBridge,
mIoThreadClient,
mInterceptNavigationDelegate);
GestureListenerManager.fromWebContents(mWebContents)
.addListener(new AwGestureStateListener());
mNavigationController = mWebContents.getNavigationController();
installWebContentsObservers();
mSettings.setWebContents(mWebContents);
mAwDarkMode.setWebContents(mWebContents);
initializeAutofillProviderIfNecessary(selectionActionMenuDelegate);
mDisplayObserver.onDIPScaleChanged(getDeviceScaleFactor());
updateWebContentsVisibility();
// The native side object has been bound to this java instance, so now is the time to
// bind all the native->java relationships.
mCleanupReference =
new CleanupReference(
this, new AwContentsDestroyRunnable(mNativeAwContents, mWindowAndroid));
if (textClassifier != null) setTextClassifier(textClassifier);
if (mOnscreenContentProvider != null) {
mOnscreenContentProvider.onWebContentsChanged(mWebContents);
}
mStylusWritingController.onWebContentsChanged(mWebContents);
}
private void installWebContentsObservers() {
if (mWebContentsObserver != null) {
mWebContentsObserver.destroy();
}
mWebContentsObserver = new AwWebContentsObserver(mWebContents, this, mContentsClient);
if (mAwWebContentsMetricsRecorder != null) {
mAwWebContentsMetricsRecorder.destroy();
}
mAwWebContentsMetricsRecorder =
new AwWebContentsMetricsRecorder(mWebContents, mContext, mSettings);
}
/**
* Called on the "source" AwContents that is opening the popup window to
* provide the AwContents to host the pop up content.
*
* See //android_webview/docs/how-does-on-create-window-work.md for more details.
*/
public void supplyContentsForPopup(AwContents newContents) {
if (TRACE) Log.i(TAG, "%s supplyContentsForPopup", this);
if (isDestroyed(WARN)) return;
long popupNativeAwContents = AwContentsJni.get().releasePopupAwContents(mNativeAwContents);
if (popupNativeAwContents == 0) {
Log.w(TAG, "Popup WebView bind failed: no pending content.");
if (newContents != null) newContents.destroy();
return;
}
if (newContents == null) {
AwContentsJni.get().destroy(popupNativeAwContents);
return;
}
newContents.receivePopupContents(popupNativeAwContents);
}
/**
* Captures the WebView's state before doing a full reset of the view state to prepare
* for a new native web contents.
*/
private StateSnapshot captureStateAndResetView() {
// Save the existing state.
StateSnapshot state = new StateSnapshot(this);
// Reset the view state.
if (state.wasFocused) onFocusChanged(false, 0, null);
if (state.wasWindowFocused) onWindowFocusChanged(false);
if (state.wasViewVisible) setViewVisibilityInternal(false);
if (state.wasWindowVisible) setWindowVisibilityInternal(false);
if (state.wasAttached) onDetachedFromWindow();
if (!state.wasPaused) onPause();
return state;
}
/**
* Restores the WebView to its previous state before a native web contents change.
* @param previousState the state to restore to.
*/
private void restoreState(StateSnapshot previousState) {
if (!previousState.wasPaused) onResume();
if (previousState.wasAttached) {
onAttachedToWindow();
mContainerView.postInvalidateOnAnimation();
}
onSizeChanged(mContainerView.getWidth(), mContainerView.getHeight(), 0, 0);
if (previousState.wasWindowVisible) setWindowVisibilityInternal(true);
if (previousState.wasViewVisible) setViewVisibilityInternal(true);
if (previousState.wasWindowFocused) onWindowFocusChanged(true);
if (previousState.wasFocused) onFocusChanged(true, 0, null);
// Restore injected JavaScript interfaces.
for (Map.Entry<String, Pair<Object, Class>> entry :
previousState.javascriptInterfaces.entrySet()) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> requiredAnnotation = entry.getValue().second;
getJavascriptInjector()
.addPossiblyUnsafeInterface(
entry.getValue().first, entry.getKey(), requiredAnnotation);
}
// Restore injected WebMessageListeners.
WebMessageListenerInfo[] previousWebMessageListenerInfo =
previousState.webMessageListenerInfo;
if (previousWebMessageListenerInfo != null) {
for (WebMessageListenerInfo info : previousWebMessageListenerInfo) {
addWebMessageListener(
info.mObjectName, info.mAllowedOriginRules, info.mHolder.getListener());
}
}
StartupJavascriptInfo[] previousDocumentStartupJavascripts =
previousState.startupJavascriptInfo;
if (previousDocumentStartupJavascripts != null) {
for (StartupJavascriptInfo info : previousDocumentStartupJavascripts) {
addDocumentStartJavaScript(info.mScript, info.mAllowedOriginRules);
}
}
}
// Recap: supplyContentsForPopup() is called on the parent window's content, this method is
// called on the popup window's content.
// See //android_webview/docs/how-does-on-create-window-work.md for more details.
private void receivePopupContents(long popupNativeAwContents) {
if (isDestroyed(WARN)) return;
// Capture as reset the view state for mNativeAwContents.
StateSnapshot prePopupState = captureStateAndResetView();
setNewAwContents(popupNativeAwContents);
// We defer loading any URL on the popup until it has been properly initialized (through
// setNewAwContents). We resume the load here.
AwContentsJni.get().resumeLoadingCreatedPopupWebContents(mNativeAwContents);
// Finally refresh all view state for mNativeAwContents.
restoreState(prePopupState);
mIsPopupWindow = true;
}
private JavascriptInjector getJavascriptInjector() {
if (mJavascriptInjector == null) {
mJavascriptInjector = JavascriptInjector.fromWebContents(mWebContents);
}
return mJavascriptInjector;
}
@CalledByNative
private void onRendererResponsive(AwRenderProcess renderProcess) {
if (isDestroyed(NO_WARN)) return;
AwThreadUtils.postToCurrentLooper(
() -> mContentsClient.onRendererResponsive(renderProcess));
}
@CalledByNative
private void onRendererUnresponsive(AwRenderProcess renderProcess) {
if (isDestroyed(NO_WARN)) return;
AwThreadUtils.postToCurrentLooper(
() -> mContentsClient.onRendererUnresponsive(renderProcess));
}
@VisibleForTesting
@CalledByNativeUnchecked
protected boolean onRenderProcessGone(int childProcessID, boolean crashed) {
if (isDestroyed(NO_WARN)) return true;
return mContentsClient.onRenderProcessGone(
new AwRenderProcessGoneDetail(
crashed, AwContentsJni.get().getEffectivePriority(mNativeAwContents)));
}
public @RendererPriority int getEffectivePriorityForTesting() {
assert !isDestroyed(NO_WARN);
return AwContentsJni.get().getEffectivePriority(mNativeAwContents);
}
public AwDarkMode getAwDarkModeForTesting() {
return mAwDarkMode;
}
public void flushBackForwardCache() {
flushBackForwardCache(BackForwardCacheNotRestoredReason.CACHE_FLUSHED);
}
public void flushBackForwardCache(int reason) {
if (isDestroyed(NO_WARN)) return;
AwContentsJni.get().flushBackForwardCache(mNativeAwContents, reason);
}
public void cancelAllPrerendering() {
if (isDestroyed(NO_WARN)) return;
AwContentsJni.get().cancelAllPrerendering(mNativeAwContents);
}
/** Destroys this object and deletes its native counterpart. */
public void destroy() {
if (TRACE) Log.i(TAG, "%s destroy", this);
if (isDestroyed(NO_WARN)) return;
if (mOnscreenContentProvider != null) {
mOnscreenContentProvider.destroy();
mOnscreenContentProvider = null;
}
if (mAutofillProvider != null) {
mAutofillProvider.destroy();
mAutofillProvider = null;
}
if (mAwDarkMode != null) {
mAwDarkMode.destroy();
mAwDarkMode = null;
}
// Remove pending messages
mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
if (mIsAttachedToWindow) {
Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
// Need to call detach to avoid leaks because the real detach later will be ignored.
onDetachedFromWindow();
}
mIsDestroyed = true;
PostTask.postTask(TaskTraits.UI_DEFAULT, () -> destroyNatives());
}
/** Deletes the native counterpart of this object. */
@VisibleForTesting
public void destroyNatives() {
if (mCleanupReference != null) {
assert mNativeAwContents != 0;
mWebContentsObserver.destroy();
mWebContentsObserver = null;
mAwWebContentsMetricsRecorder.destroy();
mAwWebContentsMetricsRecorder = null;
mNativeAwContents = 0;
mWebContents = null;
mWebContentsInternals = null;
mNavigationController = null;
mCleanupReference.cleanupNow();
mCleanupReference = null;
}
assert mWebContents == null;
assert mNavigationController == null;
assert mNativeAwContents == 0;
onDestroyed();
}
@VisibleForTesting
protected void onDestroyed() {}
/**
* Returns whether this instance of WebView is flagged as destroyed.
* If {@link WARN} is passed as a parameter, the method also issues a warning
* log message and dumps stack, as embedders are advised not to call any
* methods on destroyed WebViews.
*
* @param warnIfDestroyed use {@link WARN} if the check is done from a method
* that is called via public WebView API, and {@link NO_WARN} otherwise.
* @return whether this instance of WebView is flagged as destroyed.
*/
private boolean isDestroyed(int warnIfDestroyed) {
if (mIsDestroyed && warnIfDestroyed == WARN) {
Log.w(TAG, "Application attempted to call on a destroyed WebView", new Throwable());
}
boolean destroyRunnableHasRun =
mCleanupReference != null && mCleanupReference.hasCleanedUp();
boolean weakRefsCleared =
mWebContentsInternalsHolder != null && mWebContentsInternalsHolder.weakRefCleared();
if (TRACE && destroyRunnableHasRun && !mIsDestroyed) {
// Swallow the error. App developers are not going to do anything with an error msg.
Log.d(TAG, "AwContents is kept alive past CleanupReference by finalizer");
}
return mIsDestroyed || destroyRunnableHasRun || weakRefsCleared;
}
@VisibleForTesting
public WebContents getWebContents() {
return mWebContents;
}
@VisibleForTesting
public NavigationController getNavigationController() {
return mNavigationController;
}
public AutofillProvider getAutofillProviderForTesting() {
return mAutofillProvider;
}
// Can be called from any thread.
public AwSettings getSettings() {
if (TRACE) Log.i(TAG, "%s getSettings", this);
return mSettings;
}
ViewGroup getContainerView() {
return mContainerView;
}
public AwPdfExporter getPdfExporter() {
if (TRACE) Log.i(TAG, "%s getPdfExporter", this);
if (isDestroyed(WARN)) return null;
if (mAwPdfExporter == null) {
mAwPdfExporter = new AwPdfExporter(mContainerView);
AwContentsJni.get().createPdfExporter(mNativeAwContents, mAwPdfExporter);
}
return mAwPdfExporter;
}
public static void setAwDrawSWFunctionTable(long functionTablePointer) {
AwContentsJni.get().setAwDrawSWFunctionTable(functionTablePointer);
}
public static void setAwDrawGLFunctionTable(long functionTablePointer) {
AwContentsJni.get().setAwDrawGLFunctionTable(functionTablePointer);
}
public static long getAwDrawGLFunction() {
return AwGLFunctor.getAwDrawGLFunction();
}
public static void setShouldDownloadFavicons() {
AwContentsJni.get().setShouldDownloadFavicons();
}
/**
* Disables contents of JS-to-Java bridge objects to be inspectable using
* Object.keys() method and "for .. in" loops. This is intended for applications
* targeting earlier Android releases where this was not possible, and we want
* to ensure backwards compatible behavior.
*/
public void disableJavascriptInterfacesInspection() {
if (TRACE) Log.i(TAG, "%s disableJavascriptInterfacesInspection", this);
if (!isDestroyed(WARN)) {
getJavascriptInjector().setAllowInspection(false);
}
}
/**
* Intended for test code.
* @return the number of native instances of this class.
*/
@VisibleForTesting
public static int getNativeInstanceCount() {
return AwContentsJni.get().getNativeInstanceCount();
}
// This is only to avoid heap allocations inside getGlobalVisibleRect. It should treated
// as a local variable in the function and not used anywhere else.
private static final Rect sLocalGlobalVisibleRect = new Rect();
private Rect getGlobalVisibleRect() {
if (!mContainerView.getGlobalVisibleRect(sLocalGlobalVisibleRect)) {
sLocalGlobalVisibleRect.setEmpty();
}
return sLocalGlobalVisibleRect;
}
public void setOnscreenContentProvider(OnscreenContentProvider onscreenContentProvider) {
if (mOnscreenContentProvider != null) {
mOnscreenContentProvider.destroy();
}
mOnscreenContentProvider = onscreenContentProvider;
}
/** Release any DragAndDropPermissions currently held. */
protected void releaseDragAndDropPermissions() {
if (mDragAndDropPermissions != null) {
mDragAndDropPermissions.release();
mDragAndDropPermissions = null;
}
}
// --------------------------------------------------------------------------------------------
// WebView[Provider] method implementations
// --------------------------------------------------------------------------------------------
public void onDraw(Canvas canvas) {
try {
TraceEvent.begin("AwContents.onDraw");
mAwViewMethods.onDraw(canvas);
} finally {
TraceEvent.end("AwContents.onDraw");
}
}
public void setLayoutParams(final ViewGroup.LayoutParams layoutParams) {
mLayoutSizer.onLayoutParamsChange();
}
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mAwViewMethods.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public int getContentHeightCss() {
if (TRACE) Log.i(TAG, "%s getContentHeightCss", this);
if (isDestroyed(WARN)) return 0;
return (int) Math.ceil(mContentHeightDip);
}
public int getContentWidthCss() {
if (TRACE) Log.i(TAG, "%s getContentWidthCss", this);
if (isDestroyed(WARN)) return 0;
return (int) Math.ceil(mContentWidthDip);
}
public Picture capturePicture() {
if (TRACE) Log.i(TAG, "%s capturePicture", this);
if (isDestroyed(WARN)) return null;
return new AwPicture(
AwContentsJni.get()
.capturePicture(
mNativeAwContents,
mScrollOffsetManager.computeHorizontalScrollRange(),
mScrollOffsetManager.computeVerticalScrollRange()));
}
public void clearView() {
if (TRACE) Log.i(TAG, "%s clearView", this);
if (!isDestroyed(WARN)) AwContentsJni.get().clearView(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) {
if (TRACE) Log.i(TAG, "%s enableOnNewPicture=%s", this, enabled);
if (isDestroyed(WARN)) return;
if (invalidationOnly) {
mPictureListenerContentProvider = null;
} else if (enabled && mPictureListenerContentProvider == null) {
mPictureListenerContentProvider = () -> capturePicture();
}
AwContentsJni.get().enableOnNewPicture(mNativeAwContents, enabled);
}
public void findAllAsync(String searchString) {
if (TRACE) Log.i(TAG, "%s findAllAsync", this);
if (isDestroyed(WARN)) return;
if (searchString == null) {
throw new IllegalArgumentException("Search string shouldn't be null");
}
AwContentsJni.get().findAllAsync(mNativeAwContents, searchString);
}
public void findNext(boolean forward) {
if (TRACE) Log.i(TAG, "%s findNext", this);
if (!isDestroyed(WARN)) {
AwContentsJni.get().findNext(mNativeAwContents, forward);
}
}
public void clearMatches() {
if (TRACE) Log.i(TAG, "%s clearMatches", this);
if (!isDestroyed(WARN)) {
AwContentsJni.get().clearMatches(mNativeAwContents);
}
}
/** @return load progress of the WebContents, on a scale of 0-100. */
public int getMostRecentProgress() {
if (isDestroyed(WARN)) return 0;
if (!mWebContents.isLoading()) return 100;
return Math.round(100 * mWebContents.getLoadProgress());
}
public Bitmap getFavicon() {
if (TRACE) Log.i(TAG, "%s getFavicon", this);
if (isDestroyed(WARN)) return null;
return mFavicon;
}
private void requestVisitedHistoryFromClient() {
Callback<String[]> callback =
value -> {
if (value != null) {
// Replace null values with empty strings, because they can't be represented
// as native strings.
for (int i = 0; i < value.length; i++) {
if (value[i] == null) value[i] = "";
}
}
PostTask.runOrPostTask(
TaskTraits.UI_DEFAULT,
() -> {
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get().addVisitedLinks(mNativeAwContents, value);
}
});
};
mContentsClient.getVisitedHistory(callback);
}
/* package */ static final Pattern BAD_HEADER_CHAR = Pattern.compile("[\u0000\r\n]");
/* package */ static final String BAD_HEADER_MSG =
"HTTP headers must not contain null, CR, or NL characters. ";
/** WebView.loadUrl. */
public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
if (TRACE) Log.i(TAG, "%s loadUrl(extra headers)=%s", this, url);
if (isDestroyed(WARN)) return;
// Early out to match old WebView implementation
if (url == null) {
return;
}
// TODO: We may actually want to do some preliminary checks here (like filter
// about://chrome).
// For backwards compatibility, apps targeting less than K will have JS URLs evaluated
// directly and any result of the evaluation will not replace the current page content.
// Matching Chrome behavior more closely; apps targetting >= K that load a JS URL will
// have the result of that URL replace the content of the current page.
final String javaScriptScheme = "javascript:";
if (mAppTargetSdkVersion < Build.VERSION_CODES.KITKAT && url.startsWith(javaScriptScheme)) {
evaluateJavaScript(url.substring(javaScriptScheme.length()), null);
return;
}
LoadUrlParams params = new LoadUrlParams(url, PageTransition.TYPED);
if (additionalHttpHeaders != null) {
for (Map.Entry<String, String> header : additionalHttpHeaders.entrySet()) {
String headerName = header.getKey();
String headerValue = header.getValue();
if (headerName != null && BAD_HEADER_CHAR.matcher(headerName).find()) {
throw new IllegalArgumentException(
BAD_HEADER_MSG + "Invalid header name '" + headerName + "'.");
}
if (headerValue != null && BAD_HEADER_CHAR.matcher(headerValue).find()) {
throw new IllegalArgumentException(
BAD_HEADER_MSG
+ "Header '"
+ headerName
+ "' has invalid value '"
+ headerValue
+ "'");
}
}
params.setExtraHeaders(new HashMap<String, String>(additionalHttpHeaders));
}
loadUrl(params);
}
/** WebView.loadUrl. */
public void loadUrl(String url) {
if (TRACE) Log.i(TAG, "%s loadUrl=%s", this, url);
if (isDestroyed(WARN)) return;
// Early out to match old WebView implementation
if (url == null) {
return;
}
loadUrl(url, null);
}
/** WebView.postUrl. */
public void postUrl(String url, byte[] postData) {
if (TRACE) Log.i(TAG, "%s postUrl=%s", this, url);
if (isDestroyed(WARN)) return;
LoadUrlParams params = LoadUrlParams.createLoadHttpPostParams(url, postData);
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content-Type", "application/x-www-form-urlencoded");
params.setExtraHeaders(headers);
loadUrl(params);
}
private static String fixupMimeType(String mimeType) {
return TextUtils.isEmpty(mimeType) ? "text/html" : mimeType;
}
private static String fixupData(String data) {
return TextUtils.isEmpty(data) ? "" : data;
}
private static String fixupBase(String url) {
return TextUtils.isEmpty(url) ? ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL : url;
}
private static String fixupHistory(String url) {
return TextUtils.isEmpty(url) ? ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL : url;
}
private static boolean isBase64Encoded(String encoding) {
return "base64".equals(encoding);
}
private static void recordLoadUrlScheme(@UrlScheme int value) {
RecordHistogram.recordEnumeratedHistogram(
LOAD_URL_SCHEME_HISTOGRAM_NAME, value, UrlScheme.COUNT);
}
/** WebView.loadData. */
public void loadData(String data, String mimeType, String encoding) {
if (TRACE) Log.i(TAG, "%s loadData", this);
if (isDestroyed(WARN)) return;
if (data != null && data.contains("#")) {
if (ContextUtils.getApplicationContext().getApplicationInfo().targetSdkVersion
< Build.VERSION_CODES.Q
&& !isBase64Encoded(encoding)) {
// As of Chromium M72, data URI parsing strictly enforces encoding of '#'. To
// support WebView applications which were not expecting this change, we do it for
// them.
data = fixupOctothorpesInLoadDataContent(data);
}
}
loadUrl(
LoadUrlParams.createLoadDataParams(
fixupData(data), fixupMimeType(mimeType), isBase64Encoded(encoding)));
}
/**
* Helper method to fixup content passed to {@link #loadData} which may not have had '#'
* characters encoded correctly. Historically Chromium did not strictly enforce the encoding of
* '#' characters in Data URLs; they would be treated both as renderable content and as
* potential URL fragments for DOM id matching. This behavior changed in Chromium M72 where
* stricter parsing was enforced; the first '#' character now marks the end of the renderable
* section and the start of the DOM fragment.
*
* @param data The content passed to {@link #loadData}, which may contain unencoded '#'s.
* @return A version of the input with '#' characters correctly encoded, preserving any DOM id
* selector which may have been present in the original.
*/
@VisibleForTesting
public static String fixupOctothorpesInLoadDataContent(String data) {
// If the data may have had a valid DOM selector, we duplicate the selector and append it as
// a proper URL fragment. For example, "<a id='target'>Target</a>#target" will be converted
// to "<a id='target'>Target</a>%23target#target". This preserves both the rendering (which
// should render 'Target#target' on the page) and the DOM selector behavior (which should
// scroll to the anchor).
Matcher matcher = sDataURLWithSelectorPattern.matcher(data);
String suffix = matcher.matches() ? matcher.group(1) : "";
return data.replace("#", "%23") + suffix;
}
private @UrlScheme int schemeForUrl(String url) {
if (url == null || url.equals(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL)) {
return (UrlScheme.EMPTY);
} else if (url.startsWith("http:")) {
return (UrlScheme.HTTP_SCHEME);
} else if (url.startsWith("https:")) {
return (UrlScheme.HTTPS_SCHEME);
} else if (sFileAndroidAssetPattern.matcher(url).matches()) {
return (UrlScheme.FILE_ANDROID_ASSET_SCHEME);
} else if (url.startsWith("file:")) {
return (UrlScheme.FILE_SCHEME);
} else if (url.startsWith("ftp:")) {
return (UrlScheme.FTP_SCHEME);
} else if (url.startsWith("data:")) {
return (UrlScheme.DATA_SCHEME);
} else if (url.startsWith("javascript:")) {
return (UrlScheme.JAVASCRIPT_SCHEME);
} else if (url.startsWith("about:")) {
return (UrlScheme.ABOUT_SCHEME);
} else if (url.startsWith("chrome:")) {
return (UrlScheme.CHROME_SCHEME);
} else if (url.startsWith("blob:")) {
return (UrlScheme.BLOB_SCHEME);
} else if (url.startsWith("content:")) {
return (UrlScheme.CONTENT_SCHEME);
} else if (url.startsWith("intent:")) {
return (UrlScheme.INTENT_SCHEME);
}
return (UrlScheme.UNKNOWN_SCHEME);
}
/** WebView.loadDataWithBaseURL. */
public void loadDataWithBaseURL(
String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
if (TRACE) Log.i(TAG, "%s loadDataWithBaseURL=%s", this, baseUrl);
if (isDestroyed(WARN)) return;
data = fixupData(data);
mimeType = fixupMimeType(mimeType);
LoadUrlParams loadUrlParams;
baseUrl = fixupBase(baseUrl);
historyUrl = fixupHistory(historyUrl);
if (baseUrl.startsWith("data:")) {
// For backwards compatibility with WebViewClassic, we use the value of |encoding|
// as the charset, as long as it's not "base64".
boolean isBase64 = isBase64Encoded(encoding);
loadUrlParams =
LoadUrlParams.createLoadDataParamsWithBaseUrl(
data,
mimeType,
isBase64,
baseUrl,
historyUrl,
isBase64 ? null : encoding);
} else {
// When loading data with a non-data: base URL, the classic WebView would effectively
// "dump" that string of data into the WebView without going through regular URL
// loading steps such as decoding URL-encoded entities. We achieve this same behavior by
// base64 encoding the data that is passed here and then loading that as a data: URL.
try {
loadUrlParams =
LoadUrlParams.createLoadDataParamsWithBaseUrl(
Base64.encodeToString(data.getBytes("utf-8"), Base64.DEFAULT),
mimeType,
true,
baseUrl,
historyUrl,
"utf-8");
} catch (java.io.UnsupportedEncodingException e) {
Log.wtf(TAG, "Unable to load data string %s", data, e);
return;
}
}
// This is a workaround for an issue with PlzNavigate and one of Samsung's OEM mail apps.
// See http://crbug.com/781535.
if (isSamsungMailApp() && SAMSUNG_WORKAROUND_BASE_URL.equals(loadUrlParams.getBaseUrl())) {
PostTask.postDelayedTask(
TaskTraits.UI_DEFAULT, () -> loadUrl(loadUrlParams), SAMSUNG_WORKAROUND_DELAY);
return;
}
loadUrl(loadUrlParams);
}
/**
* 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 params Parameters for this load.
*/
@VisibleForTesting
public void loadUrl(LoadUrlParams params) {
if (params.getBaseUrl() == null) {
// Don't record the URL if this was loaded via loadDataWithBaseURL(). That API is
// tracked separately under Android.WebView.LoadDataWithBaseUrl.BaseUrl.
recordLoadUrlScheme(schemeForUrl(params.getUrl()));
}
if (params.getLoadUrlType() == LoadURLType.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);
AwContentsJni.get().grantFileSchemeAccesstoChildProcess(mNativeAwContents);
}
// If we are reloading the same url, then set transition type as reload.
if (params.getUrl() != null
&& params.getUrl().equals(mWebContents.getLastCommittedUrl().getSpec())
&& params.getTransitionType() == PageTransition.TYPED) {
params.setTransitionType(PageTransition.RELOAD);
}
params.setTransitionType(params.getTransitionType() | PageTransition.FROM_API);
// For WebView, always use the user agent override, which is set
// every time the user agent in AwSettings is modified.
params.setOverrideUserAgent(UserAgentOverrideOption.TRUE);
// We don't pass extra headers to the content layer, as WebViewClassic
// was adding them in a very narrow set of conditions. See http://crbug.com/306873
// However, if the embedder is attempting to inject a Referer header for their
// loadUrl call, then we set that separately and remove it from the extra headers map/
final String referer = "referer";
Map<String, String> extraHeaders = params.getExtraHeaders();
if (extraHeaders != null) {
for (String header : extraHeaders.keySet()) {
if (referer.equals(header.toLowerCase(Locale.US))) {
params.setReferrer(
new Referrer(extraHeaders.remove(header), ReferrerPolicy.DEFAULT));
params.setExtraHeaders(extraHeaders);
break;
}
}
}
AwContentsJni.get()
.setExtraHeadersForUrl(
mNativeAwContents,
params.getUrl(),
params.getExtraHttpRequestHeadersString());
params.setExtraHeaders(new HashMap<String, String>());
// Ideally, the URL would only be "fixed" for user input (e.g. for URLs
// entered into the Omnibox), but some WebView API consumers rely on
// the legacy behavior where all navigations were subject to the
// "fixing". See also https://crbug.com/1145717.
params.setUrl(UrlFormatter.fixupUrl(params.getUrl()).getPossiblyInvalidSpec());
mNavigationController.loadUrl(params);
// 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();
}
}
/**
* Get the URL of the current page. This is the visible URL of the {@link WebContents} which may
* be a pending navigation or the last committed URL. For the last committed URL use
* #getLastCommittedUrl().
*
* @return The URL of the current page or null if it's empty.
*/
public GURL getUrl() {
if (TRACE) Log.i(TAG, "%s getUrl", this);
if (isDestroyed(WARN)) return null;
GURL url = mWebContents.getVisibleUrl();
if (url == null || url.getSpec().trim().isEmpty()) return null;
return url;
}
/**
* Gets the last committed URL. It represents the current page that is
* displayed in WebContents. It represents the current security context.
*
* @return The URL of the current page or null if it's empty.
*/
public String getLastCommittedUrl() {
if (TRACE) Log.i(TAG, "%s getLastCommittedUrl", this);
if (isDestroyed(NO_WARN)) return null;
GURL url = mWebContents.getLastCommittedUrl();
if (url == null || url.isEmpty()) return null;
return url.getSpec();
}
public void requestFocus() {
mAwViewMethods.requestFocus();
}
public void setBackgroundColor(int color) {
if (TRACE) Log.i(TAG, "%s setBackgroundColor=%x", this, color);
mBaseBackgroundColor = color;
mDidInitBackground = true;
if (!isDestroyed(WARN)) {
AwContentsJni.get().setBackgroundColor(mNativeAwContents, color);
}
}
/** @see android.view.View#setLayerType() */
public void setLayerType(int layerType, Paint paint) {
if (TRACE) Log.i(TAG, "%s setLayerType", this);
mAwViewMethods.setLayerType(layerType, paint);
}
public int getEffectiveBackgroundColorForTesting() {
return getEffectiveBackgroundColor();
}
int getEffectiveBackgroundColor() {
// Do not ask the WebContents for the background color, as it will always
// report white prior to initial navigation or post destruction, whereas we want
// to use the client supplied base value in those cases.
if (isDestroyed(NO_WARN)) {
return mBaseBackgroundColor;
} else if (!mContentsClient.isCachedRendererBackgroundColorValid()) {
// In force dark mode or the dark style preferred , if background color not set,
// this cause a white flash, just show black background.
if ((mSettings.isForceDarkApplied() || mSettings.prefersDarkFromTheme())
&& !mDidInitBackground) {
return Color.BLACK;
}
return mBaseBackgroundColor;
}
return mContentsClient.getCachedRendererBackgroundColor();
}
public boolean isMultiTouchZoomSupported() {
return mSettings.supportsMultiTouchZoom();
}
public View getZoomControlsViewForTest() {
return mZoomControls.getZoomControlsViewForTest();
}
public AwZoomControls getZoomControlsForTest() {
return mZoomControls;
}
/** @see View#setOverScrollMode(int) */
public void setOverScrollMode(int mode) {
if (TRACE) Log.i(TAG, "%s setOverScrollMode", this);
if (mode != View.OVER_SCROLL_NEVER) {
mOverScrollGlow = new OverScrollGlow(mContext, mContainerView);
} else {
mOverScrollGlow = null;
}
}
// TODO(mkosiba): In WebViewClassic these appear in some of the scroll extent calculation
// methods but toggling them has no visiual effect on the content (in other words the scrolling
// code behaves as if the scrollbar-related padding is in place but the onDraw code doesn't
// take that into consideration).
// http://crbug.com/269032
private boolean mOverlayHorizontalScrollbar = true;
private boolean mOverlayVerticalScrollbar;
/** @see View#setScrollBarStyle(int) */
public void setScrollBarStyle(int style) {
if (TRACE) Log.i(TAG, "%s setScrollBarStyle", this);
boolean scrollbars =
style == View.SCROLLBARS_INSIDE_OVERLAY || style == View.SCROLLBARS_OUTSIDE_OVERLAY;
mOverlayHorizontalScrollbar = scrollbars;
mOverlayVerticalScrollbar = scrollbars;
}
/** @see View#setHorizontalScrollbarOverlay(boolean) */
public void setHorizontalScrollbarOverlay(boolean overlay) {
if (TRACE) Log.i(TAG, "%s setHorizontalScrollbarOverlay=%s", this, overlay);
mOverlayHorizontalScrollbar = overlay;
}
/** @see View#setVerticalScrollbarOverlay(boolean) */
public void setVerticalScrollbarOverlay(boolean overlay) {
if (TRACE) Log.i(TAG, "%s setVerticalScrollbarOverlay=%s", this, overlay);
mOverlayVerticalScrollbar = overlay;
}
/** @see View#overlayHorizontalScrollbar() */
public boolean overlayHorizontalScrollbar() {
if (TRACE) Log.i(TAG, "%s overlayHorizontalScrollbar", this);
return mOverlayHorizontalScrollbar;
}
/** @see View#overlayVerticalScrollbar() */
public boolean overlayVerticalScrollbar() {
if (TRACE) Log.i(TAG, "%s overlayVerticalScrollbar", this);
return mOverlayVerticalScrollbar;
}
/**
* Called by the embedder when the scroll offset of the containing view has changed.
* @see View#onScrollChanged(int,int)
*/
public void onContainerViewScrollChanged(int l, int t, int oldl, int oldt) {
mAwViewMethods.onContainerViewScrollChanged(l, t, oldl, oldt);
}
/**
* Called by the embedder when the containing view is to be scrolled or overscrolled.
* @see View#onOverScrolled(int,int,int,int)
*/
public void onContainerViewOverScrolled(
int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
mAwViewMethods.onContainerViewOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
/** @see android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean) */
public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
if (isDestroyed(WARN)) return false;
return mScrollOffsetManager.requestChildRectangleOnScreen(
child.getLeft() - child.getScrollX(),
child.getTop() - child.getScrollY(),
rect,
immediate);
}
/** @see View#computeHorizontalScrollRange() */
public int computeHorizontalScrollRange() {
return mAwViewMethods.computeHorizontalScrollRange();
}
/** @see View#computeHorizontalScrollOffset() */
public int computeHorizontalScrollOffset() {
return mAwViewMethods.computeHorizontalScrollOffset();
}
/** @see View#computeVerticalScrollRange() */
public int computeVerticalScrollRange() {
return mAwViewMethods.computeVerticalScrollRange();
}
/** @see View#computeVerticalScrollOffset() */
public int computeVerticalScrollOffset() {
return mAwViewMethods.computeVerticalScrollOffset();
}
/** @see View#computeVerticalScrollExtent() */
public int computeVerticalScrollExtent() {
return mAwViewMethods.computeVerticalScrollExtent();
}
/** @see View.computeScroll() */
public void computeScroll() {
mAwViewMethods.computeScroll();
}
/** @see View#onCheckIsTextEditor() */
public boolean onCheckIsTextEditor() {
return mAwViewMethods.onCheckIsTextEditor();
}
/** @see android.webkit.WebView#stopLoading() */
public void stopLoading() {
if (TRACE) Log.i(TAG, "%s stopLoading", this);
if (!isDestroyed(WARN)) mWebContents.stop();
}
/** @see android.webkit.WebView#reload() */
public void reload() {
if (TRACE) Log.i(TAG, "%s reload", this);
if (!isDestroyed(WARN)) mNavigationController.reload(true);
}
/** @see android.webkit.WebView#canGoBack() */
public boolean canGoBack() {
return isDestroyed(WARN) ? false : mNavigationController.canGoBack();
}
/** @see android.webkit.WebView#goBack() */
public void goBack() {
if (TRACE) Log.i(TAG, "%s goBack", this);
if (!isDestroyed(WARN) && mNavigationController.canGoBack()) {
mNavigationController.goBack();
}
}
/** @see android.webkit.WebView#canGoForward() */
public boolean canGoForward() {
return isDestroyed(WARN) ? false : mNavigationController.canGoForward();
}
/** @see android.webkit.WebView#goForward() */
public void goForward() {
if (TRACE) Log.i(TAG, "%s goForward", this);
if (!isDestroyed(WARN) && mNavigationController.canGoForward()) {
mNavigationController.goForward();
}
}
/** @see android.webkit.WebView#canGoBackOrForward(int) */
public boolean canGoBackOrForward(int steps) {
return isDestroyed(WARN) ? false : mNavigationController.canGoToOffset(steps);
}
/** @see android.webkit.WebView#goBackOrForward(int) */
public void goBackOrForward(int steps) {
if (TRACE) Log.i(TAG, "%s goBackOrForward=%d", this, steps);
if (!canGoBackOrForward(steps)) {
return;
}
if (!isDestroyed(WARN)) mNavigationController.goToOffset(steps);
}
/** @see android.webkit.WebView#pauseTimers() */
public void pauseTimers() {
if (TRACE) Log.i(TAG, "%s pauseTimers", this);
if (!isDestroyed(WARN)) {
ContentViewStatics.setWebKitSharedTimersSuspended(true);
}
}
/** @see android.webkit.WebView#resumeTimers() */
public void resumeTimers() {
if (TRACE) Log.i(TAG, "%s resumeTimers", this);
if (!isDestroyed(WARN)) {
ContentViewStatics.setWebKitSharedTimersSuspended(false);
}
}
/** @see android.webkit.WebView#onPause() */
public void onPause() {
if (TRACE) Log.i(TAG, "%s onPause", this);
if (mIsPaused || isDestroyed(NO_WARN)) return;
mIsPaused = true;
AwContentsJni.get().setIsPaused(mNativeAwContents, mIsPaused);
// Geolocation is paused/resumed via the page visibility mechanism.
updateWebContentsVisibility();
}
/** @see android.webkit.WebView#onResume() */
public void onResume() {
if (TRACE) Log.i(TAG, "%s onResume", this);
if (!mIsPaused || isDestroyed(NO_WARN)) return;
mIsPaused = false;
AwContentsJni.get().setIsPaused(mNativeAwContents, mIsPaused);
updateWebContentsVisibility();
}
/** @see android.webkit.WebView#isPaused() */
public boolean isPaused() {
if (TRACE) Log.i(TAG, "%s isPaused", this);
return isDestroyed(WARN) ? false : mIsPaused;
}
/** @see android.webkit.WebView#onCreateInputConnection(EditorInfo) */
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return mAwViewMethods.onCreateInputConnection(outAttrs);
}
/** @see android.webkit.WebView#onDragEvent(DragEvent) */
public boolean onDragEvent(DragEvent event) {
return mAwViewMethods.onDragEvent(event);
}
/** @see android.webkit.WebView#onKeyUp(int, KeyEvent) */
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mAwViewMethods.onKeyUp(keyCode, event);
}
/** @see android.webkit.WebView#dispatchKeyEvent(KeyEvent) */
public boolean dispatchKeyEvent(KeyEvent event) {
return mAwViewMethods.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 (TRACE) Log.i(TAG, "%s clearCache", this);
if (!isDestroyed(WARN)) {
AwContentsJni.get().clearCache(mNativeAwContents, includeDiskFiles);
}
}
public void documentHasImages(Message message) {
if (TRACE) Log.i(TAG, "%s documentHasImages", this);
if (!isDestroyed(WARN)) {
AwContentsJni.get().documentHasImages(mNativeAwContents, message);
}
}
public void saveWebArchive(
final String basename, boolean autoname, final Callback<String> callback) {
if (TRACE) Log.i(TAG, "%s saveWebArchive=%s", this, basename);
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<String>() {
@Override
protected String doInBackground() {
return generateArchiveAutoNamePath(getOriginalUrl(), basename);
}
@Override
protected void onPostExecute(String result) {
saveWebArchiveInternal(result, callback);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public String getOriginalUrl() {
if (TRACE) Log.i(TAG, "%s getOriginalUrl", this);
if (isDestroyed(WARN)) return null;
NavigationHistory history = getNavigationHistory();
int currentIndex = history.getCurrentEntryIndex();
// The current entry will always exist, but only return it if it is not
// the initial NavigationEntry, to preserve legacy behavior. This is
// because initial NavigationEntries used to not exist (see
// https://crbug.com/524208), and the API used to return null when
// no navigation had committed. Keeping the legacy behavior prevents
// unexpected breakages on things that depend on it. See also
// https://crbug.com/1277414.
if (!history.getEntryAtIndex(currentIndex).isInitialEntry()) {
return history.getEntryAtIndex(currentIndex).getOriginalUrl().getSpec();
}
// When there is no committed navigation, return null.
return null;
}
/** @see NavigationController#getNavigationHistory() */
public NavigationHistory getNavigationHistory() {
return isDestroyed(WARN) ? null : mNavigationController.getNavigationHistory();
}
/** @see android.webkit.WebView#getTitle() */
public String getTitle() {
if (TRACE) Log.i(TAG, "%s getTitle", this);
return isDestroyed(WARN) ? null : mWebContents.getTitle();
}
/** @see android.webkit.WebView#clearHistory() */
public void clearHistory() {
if (TRACE) Log.i(TAG, "%s clearHistory", this);
if (!isDestroyed(WARN)) mNavigationController.clearHistory();
}
/** @see android.webkit.WebView#getCertificate() */
public SslCertificate getCertificate() {
if (TRACE) Log.i(TAG, "%s getCertificate", this);
return isDestroyed(WARN)
? null
: SslUtil.getCertificateFromDerBytes(
AwContentsJni.get().getCertificate(mNativeAwContents));
}
/** @see android.webkit.WebView#clearSslPreferences() */
public void clearSslPreferences() {
if (TRACE) Log.i(TAG, "%s clearSslPreferences", this);
if (!isDestroyed(WARN)) mNavigationController.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 (TRACE) Log.i(TAG, "%s getLastHitTestResult", this);
if (isDestroyed(WARN)) return null;
AwContentsJni.get().updateLastHitTestData(mNativeAwContents);
return mPossiblyStaleHitTestData;
}
/** @see android.webkit.WebView#requestFocusNodeHref() */
public void requestFocusNodeHref(Message msg) {
if (TRACE) Log.i(TAG, "%s requestFocusNodeHref", this);
if (msg == null || isDestroyed(WARN)) return;
AwContentsJni.get().updateLastHitTestData(mNativeAwContents);
Bundle data = msg.getData();
// In order to maintain compatibility with the old WebView's implementation,
// the absolute (full) url is passed in the |url| field, not only the href attribute.
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 (TRACE) Log.i(TAG, "%s requestImageRef", this);
if (msg == null || isDestroyed(WARN)) return;
AwContentsJni.get().updateLastHitTestData(mNativeAwContents);
Bundle data = msg.getData();
data.putString("url", mPossiblyStaleHitTestData.imgSrc);
msg.setData(data);
msg.sendToTarget();
}
@VisibleForTesting
public float getPageScaleFactor() {
return mPageScaleFactor;
}
private float getDeviceScaleFactor() {
return mWindowAndroid.getWindowAndroid().getDisplay().getDipScale();
}
/**
* Add a JavaScript snippet that will run after the document has been created, but before any
* script in the document executes. Note that calling this method multiple times will add
* multiple scripts. Added scripts will take effect from the next navigation. If want to remove
* previously set script, use the returned ScriptHandler object to do so. Any JavaScript objects
* injected by addWebMessageListener() or addJavascriptInterface() will be available to use in
* this script. Scripts can be removed using the ScriptHandler object returned when they were
* added. The DOM tree may not be ready at the moment that the script runs.
*
* <p>If multiple scripts are added, they will be executed in the same order they were added.
*
* @param script The JavaScript snippet to be run.
* @param allowedOriginRules The JavaScript snippet will run on every frame whose origin matches
* any one of the allowedOriginRules.
* @throws IllegalArgumentException if one of the allowedOriginRules is invalid or one of
* jsObjectName and allowedOriginRules is {@code null}.
* @return A {@link ScriptHandler} for removing the script.
*/
public ScriptHandler addDocumentStartJavaScript(
@NonNull String script, @NonNull String[] allowedOriginRules) {
if (TRACE) Log.i(TAG, "%s addDocumentStartJavaScript", this);
if (isDestroyed(WARN)) return null;
if (script == null) {
throw new IllegalArgumentException("script shouldn't be null.");
}
for (int i = 0; i < allowedOriginRules.length; ++i) {
if (TextUtils.isEmpty(allowedOriginRules[i])) {
throw new IllegalArgumentException(
"allowedOriginRules[" + i + "] shouldn't be null or empty");
}
}
return new ScriptHandler(
AwContents.this,
AwContentsJni.get()
.addDocumentStartJavaScript(mNativeAwContents, script, allowedOriginRules));
}
/* package */ void removeDocumentStartJavaScript(int scriptId) {
if (isDestroyed(WARN)) return;
AwContentsJni.get().removeDocumentStartJavaScript(mNativeAwContents, scriptId);
}
/**
* Add the {@link WebMessageListener} to AwContents, it will also inject the JavaScript object
* with the given name to frames that have origins matching the allowedOriginRules. Note that
* this call will not inject the JS object immediately. The JS object will be injected only for
* future navigations (in DidClearWindowObject).
*
* @param jsObjectName The name for the injected JavaScript object for this {@link
* WebMessageListener}.
* @param allowedOrigins A list of matching rules for the allowed origins. The JavaScript object
* will be injected when the frame's origin matches any one of the allowed origins. If a
* wildcard "*" is provided, it will inject JavaScript object to all frames.
* @param listener The {@link WebMessageListener} to be called when received onPostMessage().
* @throws IllegalArgumentException if one of the allowedOriginRules is invalid or one of
* jsObjectName and allowedOriginRules is {@code null}.
* @throws NullPointerException if listener is {@code null}.
*/
public void addWebMessageListener(
@NonNull String jsObjectName,
@NonNull String[] allowedOriginRules,
@NonNull WebMessageListener listener) {
if (TRACE) Log.i(TAG, "%s addWebMessageListener=%s", this, jsObjectName);
if (isDestroyed(WARN)) return;
if (listener == null) {
throw new NullPointerException("listener shouldn't be null");
}
if (TextUtils.isEmpty(jsObjectName)) {
throw new IllegalArgumentException("jsObjectName shouldn't be null or empty string");
}
for (int i = 0; i < allowedOriginRules.length; ++i) {
if (TextUtils.isEmpty(allowedOriginRules[i])) {
throw new IllegalArgumentException(
"allowedOriginRules[" + i + "] is null or empty");
}
}
final String exceptionMessage =
AwContentsJni.get()
.addWebMessageListener(
mNativeAwContents,
new WebMessageListenerHolder(listener),
jsObjectName,
allowedOriginRules);
if (!TextUtils.isEmpty(exceptionMessage)) {
throw new IllegalArgumentException(exceptionMessage);
}
}
/**
* Removes the {@link WebMessageListener} added by {@link addWebMessageListener}. This call will
* immediately remove the JavaScript object/WebMessageListener mapping pair. So any messages
* from the JavaScript object will be dropped. However the JavaScript object will only be
* removed for future navigations.
*
* @param listener The {@link WebMessageListener} to be removed. Can not be {@code null}.
*/
public void removeWebMessageListener(@NonNull String jsObjectName) {
if (TRACE) Log.i(TAG, "%s removeWebMessageListener=%s", this, jsObjectName);
if (isDestroyed(WARN)) return;
AwContentsJni.get().removeWebMessageListener(mNativeAwContents, jsObjectName);
}
/**
* @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() {
if (TRACE) Log.i(TAG, "%s getScale", this);
if (isDestroyed(WARN)) return 1;
return mPageScaleFactor * getDeviceScaleFactor();
}
/** @see android.webkit.WebView#flingScroll(int, int) */
public void flingScroll(int velocityX, int velocityY) {
if (TRACE) Log.i(TAG, "%s flingScroll", this);
if (isDestroyed(WARN)) return;
mWebContents
.getEventForwarder()
.startFling(SystemClock.uptimeMillis(), -velocityX, -velocityY, false, true);
}
/** @see android.webkit.WebView#pageUp(boolean) */
public boolean pageUp(boolean top) {
if (TRACE) Log.i(TAG, "%s pageUp", this);
if (isDestroyed(WARN)) return false;
return mScrollOffsetManager.pageUp(top);
}
/** @see android.webkit.WebView#pageDown(boolean) */
public boolean pageDown(boolean bottom) {
if (TRACE) Log.i(TAG, "%s pageDown", this);
if (isDestroyed(WARN)) return false;
return mScrollOffsetManager.pageDown(bottom);
}
/** @see android.webkit.WebView#canZoomIn() */
// This method uses the term 'zoom' for legacy reasons, but relates
// to what chrome calls the 'page scale factor'.
public boolean canZoomIn() {
if (TRACE) Log.i(TAG, "%s canZoomIn", this);
if (isDestroyed(WARN)) return false;
final float zoomInExtent = mMaxPageScaleFactor - mPageScaleFactor;
return zoomInExtent > ZOOM_CONTROLS_EPSILON;
}
/** @see android.webkit.WebView#canZoomOut() */
// This method uses the term 'zoom' for legacy reasons, but relates
// to what chrome calls the 'page scale factor'.
public boolean canZoomOut() {
if (TRACE) Log.i(TAG, "%s canZoomOut", this);
if (isDestroyed(WARN)) return false;
final float zoomOutExtent = mPageScaleFactor - mMinPageScaleFactor;
return zoomOutExtent > ZOOM_CONTROLS_EPSILON;
}
/** @see android.webkit.WebView#zoomIn() */
// This method uses the term 'zoom' for legacy reasons, but relates
// to what chrome calls the 'page scale factor'.
public boolean zoomIn() {
if (!canZoomIn()) {
return false;
}
zoomBy(ZoomConstants.ZOOM_IN_DELTA);
return true;
}
/** @see android.webkit.WebView#zoomOut() */
// This method uses the term 'zoom' for legacy reasons, but relates
// to what chrome calls the 'page scale factor'.
public boolean zoomOut() {
if (!canZoomOut()) {
return false;
}
zoomBy(ZoomConstants.ZOOM_OUT_DELTA);
return true;
}
/** Resets the zoom to default */
// This method uses the term 'zoom' for legacy reasons, but relates
// to what chrome calls the 'page scale factor'.
public boolean zoomReset() {
zoomByInternal(ZoomConstants.ZOOM_RESET_DELTA);
return true;
}
/** @see android.webkit.WebView#zoomBy() */
// This method uses the term 'zoom' for legacy reasons, but relates
// to what chrome calls the 'page scale factor'.
public void zoomBy(float delta) {
if (delta < 0.01f || delta > 100.0f) {
throw new IllegalStateException("zoom delta value outside [0.01, 100] range.");
}
zoomByInternal(delta);
}
private void zoomByInternal(float delta) {
if (TRACE) Log.i(TAG, "%s zoomBy=%f", this, delta);
if (isDestroyed(WARN)) return;
AwContentsJni.get().zoomBy(mNativeAwContents, delta);
}
/** @see android.webkit.WebView#invokeZoomPicker() */
public void invokeZoomPicker() {
if (TRACE) Log.i(TAG, "%s invokeZoomPicker", this);
if (!isDestroyed(WARN)) mZoomControls.invokeZoomPicker();
}
/** @see android.webkit.WebView#preauthorizePermission(Uri, long) */
public void preauthorizePermission(Uri origin, long resources) {
if (TRACE) Log.i(TAG, "%s preauthorizePermission=%s", this, origin);
if (isDestroyed(NO_WARN)) return;
AwContentsJni.get().preauthorizePermission(mNativeAwContents, origin.toString(), resources);
}
/** @see WebContents.evaluateJavaScript(String, JavaScriptCallback) */
public void evaluateJavaScript(String script, final Callback<String> callback) {
if (TRACE) Log.i(TAG, "%s evaluateJavascript=%s", this, script);
if (isDestroyed(WARN)) return;
JavaScriptCallback jsCallback = null;
if (callback != null) {
jsCallback =
jsonResult -> {
// Post the application callback back to the current thread to ensure the
// application callback is executed without any native code on the stack.
// This so that any exception thrown by the application callback won't
// have to be propagated through a native call stack.
AwThreadUtils.postToCurrentLooper(callback.bind(jsonResult));
};
}
mHasEvaluatedJavascript = true;
mWebContents.evaluateJavaScript(script, jsCallback);
}
public void evaluateJavaScriptForTests(String script, final Callback<String> callback) {
if (TRACE) Log.i(TAG, "%s evaluateJavascriptForTests=%s", this, script);
if (isDestroyed(NO_WARN)) return;
JavaScriptCallback jsCallback = null;
if (callback != null) {
jsCallback = jsonResult -> callback.onResult(jsonResult);
}
mWebContents.evaluateJavaScriptForTests(script, jsCallback);
}
/**
* Send a MessageEvent to main frame.
*
* @param message The String message for the JavaScript MessageEvent.
* @param targetOrigin The expected target frame's origin.
* @param sentPorts ports for the JavaScript MessageEvent.
*/
public void postMessageToMainFrame(
MessagePayload messagePayload, String targetOrigin, MessagePort[] sentPorts) {
if (TRACE) Log.i(TAG, "%s postMessageToMainFrame", this);
if (isDestroyed(WARN)) return;
RenderFrameHost mainFrame = mWebContents.getMainFrame();
// If the RenderFrameHost or the RenderFrame doesn't exist we couldn't post the message.
if (mainFrame == null || !mainFrame.isRenderFrameLive()) return;
mWebContents.postMessageToMainFrame(messagePayload, null, targetOrigin, sentPorts);
}
/** Creates a message channel and returns the ports for each end of the channel. */
public MessagePort[] createMessageChannel() {
if (TRACE) Log.i(TAG, "%s createMessageChannel", this);
if (isDestroyed(WARN)) return null;
return MessagePort.createPair();
}
public boolean hasAccessedInitialDocument() {
if (TRACE) Log.i(TAG, "%s hasAccessedInitialDocument", this);
if (isDestroyed(NO_WARN)) return false;
return mWebContents.hasAccessedInitialDocument();
}
private WebContentsAccessibility getWebContentsAccessibility() {
return WebContentsAccessibility.fromWebContents(mWebContents);
}
public void onProvideVirtualStructure(ViewStructure structure) {
if (TRACE) Log.i(TAG, "%s onProvideVirtualStructure", this);
if (isDestroyed(WARN)) return;
if (!mWebContentsObserver.didEverCommitNavigation()) {
// TODO(sgurun) write a test case for this condition crbug/605251
structure.setChildCount(0);
return;
}
// for webview, the platform already calculates the scroll (as it is a view) in
// ViewStructure tree. Do not offset for it in the snapshop x,y position calculations.
getWebContentsAccessibility().onProvideVirtualStructure(structure, true);
}
public void onProvideAutoFillVirtualStructure(ViewStructure structure, int flags) {
if (TRACE) Log.i(TAG, "%s onProvideAutoFillVirtualStructure", this);
if (mAutofillProvider != null) {
mAutofillProvider.onProvideAutoFillVirtualStructure(structure, flags);
}
}
public void autofill(final SparseArray<AutofillValue> values) {
if (TRACE) Log.i(TAG, "%s autofill", this);
if (mAutofillProvider != null) {
mAutofillProvider.autofill(values);
}
}
public boolean isSelectActionModeAllowed(int actionModeItem) {
return (mSettings.getDisabledActionModeMenuItems() & actionModeItem) != actionModeItem;
}
// --------------------------------------------------------------------------------------------
// View and ViewGroup method implementations
// --------------------------------------------------------------------------------------------
/**
* Calls android.view.View#startActivityForResult. A RuntimeException will
* be thrown by Android framework if startActivityForResult is called with
* a non-Activity context.
*/
void startActivityForResult(Intent intent, int requestCode) {
// Even in fullscreen mode, startActivityForResult will still use the
// initial internal access delegate because it has access to
// the hidden API View#startActivityForResult.
mFullScreenTransitionsState
.getInitialInternalAccessDelegate()
.super_startActivityForResult(intent, requestCode);
}
void startProcessTextIntent(Intent intent) {
if (ContextUtils.activityFromContext(mContext) == null) {
mContext.startActivity(intent);
return;
}
startActivityForResult(intent, PROCESS_TEXT_REQUEST_CODE);
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (TRACE) Log.i(TAG, "%s onActivityResult", this);
if (isDestroyed(NO_WARN)) return;
if (requestCode == PROCESS_TEXT_REQUEST_CODE) {
SelectionPopupController.fromWebContents(mWebContents)
.onReceivedProcessTextResult(resultCode, data);
} else {
Log.e(TAG, "Received activity result for an unknown request code %d", requestCode);
}
}
/** @see android.webkit.View#onTouchEvent() */
public boolean onTouchEvent(MotionEvent event) {
return mAwViewMethods.onTouchEvent(event);
}
/** @see android.view.View#onHoverEvent() */
public boolean onHoverEvent(MotionEvent event) {
return mAwViewMethods.onHoverEvent(event);
}
/** @see android.view.View#onGenericMotionEvent() */
public boolean onGenericMotionEvent(MotionEvent event) {
return isDestroyed(NO_WARN) ? false : mAwViewMethods.onGenericMotionEvent(event);
}
/** @see android.view.View#onConfigurationChanged() */
public void onConfigurationChanged(Configuration newConfig) {
if (TRACE) Log.i(TAG, "%s onConfigurationChanged", this);
mAwViewMethods.onConfigurationChanged(newConfig);
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get().onConfigurationChanged(mNativeAwContents);
}
}
/** @see android.view.View#onAttachedToWindow() */
public void onAttachedToWindow() {
if (TRACE) Log.i(TAG, "%s onAttachedToWindow", this);
mTemporarilyDetached = false;
mAwViewMethods.onAttachedToWindow();
mWindowAndroid.getWindowAndroid().getDisplay().addObserver(mDisplayObserver);
AwWindowCoverageTracker tracker =
AwWindowCoverageTracker.getOrCreateForRootView(this, mContainerView.getRootView());
tracker.trackContents(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (mDisplayCutoutController != null) mDisplayCutoutController.onAttachedToWindow();
}
if (AwFeatureMap.isEnabled(BaseFeatures.COLLECT_ANDROID_FRAME_TIMELINE_METRICS)) {
Window window = mWindowAndroid.getWindowAndroid().getWindow();
if (window != null && mContainerView.isHardwareAccelerated()) {
mAwFrameMetricsListener = AwFrameMetricsListener.onAttachedToWindow(window, this);
}
}
ViewGroup.LayoutParams viewGroupParams = mContainerView.getRootView().getLayoutParams();
if (viewGroupParams instanceof WindowManager.LayoutParams params) {
if (params.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
|| params.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
recordUsedInPopupWindow(UsedInPopupWindow.IN_POPUP_WINDOW);
} else {
recordUsedInPopupWindow(UsedInPopupWindow.NOT_IN_POPUP_WINDOW);
}
} else {
recordUsedInPopupWindow(UsedInPopupWindow.UNKNOWN);
}
}
private static void recordUsedInPopupWindow(@UsedInPopupWindow int value) {
RecordHistogram.recordEnumeratedHistogram(
"Android.WebView.UsedInPopupWindow", value, UsedInPopupWindow.COUNT);
}
private void detachWindowCoverageTracker() {
if (mAwWindowCoverageTracker == null) return;
mAwWindowCoverageTracker.untrackContents(this);
}
/** @see android.view.View#onDetachedFromWindow() */
@SuppressLint("MissingSuperCall")
public void onDetachedFromWindow() {
if (TRACE) Log.i(TAG, "%s onDetachedFromWindow", this);
detachWindowCoverageTracker();
mWindowAndroid.getWindowAndroid().getDisplay().removeObserver(mDisplayObserver);
mAwViewMethods.onDetachedFromWindow();
if (mAwFrameMetricsListener != null) {
Window window = mWindowAndroid.getWindowAndroid().getWindow();
if (window != null && mContainerView.isHardwareAccelerated()) {
AwFrameMetricsListener.onDetachedFromWindow(window, this);
mAwFrameMetricsListener = null;
}
}
}
/** @see android.view.View#onWindowFocusChanged() */
public void onWindowFocusChanged(boolean hasWindowFocus) {
mAwViewMethods.onWindowFocusChanged(hasWindowFocus);
}
/** @see android.view.View#onFocusChanged() */
public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (!mTemporarilyDetached) {
mAwViewMethods.onFocusChanged(focused, direction, previouslyFocusedRect);
}
}
/** @see android.view.View#onStartTemporaryDetach() */
public void onStartTemporaryDetach() {
mTemporarilyDetached = true;
}
/** @see android.view.View#onFinishTemporaryDetach() */
public void onFinishTemporaryDetach() {
mTemporarilyDetached = false;
}
/** @see android.view.View#onSizeChanged() */
public void onSizeChanged(int w, int h, int ow, int oh) {
mAwViewMethods.onSizeChanged(w, h, ow, oh);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (mDisplayCutoutController != null) mDisplayCutoutController.onSizeChanged();
}
}
/** @see android.view.View#onVisibilityChanged() */
public void onVisibilityChanged(View changedView, int visibility) {
mAwViewMethods.onVisibilityChanged(changedView, visibility);
}
/** @see android.view.View#onWindowVisibilityChanged() */
public void onWindowVisibilityChanged(int visibility) {
mAwViewMethods.onWindowVisibilityChanged(visibility);
}
/** @see android.view.View#onResolvePointerIcon(MotionEvent, int) */
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
return mStylusWritingController.resolvePointerIcon();
}
private void setViewVisibilityInternal(boolean visible) {
mIsViewVisible = visible;
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get().setViewVisibility(mNativeAwContents, mIsViewVisible);
}
postUpdateWebContentsVisibility();
}
private void setWindowVisibilityInternal(boolean visible) {
mIsWindowVisible = visible;
if (!isDestroyed(NO_WARN)) {
AwContentsJni.get().setWindowVisibility(mNativeAwContents, mIsWindowVisible);
if (mAwFrameMetricsListener != null) {
if (mIsWindowVisible) {
mAwFrameMetricsListener.onWebviewVisible();
} else {
mAwFrameMetricsListener.onWebviewHidden();
}
}
}
// Using TimeUtils to allow it being overridden in tests.
mLastWindowVisibleTime = visible ? CURRENTLY_VISIBLE : TimeUtils.uptimeMillis();
if (!visible) afterWindowHiddenTask();
postUpdateWebContentsVisibility();
}
private void afterWindowHiddenTask() {
try (TraceEvent e = TraceEvent.scoped("afterWindowHiddenTask")) {
if (isDestroyed(NO_WARN)) return;
if (mLastWindowVisibleTime == CURRENTLY_VISIBLE) return;
long timeNotVisibleMs = TimeUtils.uptimeMillis() - mLastWindowVisibleTime;
// Not in background for long enough.
if (timeNotVisibleMs < FUNCTOR_RECLAIM_DELAY_MS) {
// A task has been posted. If it's not far enough into the future, it will
// reschedule itself, nothing to do.
if (mHasPendingReclaimTask) return;
mHasPendingReclaimTask = true;
// We may have any number of fg <-> bg transitions happen in the meantime. Make
// sure that we only reclaim memory when we've spent enough continuous time in
// background. Use a weak ref to make sure we don't prevent AwContents from being
// GC-eligible while this task is in the queue.
WeakReference<AwContents> weakAwc = new WeakReference(this);
Runnable task =
() -> {
AwContents awc = weakAwc.get();
if (awc != null) {
awc.mHasPendingReclaimTask = false;
awc.afterWindowHiddenTask();
}
};
long delayMs = FUNCTOR_RECLAIM_DELAY_MS - timeNotVisibleMs;
postDelayedTaskWithOverride(task, delayMs);
return;
}
if (mDrawFunctor != null) {
// Clear the functor. This causes native-side resources to be freed. The functor
// will be re-created at the next draw.
setFunctor(null);
// Since we discarded the functor, it is a good time to reclaim resources as well.
AwContentsJni.get()
.trimMemory(
mNativeAwContents, ComponentCallbacks2.TRIM_MEMORY_COMPLETE, false);
}
// Not immediately collecting memory metrics, because actual memory release can take
// some time, either through async tasks here, or in the driver.
postDelayedTaskWithOverride(this::maybeRecordMemory, METRICS_COLLECTION_DELAY_MS);
}
}
/* PostTask can be overridden for testing. */
private void postDelayedTaskWithOverride(Runnable task, long delayMs) {
if (mPostDelayedTaskForTesting != null) {
mPostDelayedTaskForTesting.apply(task, delayMs);
} else {
PostTask.postDelayedTask(TaskTraits.UI_DEFAULT, task, delayMs);
}
}
@VisibleForTesting
public boolean hasDrawFunctor() {
return mDrawFunctor != null;
}
public void setPostDelayedTaskForTesting(BiFunction<Runnable, Long, Void> fn) {
mPostDelayedTaskForTesting = fn;
}
private void postUpdateWebContentsVisibility() {
if (mIsUpdateVisibilityTaskPending) return;
// When WebView is attached to a visible window, WebView will be
// attached to a window whose visibility is initially invisible, then
// the window visibility will be updated to true. This means CVC
// visibility will be set to false then true immediately, in the same
// function call of View#dispatchAttachedToWindow. DetachedFromWindow
// is a similar case, where window visibility changes before AwContents
// is detached from window.
//
// To prevent this flip of CVC visibility, post the task to update CVC
// visibility during attach, detach and window visibility change.
mIsUpdateVisibilityTaskPending = true;
PostTask.postTask(TaskTraits.UI_DEFAULT, mUpdateVisibilityRunnable);
}
private void updateWebContentsVisibility() {
mIsUpdateVisibilityTaskPending = false;
if (isDestroyed(NO_WARN)) return;
boolean contentVisible = AwContentsJni.get().isVisible(mNativeAwContents);
if (contentVisible && !mIsContentVisible) {
mWebContents.onShow();
} else if (!contentVisible && mIsContentVisible) {
mWebContents.onHide();
}
mIsContentVisible = contentVisible;
updateChildProcessImportance();
}
/**
* Returns true if the page is visible according to DOM page visibility API.
* See http://www.w3.org/TR/page-visibility/
* This method is only called by tests and will return the supposed CVC
* visibility without waiting a pending mUpdateVisibilityRunnable to run.
*/
@VisibleForTesting
public boolean isPageVisible() {
if (isDestroyed(NO_WARN)) return mIsContentVisible;
return AwContentsJni.get().isVisible(mNativeAwContents);
}
/**
* Returns true if the web contents has an associated interstitial.
* This method is only called by tests.
*/
public boolean isDisplayingInterstitialForTesting() {
return AwContentsJni.get().isDisplayingInterstitialForTesting(mNativeAwContents);
}
/** 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 (TRACE) Log.i(TAG, "%s saveState", this);
if (isDestroyed(WARN) || outState == null) return false;
byte[] state = AwContentsJni.get().getOpaqueState(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 (TRACE) Log.i(TAG, "%s restoreState", this);
if (isDestroyed(WARN) || inState == null) return false;
byte[] state = inState.getByteArray(SAVE_RESTORE_STATE_KEY);
if (state == null) return false;
boolean result = AwContentsJni.get().restoreFromOpaqueState(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(mWebContents.getTitle());
return result;
}
/** @see JavascriptInjector#addPossiblyUnsafeInterface(Object, String, Class) */
public void addJavascriptInterface(Object object, String name) {
if (TRACE) Log.i(TAG, "%s addJavascriptInterface=%s", this, name);
if (isDestroyed(WARN)) return;
Class<? extends Annotation> requiredAnnotation = null;
if (mAppTargetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
requiredAnnotation = JavascriptInterface.class;
}
getJavascriptInjector().addPossiblyUnsafeInterface(object, name, requiredAnnotation);
}
/** @see android.webkit.WebView#removeJavascriptInterface(String) */
public void removeJavascriptInterface(String interfaceName) {
if (TRACE) Log.i(TAG, "%s removeInterface=%s", this, interfaceName);
if (isDestroyed(WARN)) return;
getJavascriptInjector().removeInterface(interfaceName);
}
/**
* If native accessibility (not script injection) is enabled, and if this is
* running on JellyBean or later, returns an AccessibilityNodeProvider that
* implements native accessibility for this view. Returns null otherwise.
* @return The AccessibilityNodeProvider, if available, or null otherwise.
*/
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
return mAwViewMethods.getAccessibilityNodeProvider();
}
/** @see android.webkit.WebView#performAccessibilityAction(int, Bundle) */
public boolean performAccessibilityAction(int action, Bundle arguments) {
return mAwViewMethods.performAccessibilityAction(action, arguments);
}
/**
* @see android.webkit.WebView#clearFormData().
* <p>The popup shall also be hidden on the WebView detached from window.
*/
public void hideAutofillPopup() {
if (TRACE) Log.i(TAG, "%s hideAutofillPopup", this);
if (mAutofillProvider != null) {
mAutofillProvider.hideDatalistPopup();
}
}
public void setNetworkAvailable(boolean networkUp) {
if (TRACE) Log.i(TAG, "%s setNetworkAvailable=%s", this, networkUp);
if (!isDestroyed(WARN)) {
// For backward compatibility when an app uses this API disable the
// Network Information API to prevent inconsistencies,
// see crbug.com/520088.
NetworkChangeNotifier.setAutoDetectConnectivityState(false);
AwContentsJni.get().setJsOnlineProperty(mNativeAwContents, networkUp);
}
}
/**
* Inserts a {@link VisualStateCallback}.
*
* The {@link VisualStateCallback} will be inserted in Blink and will be invoked when the
* contents of the DOM tree at the moment that the callback was inserted (or later) are drawn
* into the screen. In other words, the following events need to happen before the callback is
* invoked:
* 1. The DOM tree is committed becoming the pending tree - see ThreadProxy::BeginMainFrame
* 2. The pending tree is activated becoming the active tree
* 3. A frame swap happens that draws the active tree into the screen
*
* @param requestId an id that will be returned from the callback invocation to allow
* callers to match requests with callbacks.
* @param callback the callback to be inserted
* @throw IllegalStateException if this method is invoked after {@link #destroy()} has been
* called.
*/
public void insertVisualStateCallback(long requestId, VisualStateCallback callback) {
if (TRACE) Log.i(TAG, "%s insertVisualStateCallback", this);
if (isDestroyed(NO_WARN)) {
throw new IllegalStateException(
"insertVisualStateCallback cannot be called after the WebView has been"
+ " destroyed");
}
if (callback == null) {
throw new IllegalArgumentException("VisualStateCallback shouldn't be null");
}
AwContentsJni.get().insertVisualStateCallback(mNativeAwContents, requestId, callback);
}
public boolean isPopupWindow() {
return mIsPopupWindow;
}
private void updateChildProcessImportance() {
@ChildProcessImportance int effectiveImportance = ChildProcessImportance.IMPORTANT;
if (mRendererPriorityWaivedWhenNotVisible && !mIsContentVisible) {
effectiveImportance = ChildProcessImportance.NORMAL;
} else {
switch (mRendererPriority) {
case RendererPriority.HIGH:
effectiveImportance = ChildProcessImportance.IMPORTANT;
break;
case RendererPriority.LOW:
effectiveImportance = ChildProcessImportance.MODERATE;
break;
case RendererPriority.WAIVED:
effectiveImportance = ChildProcessImportance.NORMAL;
break;
default:
assert false;
}
}
mWebContents.setImportance(effectiveImportance);
}
@RendererPriority
public int getRendererRequestedPriority() {
return mRendererPriority;
}
public boolean getRendererPriorityWaivedWhenNotVisible() {
return mRendererPriorityWaivedWhenNotVisible;
}
public void setAudioMuted(boolean mute) {
if (AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_MUTE_AUDIO)) {
mWebContents.setAudioMuted(mute);
}
}
public boolean isAudioMuted() {
if (AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_MUTE_AUDIO)) {
return mWebContents.isAudioMuted();
}
return false;
}
public void setRendererPriorityPolicy(
@RendererPriority int rendererRequestedPriority, boolean waivedWhenNotVisible) {
if (TRACE) Log.i(TAG, "%s setRendererPriorityPolicy", this);
mRendererPriority = rendererRequestedPriority;
mRendererPriorityWaivedWhenNotVisible = waivedWhenNotVisible;
updateChildProcessImportance();
}
public void setTextClassifier(TextClassifier textClassifier) {
if (TRACE) Log.i(TAG, "%s setTextClassifier", this);
assert mWebContents != null;
SelectionPopupController.fromWebContents(mWebContents).setTextClassifier(textClassifier);
}
public TextClassifier getTextClassifier() {
if (TRACE) Log.i(TAG, "%s getTextClassifier", this);
assert mWebContents != null;
return SelectionPopupController.fromWebContents(mWebContents).getTextClassifier();
}
public AwRenderProcess getRenderProcess() {
if (isDestroyed(WARN)) {
return null;
}
return AwContentsJni.get().getRenderProcess(mNativeAwContents);
}
@VisibleForTesting
public AwDisplayCutoutController getDisplayCutoutController() {
return mDisplayCutoutController;
}
public int getDisplayMode() {
return mDisplayModeController.getDisplayMode();
}
// --------------------------------------------------------------------------------------------
// 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;
}
@CalledByNative
private long onCreateTouchHandle() {
PopupTouchHandleDrawable drawable =
PopupTouchHandleDrawable.create(
mTouchHandleDrawables, mWebContents, mContainerView);
return drawable.getNativeDrawable();
}
/** Callback for generateMHTML. */
@CalledByNative
private static void generateMHTMLCallback(String path, long size, Callback<String> callback) {
if (callback == null) return;
AwThreadUtils.postToUiThreadLooper(
() -> {
callback.onResult(size < 0 ? null : path);
});
}
@CalledByNative
private void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
try (TraceEvent event =
TraceEvent.scoped("WebView.APICallback.ON_RECEIVED_HTTP_AUTH_REQUEST")) {
mContentsClient.onReceivedHttpAuthRequest(handler, host, realm);
AwHistogramRecorder.recordCallbackInvocation(
AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_HTTP_AUTH_REQUEST);
}
}
public AwGeolocationPermissions getGeolocationPermissions() {
return mBrowserContext.getGeolocationPermissions();
}
public void invokeGeolocationCallback(boolean value, String requestingFrame) {
if (TRACE) Log.i(TAG, "%s invokeGeolocationCallback", this);
if (isDestroyed(NO_WARN)) return;
AwContentsJni.get().invokeGeolocationCallback(mNativeAwContents, value, requestingFrame);
}
@CalledByNative
private void onGeolocationPermissionsShowPrompt(String origin) {
if (isDestroyed(NO_WARN)) return;
AwGeolocationPermissions permissions = mBrowserContext.getGeolocationPermissions();
// Reject if geoloaction is disabled, or the origin has a retained deny
if (!mSettings.getGeolocationEnabled()) {
AwContentsJni.get().invokeGeolocationCallback(mNativeAwContents, false, origin);
return;
}
// Allow if the origin has a retained allow
if (permissions.hasOrigin(origin)) {
AwContentsJni.get()
.invokeGeolocationCallback(
mNativeAwContents, permissions.isOriginAllowed(origin), origin);
return;
}
mContentsClient.onGeolocationPermissionsShowPrompt(
origin, new AwGeolocationCallback(origin, this));
}
@CalledByNative
private void onGeolocationPermissionsHidePrompt() {
mContentsClient.onGeolocationPermissionsHidePrompt();
}
@CalledByNative
private void onPermissionRequest(AwPermissionRequest awPermissionRequest) {
mContentsClient.onPermissionRequest(awPermissionRequest);
}
@CalledByNative
private void onPermissionRequestCanceled(AwPermissionRequest awPermissionRequest) {
mContentsClient.onPermissionRequestCanceled(awPermissionRequest);
}
@CalledByNative
private void logOriginVisit(long originHash) {
if (isDestroyed(NO_WARN)) return;
PostTask.postTask(
TaskTraits.BEST_EFFORT_MAY_BLOCK,
() -> AwOriginVisitLogger.logOriginVisit(originHash));
}
@CalledByNative
public void onFindResultReceived(
int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
mContentsClient.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
}
@CalledByNative
public void onNewPicture() {
// Don't call capturePicture() here but instead defer it until the posted task runs within
// the callback helper, to avoid doubling back into the renderer compositor in the middle
// of the notification it is sending up to here.
mContentsClient.getCallbackHelper().postOnNewPicture(mPictureListenerContentProvider);
}
@CalledByNative
public void onPreferredFrameIntervalChanged(long preferredFrameIntervalNanos) {
mPreferredFrameIntervalNanos = preferredFrameIntervalNanos;
}
/**
* Invokes the given {@link VisualStateCallback}.
*
* @param callback the callback to be invoked
* @param requestId the id passed to {@link AwContents#insertVisualStateCallback}
* @param result true if the callback should succeed and false otherwise
*/
@CalledByNative
public void invokeVisualStateCallback(
final VisualStateCallback callback, final long requestId) {
if (isDestroyed(NO_WARN)) return;
// Posting avoids invoking the callback inside invoking_composite_
// (see synchronous_compositor_impl.cc and crbug/452530).
AwThreadUtils.postToUiThreadLooper(() -> callback.onComplete(requestId));
}
// Called as a result of AwContentsJni.get().updateLastHitTestData.
@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 postInvalidate(boolean insideVSync) {
if (insideVSync) {
mContainerView.invalidate();
} else {
mContainerView.postInvalidateOnAnimation();
}
}
@CalledByNative
private int[] getLocationOnScreen() {
int[] result = new int[2];
mContainerView.getLocationOnScreen(result);
return result;
}
@CalledByNative
private void onWebLayoutPageScaleFactorChanged(float webLayoutPageScaleFactor) {
// This change notification comes from the renderer thread, not from the cc/ impl thread.
mLayoutSizer.onPageScaleChanged(webLayoutPageScaleFactor);
}
@CalledByNative
private void onWebLayoutContentsSizeChanged(int widthCss, int heightCss) {
// This change notification comes from the renderer thread, not from the cc/ impl thread.
mLayoutSizer.onContentSizeChanged(widthCss, heightCss);
}
@CalledByNative
private void scrollContainerViewTo(int xPx, int yPx) {
// Both xPx and yPx are in physical pixel scale.
mScrollOffsetManager.scrollContainerViewTo(xPx, yPx);
}
@CalledByNative
private void updateScrollState(
int maxContainerViewScrollOffsetX,
int maxContainerViewScrollOffsetY,
float contentWidthDip,
float contentHeightDip,
float pageScaleFactor,
float minPageScaleFactor,
float maxPageScaleFactor) {
mContentWidthDip = contentWidthDip;
mContentHeightDip = contentHeightDip;
mScrollOffsetManager.setMaxScrollOffset(
maxContainerViewScrollOffsetX, maxContainerViewScrollOffsetY);
setPageScaleFactorAndLimits(pageScaleFactor, minPageScaleFactor, maxPageScaleFactor);
}
@CalledByNative
private void didOverscroll(
int deltaX, int deltaY, float velocityX, float velocityY, boolean insideVSync) {
mScrollOffsetManager.overScrollBy(deltaX, deltaY);
if (mOverScrollGlow == null) return;
mOverScrollGlow.setOverScrollDeltas(deltaX, deltaY);
final int oldX = mContainerView.getScrollX();
final int oldY = mContainerView.getScrollY();
final int x = oldX + deltaX;
final int y = oldY + deltaY;
final int scrollRangeX = mScrollOffsetManager.computeMaximumHorizontalScrollOffset();
final int scrollRangeY = mScrollOffsetManager.computeMaximumVerticalScrollOffset();
// absorbGlow() will release the glow if it is not finished.
mOverScrollGlow.absorbGlow(
x,
y,
oldX,
oldY,
scrollRangeX,
scrollRangeY,
(float) Math.hypot(velocityX, velocityY));
if (mOverScrollGlow.isAnimating()) {
postInvalidate(insideVSync);
}
}
/** Determine if at least one edge of the WebView extends over the edge of the window. */
private boolean extendsOutOfWindow() {
int[] loc = new int[2];
mContainerView.getLocationOnScreen(loc);
int x = loc[0];
int y = loc[1];
mContainerView.getRootView().getLocationOnScreen(loc);
int rootX = loc[0];
int rootY = loc[1];
// Get the position of the current view, relative to its root view
int relativeX = x - rootX;
int relativeY = y - rootY;
if (relativeX < 0
|| relativeY < 0
|| relativeX + mContainerView.getWidth() > mContainerView.getRootView().getWidth()
|| relativeY + mContainerView.getHeight()
> mContainerView.getRootView().getHeight()) {
return true;
}
return false;
}
/**
* Determine if it's reasonable to show any sort of interstitial. If the WebView is not visible,
* the user may not be able to interact with the UI.
* @return true if the WebView is visible
*/
@VisibleForTesting
@CalledByNative
protected boolean canShowInterstitial() {
return mIsAttachedToWindow && mIsViewVisible;
}
@CalledByNative
private int getErrorUiType() {
if (extendsOutOfWindow()) {
return ErrorUiType.QUIET_GIANT;
} else if (canShowBigInterstitial()) {
return ErrorUiType.LOUD;
} else {
return ErrorUiType.QUIET_SMALL;
}
}
/**
* Determine if it's suitable to show the interstitial for browsers and main UIs. If the WebView
* is close to full-screen, we assume the app is using it as the main UI, so we show the same
* interstitial Chrome uses. Otherwise, we assume the WebView is part of a larger composed page,
* and will show a different interstitial.
* @return true if the WebView should display the large interstitial
*/
@VisibleForTesting
protected boolean canShowBigInterstitial() {
double percentOfScreenHeight =
(double) mContainerView.getHeight() / mContainerView.getRootView().getHeight();
// Make a guess as to whether the WebView is the predominant part of the UI
return mContainerView.getWidth() == mContainerView.getRootView().getWidth()
&& percentOfScreenHeight >= MIN_SCREEN_HEIGHT_PERCENTAGE_FOR_INTERSTITIAL;
}
/**
* Return the device locale in the same format we use to populate the 'hl' query parameter for
* Safe Browsing interstitial urls, as done in BaseUIManager::app_locale().
*/
public static String getSafeBrowsingLocaleForTesting() {
return AwContentsJni.get().getSafeBrowsingLocaleForTesting();
}
/** Returns the AwContents instance associated with |webContents|, or NULL */
public static AwContents fromWebContents(WebContents webContents) {
return AwContentsJni.get().fromWebContents(webContents);
}
// -------------------------------------------------------------------------------------------
// Helper methods
// -------------------------------------------------------------------------------------------
private void setPageScaleFactorAndLimits(
float pageScaleFactor, float minPageScaleFactor, float maxPageScaleFactor) {
if (mPageScaleFactor == pageScaleFactor
&& mMinPageScaleFactor == minPageScaleFactor
&& mMaxPageScaleFactor == maxPageScaleFactor) {
return;
}
mMinPageScaleFactor = minPageScaleFactor;
mMaxPageScaleFactor = maxPageScaleFactor;
if (mPageScaleFactor != pageScaleFactor) {
float oldPageScaleFactor = mPageScaleFactor;
mPageScaleFactor = pageScaleFactor;
float dipScale = getDeviceScaleFactor();
mContentsClient
.getCallbackHelper()
.postOnScaleChangedScaled(
oldPageScaleFactor * dipScale, mPageScaleFactor * dipScale);
}
mZoomControls.updateZoomControls();
}
private void saveWebArchiveInternal(String path, final Callback<String> callback) {
if (path == null || isDestroyed(WARN)) {
if (callback == null) return;
AwThreadUtils.postToUiThreadLooper(callback.bind(null));
} else {
AwContentsJni.get().generateMHTML(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: %s", baseName);
return null;
}
@Override
public void extractSmartClipData(int x, int y, int width, int height) {
if (TRACE) Log.i(TAG, "%s extractSmartClipData", this);
if (!isDestroyed(WARN)) {
mWebContents.requestSmartClipExtract(x, y, width, height);
}
}
@Override
public void setSmartClipResultHandler(final Handler resultHandler) {
if (TRACE) Log.i(TAG, "%s setSmartClipResultHandler", this);
if (isDestroyed(WARN)) return;
mWebContents.setSmartClipResultHandler(resultHandler);
}
protected void insertVisualStateCallbackIfNotDestroyed(
long requestId, VisualStateCallback callback) {
if (TRACE) Log.i(TAG, "%s insertVisualStateCallbackIfNotDestroyed", this);
if (isDestroyed(NO_WARN)) return;
AwContentsJni.get().insertVisualStateCallback(mNativeAwContents, requestId, callback);
}
public static boolean isDpadEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
return true;
}
}
return false;
}
@VisibleForTesting
public void onTrimMemory(final int level) {
try (TraceEvent e = TraceEvent.scoped("onTrimMemory", String.valueOf(level))) {
boolean visibleRectEmpty = getGlobalVisibleRect().isEmpty();
final boolean visible = mIsViewVisible && mIsWindowVisible && !visibleRectEmpty;
ThreadUtils.runOnUiThreadBlocking(
() -> {
if (isDestroyed(NO_WARN)) return;
// Post the task in the case where we would have cleared the functor if the
// feature was enabled, so that the two experiment arms have the same
// number of samples.
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
postDelayedTaskWithOverride(
this::maybeRecordMemory, METRICS_COLLECTION_DELAY_MS);
if (mDrawFunctor != null) {
mDrawFunctor.trimMemory();
setFunctor(null);
}
}
AwContentsJni.get().trimMemory(mNativeAwContents, level, visible);
});
}
}
private void maybeRecordMemory() {
// Note: there is a corner case here: if there are no visible WebViews, but the last one
// was removed too recently to have had its functor reclaimed, we still collect data.
// This likely doesn't matter too much, especially since as noted below, the metrics are
// expected to only be useful to tell whether the experiment produces a signal.
if (AwContentsLifecycleNotifier.getInstance().getAppState() != AppState.BACKGROUND) return;
// Comment below from base/android/meminfo_dump_provider.cc:
//
// This is best-effort, and will be wrong if there are other callers of
// ActivityManager#getProcessMemoryInfo(), either in this process or from another
// process which is allowed to do so (typically, adb).
//
// However, since the framework doesn't document throttling in any non-vague terms and
// the results are not timestamped, this is the best we can do. The delay and the rest
// of the assumptions here come from
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android13-dev/services/core/java/com/android/server/am/ActivityManagerService.java#4093.
//
// We could always report the value on pre-Q devices, but that would skew reported
// data. Also, some OEMs may have cherry-picked the Q change, meaning that it's safer
// and more accurate to not report likely-stale data on all Android releases.
//
// Nevertheless, this has proved useful to detect whether an experiment is doing
// *something* for Chromium (the browser, not WebView), where is it collected as part of
// memory metrics, that are not collected in WebView.
long now = SystemClock.uptimeMillis();
if (now - sLastCollectionTime < MEMORY_COLLECTION_INTERVAL_MS) return;
sLastCollectionTime = now;
Runnable recordMetrics =
() -> {
Debug.MemoryInfo info = MemoryInfoBridge.getActivityManagerMemoryInfoForSelf();
if (info == null) return;
RecordHistogram.recordMemoryMediumMBHistogram(
PSS_HISTOGRAM, info.otherPss / 1024);
RecordHistogram.recordMemoryMediumMBHistogram(
PRIVATE_DIRTY_HISTOGRAM, info.otherPrivateDirty / 1024);
};
// Record synchronously for testing, to reduce flakiness.
if (mPostDelayedTaskForTesting != null) {
recordMetrics.run();
} else {
AsyncTask.THREAD_POOL_EXECUTOR.execute(recordMetrics);
}
}
public static void resetRecordMemoryForTesting() {
sLastCollectionTime = -MEMORY_COLLECTION_INTERVAL_MS;
}
// --------------------------------------------------------------------------------------------
// This is the AwViewMethods implementation that does real work. The AwViewMethodsImpl is
// hooked up to the WebView in embedded mode and to the FullScreenView in fullscreen mode,
// but not to both at the same time.
private class AwViewMethodsImpl implements AwViewMethods {
private int mLayerType = View.LAYER_TYPE_NONE;
private ComponentCallbacks2 mComponentCallbacks;
// Only valid within software onDraw().
private final Rect mClipBoundsTemporary = new Rect();
// Variables that track the state as of the previous onDraw call.
private Rect mPreviousGlobalVisibleRect = new Rect();
private boolean mPreviouslyVisible;
private String mPreviousScheme = "";
@SuppressLint("DrawAllocation") // For new AwFunctor.
@Override
public void onDraw(Canvas canvas) {
if (isDestroyed(NO_WARN)) {
TraceEvent.instant("EarlyOut_destroyed");
canvas.drawColor(getEffectiveBackgroundColor());
return;
}
// For hardware draws, the clip at onDraw time could be different
// from the clip during DrawGL.
if (!canvas.isHardwareAccelerated() && !canvas.getClipBounds(mClipBoundsTemporary)) {
TraceEvent.instant("EarlyOut_software_empty_clip");
return;
}
if (canvas.isHardwareAccelerated() && mDrawFunctor == null) {
AwFunctor newFunctor;
AwDrawFnImpl.DrawFnAccess drawFnAccess =
mNativeDrawFunctorFactory.getDrawFnAccess();
if (drawFnAccess != null) {
newFunctor = new AwDrawFnImpl(drawFnAccess);
} else {
newFunctor = new AwGLFunctor(mNativeDrawFunctorFactory, mContainerView);
}
setFunctor(newFunctor);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
&& AwFeatureMap.isEnabled(VizFeatures.WEBVIEW_FRAME_RATE_HINTS)) {
float frame_rate = View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE;
if (mPreferredFrameIntervalNanos > 0) {
frame_rate = (float) 1e9 / mPreferredFrameIntervalNanos;
}
mContainerView.setRequestedFrameRate(frame_rate);
float velocity =
AwContentsJni.get().getVelocityInPixelsPerSecond(mNativeAwContents);
mContainerView.setFrameContentVelocity(velocity);
}
mScrollOffsetManager.syncScrollOffsetFromOnDraw();
int scrollX = mContainerView.getScrollX();
int scrollY = mContainerView.getScrollY();
Rect globalVisibleRect = getGlobalVisibleRect();
if (mAwWindowCoverageTracker != null) {
boolean visible = mIsAttachedToWindow && mIsViewVisible && mIsWindowVisible;
String scheme = AwContentsJni.get().getScheme(mNativeAwContents);
if (!globalVisibleRect.equals(mPreviousGlobalVisibleRect)
|| mPreviouslyVisible != visible
|| !mPreviousScheme.equals(scheme)) {
mPreviousGlobalVisibleRect.set(globalVisibleRect);
mPreviouslyVisible = visible;
mPreviousScheme = scheme;
mAwWindowCoverageTracker.onInputsUpdated();
}
}
boolean did_draw =
AwContentsJni.get()
.onDraw(
mNativeAwContents,
canvas,
canvas.isHardwareAccelerated(),
scrollX,
scrollY,
globalVisibleRect.left,
globalVisibleRect.top,
globalVisibleRect.right,
globalVisibleRect.bottom,
ForceAuxiliaryBitmapRendering.sResult);
if (canvas.isHardwareAccelerated()
&& !ForceAuxiliaryBitmapRendering.sResult
&& AwContentsJni.get().needToDrawBackgroundColor(mNativeAwContents)) {
TraceEvent.instant("DrawBackgroundColor");
canvas.drawColor(getEffectiveBackgroundColor());
}
if (did_draw
&& canvas.isHardwareAccelerated()
&& !ForceAuxiliaryBitmapRendering.sResult) {
did_draw = mDrawFunctor.requestDraw(canvas);
}
if (did_draw) {
int scrollXDiff = mContainerView.getScrollX() - scrollX;
int scrollYDiff = mContainerView.getScrollY() - scrollY;
canvas.translate(-scrollXDiff, -scrollYDiff);
} else {
TraceEvent.instant("NativeDrawFailed");
canvas.drawColor(getEffectiveBackgroundColor());
}
if (mOverScrollGlow != null
&& mOverScrollGlow.drawEdgeGlows(
canvas,
mScrollOffsetManager.computeMaximumHorizontalScrollOffset(),
mScrollOffsetManager.computeMaximumVerticalScrollOffset())) {
mContainerView.postInvalidateOnAnimation();
}
if (mInvalidateRootViewOnNextDraw) {
mContainerView.getRootView().invalidate();
mInvalidateRootViewOnNextDraw = false;
}
// Tint everything one color, to make WebViews easier to spot.
if (CommandLine.getInstance().hasSwitch(AwSwitches.HIGHLIGHT_ALL_WEBVIEWS)) {
int semiTransparentYellow = Color.argb(80, 252, 252, 109);
canvas.drawColor(semiTransparentYellow);
}
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mLayoutSizer.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void requestFocus() {
if (isDestroyed(NO_WARN)) return;
if (!mContainerView.isInTouchMode() && mSettings.shouldFocusFirstNode()) {
AwContentsJni.get().focusFirstNode(mNativeAwContents);
}
}
@Override
public void setLayerType(int layerType, Paint paint) {
mLayerType = layerType;
updateHardwareAcceleratedFeaturesToggle();
}
private void updateHardwareAcceleratedFeaturesToggle() {
mSettings.setEnableSupportedHardwareAcceleratedFeatures(
mIsAttachedToWindow
&& mContainerView.isHardwareAccelerated()
&& (mLayerType == View.LAYER_TYPE_NONE
|| mLayerType == View.LAYER_TYPE_HARDWARE));
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return isDestroyed(NO_WARN)
? null
: ImeAdapter.fromWebContents(mWebContents).onCreateInputConnection(outAttrs);
}
@Override
public boolean onDragEvent(DragEvent event) {
if (isDestroyed(NO_WARN)) {
return false;
}
if (AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_DRAG_DROP_FILES)) {
if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
releaseDragAndDropPermissions();
} else if (event.getAction() == DragEvent.ACTION_DROP) {
Activity activity = ContextUtils.activityFromContext(mContext);
if (activity != null) {
mDragAndDropPermissions = activity.requestDragAndDropPermissions(event);
}
}
}
return mWebContents.getEventForwarder().onDragEvent(event, mContainerView);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return isDestroyed(NO_WARN)
? false
: mWebContents.getEventForwarder().onKeyUp(keyCode, event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (isDestroyed(NO_WARN)) return false;
if (isDpadEvent(event)) {
mSettings.setSpatialNavigationEnabled(true);
}
// Following check is dup'ed from |ContentUiEventHandler.dispatchKeyEvent| to avoid
// embedder-specific customization, which is necessary only for WebView.
if (GamepadList.dispatchKeyEvent(event)) return true;
// This check reflects Chrome's behavior and is a workaround for http://b/7697782.
if (mContentsClient.hasWebViewClient()
&& mContentsClient.shouldOverrideKeyEvent(event)) {
return mInternalAccessAdapter.super_dispatchKeyEvent(event);
}
return mWebContents.getEventForwarder().dispatchKeyEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (TouchEventFilter.hasInvalidToolType(event)) return false;
if (isDestroyed(NO_WARN)) return false;
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mSettings.setSpatialNavigationEnabled(false);
}
AwContentsJni.get().onInputEvent(mNativeAwContents);
mScrollOffsetManager.setProcessingTouchEvent(true);
boolean rv = mWebContents.getEventForwarder().onTouchEvent(event);
mScrollOffsetManager.setProcessingTouchEvent(false);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
// Note this will trigger IPC back to browser even if nothing is
// hit.
float eventX = event.getX();
float eventY = event.getY();
float touchMajor = Math.max(event.getTouchMajor(), event.getTouchMinor());
AwContentsJni.get()
.requestNewHitTestDataAt(mNativeAwContents, eventX, eventY, touchMajor);
// If the stylus is above an editable element, prevent the parent element from
// intercepting the scroll event.
if (event.getPointerCount() == 1
&& event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
&& getLastHitTestResult().hitTestResultType
== 9 /* HitTestDataType::kEditText */) {
mContainerView.getParent().requestDisallowInterceptTouchEvent(true);
}
}
if (mOverScrollGlow != null) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mOverScrollGlow.setShouldPull(true);
} else if (event.getActionMasked() == MotionEvent.ACTION_UP
|| event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
mOverScrollGlow.setShouldPull(false);
mOverScrollGlow.releaseAll();
}
}
return rv;
}
@Override
public boolean onHoverEvent(MotionEvent event) {
return isDestroyed(NO_WARN)
? false
: mWebContents.getEventForwarder().onHoverEvent(event);
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return isDestroyed(NO_WARN)
? false
: mWebContents.getEventForwarder().onGenericMotionEvent(event);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (!isDestroyed(NO_WARN)) {
mViewEventSink.onConfigurationChanged(newConfig);
mInternalAccessAdapter.super_onConfigurationChanged(newConfig);
}
}
@Override
public void onAttachedToWindow() {
if (isDestroyed(NO_WARN)) return;
if (mIsAttachedToWindow) {
Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
return;
}
mIsAttachedToWindow = true;
mViewEventSink.onAttachedToWindow();
AwContentsJni.get()
.onAttachedToWindow(
mNativeAwContents,
mContainerView.getWidth(),
mContainerView.getHeight());
updateHardwareAcceleratedFeaturesToggle();
postUpdateWebContentsVisibility();
updateDefaultLocale();
if (mComponentCallbacks != null) return;
mComponentCallbacks = new AwComponentCallbacks();
mContext.registerComponentCallbacks(mComponentCallbacks);
}
@Override
public void onDetachedFromWindow() {
if (isDestroyed(NO_WARN)) return;
if (!mIsAttachedToWindow) {
Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
return;
}
mIsAttachedToWindow = false;
hideAutofillPopup();
AwContentsJni.get().onDetachedFromWindow(mNativeAwContents);
mViewEventSink.onDetachedFromWindow();
updateHardwareAcceleratedFeaturesToggle();
postUpdateWebContentsVisibility();
setFunctor(null);
if (mComponentCallbacks != null) {
mContext.unregisterComponentCallbacks(mComponentCallbacks);
mComponentCallbacks = null;
}
mScrollAccessibilityHelper.removePostedCallbacks();
mZoomControls.dismissZoomPicker();
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (isDestroyed(NO_WARN)) return;
mWindowFocused = hasWindowFocus;
mViewEventSink.onWindowFocusChanged(hasWindowFocus);
mStylusWritingController.onWindowFocusChanged(hasWindowFocus);
Clipboard.getInstance().onWindowFocusChanged(hasWindowFocus);
}
@Override
public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (isDestroyed(NO_WARN)) return;
mContainerViewFocused = focused;
mViewEventSink.onViewFocusChanged(focused);
}
@Override
public void onSizeChanged(int w, int h, int ow, int oh) {
if (isDestroyed(NO_WARN)) return;
mScrollOffsetManager.setContainerViewSize(w, h);
// The AwLayoutSizer needs to go first so that if we're in
// fixedLayoutSize mode the update to enter fixedLayoutSize mode is sent before the
// first resize update.
mLayoutSizer.onSizeChanged(w, h, ow, oh);
AwContentsJni.get().onSizeChanged(mNativeAwContents, w, h, ow, oh);
}
@Override
public void onVisibilityChanged(View changedView, int visibility) {
boolean viewVisible = mContainerView.getVisibility() == View.VISIBLE;
if (mIsViewVisible == viewVisible) return;
setViewVisibilityInternal(viewVisible);
}
@Override
public void onWindowVisibilityChanged(int visibility) {
boolean windowVisible = visibility == View.VISIBLE;
if (mIsWindowVisible == windowVisible) return;
setWindowVisibilityInternal(windowVisible);
}
@Override
public void onContainerViewScrollChanged(int l, int t, int oldl, int oldt) {
// A side-effect of View.onScrollChanged is that the scroll accessibility event being
// sent by the base class implementation. This is completely hidden from the base
// classes and cannot be prevented, which is why we need the code below.
mScrollAccessibilityHelper.removePostedViewScrolledAccessibilityEventCallback();
mScrollOffsetManager.onContainerViewScrollChanged(l, t);
}
@Override
public void onContainerViewOverScrolled(
int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
int oldX = mContainerView.getScrollX();
int oldY = mContainerView.getScrollY();
mScrollOffsetManager.onContainerViewOverScrolled(scrollX, scrollY, clampedX, clampedY);
if (mOverScrollGlow != null) {
mOverScrollGlow.pullGlow(
mContainerView.getScrollX(),
mContainerView.getScrollY(),
oldX,
oldY,
mScrollOffsetManager.computeMaximumHorizontalScrollOffset(),
mScrollOffsetManager.computeMaximumVerticalScrollOffset());
}
}
@Override
public int computeHorizontalScrollRange() {
return mScrollOffsetManager.computeHorizontalScrollRange();
}
@Override
public int computeHorizontalScrollOffset() {
return mScrollOffsetManager.computeHorizontalScrollOffset();
}
@Override
public int computeVerticalScrollRange() {
return mScrollOffsetManager.computeVerticalScrollRange();
}
@Override
public int computeVerticalScrollOffset() {
return mScrollOffsetManager.computeVerticalScrollOffset();
}
@Override
public int computeVerticalScrollExtent() {
return mScrollOffsetManager.computeVerticalScrollExtent();
}
@Override
public void computeScroll() {
if (isDestroyed(NO_WARN)) return;
AwContentsJni.get()
.onComputeScroll(
mNativeAwContents, AnimationUtils.currentAnimationTimeMillis());
}
@Override
public boolean onCheckIsTextEditor() {
if (isDestroyed(NO_WARN)) return false;
ImeAdapter imeAdapter = ImeAdapter.fromWebContents(mWebContents);
return imeAdapter != null ? imeAdapter.onCheckIsTextEditor() : false;
}
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (isDestroyed(NO_WARN)) return null;
WebContentsAccessibility wcax = getWebContentsAccessibility();
return wcax != null ? wcax.getAccessibilityNodeProvider() : null;
}
@Override
public boolean performAccessibilityAction(final int action, final Bundle arguments) {
return false;
}
}
// Return true if the GeolocationPermissionAPI should be used.
@CalledByNative
private boolean useLegacyGeolocationPermissionAPI() {
// Always return true since we are not ready to swap the geolocation yet.
// TODO: If we decide not to migrate the geolocation, there are some unreachable
// code need to remove. http://crbug.com/396184.
return true;
}
@NativeMethods
interface Natives {
long init(long browserContextPointer);
void destroy(long nativeAwContents);
boolean hasRequiredHardwareExtensions();
void setAwDrawSWFunctionTable(long functionTablePointer);
void setAwDrawGLFunctionTable(long functionTablePointer);
int getNativeInstanceCount();
void setShouldDownloadFavicons();
void updateDefaultLocale(String locale, String localeList);
String getSafeBrowsingLocaleForTesting();
AwContents fromWebContents(WebContents webContents);
void setJavaPeers(
long nativeAwContents,
AwContents awContents,
AwWebContentsDelegate webViewWebContentsDelegate,
AwContentsClientBridge contentsClientBridge,
AwContentsIoThreadClient ioThreadClient,
InterceptNavigationDelegate navigationInterceptionDelegate);
void initializeAndroidAutofill(long nativeAwContents);
WebContents getWebContents(long nativeAwContents);
AwBrowserContext getBrowserContext(long nativeAwContents);
void setCompositorFrameConsumer(long nativeAwContents, long nativeCompositorFrameConsumer);
void documentHasImages(long nativeAwContents, Message message);
void generateMHTML(long nativeAwContents, String path, Callback<String> callback);
void addVisitedLinks(long nativeAwContents, String[] visitedLinks);
void zoomBy(long nativeAwContents, float delta);
void onComputeScroll(long nativeAwContents, long currentAnimationTimeMillis);
boolean onDraw(
long nativeAwContents,
Canvas canvas,
boolean isHardwareAccelerated,
int scrollX,
int scrollY,
int visibleLeft,
int visibleTop,
int visibleRight,
int visibleBottom,
boolean forceAuxiliaryBitmapRendering);
float getVelocityInPixelsPerSecond(long nativeAwContents);
boolean needToDrawBackgroundColor(long nativeAwContents);
void findAllAsync(long nativeAwContents, String searchString);
void findNext(long nativeAwContents, boolean forward);
void clearMatches(long nativeAwContents);
void clearCache(long nativeAwContents, boolean includeDiskFiles);
byte[] getCertificate(long nativeAwContents);
// Coordinates are in physical pixels when --use-zoom-for-dsf is enabled.
// Otherwise, coordinates are in desity independent pixels.
void requestNewHitTestDataAt(long nativeAwContents, float x, float y, float touchMajor);
void updateLastHitTestData(long nativeAwContents);
void onSizeChanged(long nativeAwContents, int w, int h, int ow, int oh);
void scrollTo(long nativeAwContents, int x, int y);
void restoreScrollAfterTransition(long nativeAwContents, int x, int y);
void smoothScroll(long nativeAwContents, int targetX, int targetY, long durationMs);
void setViewVisibility(long nativeAwContents, boolean visible);
void setWindowVisibility(long nativeAwContents, boolean visible);
void setIsPaused(long nativeAwContents, boolean paused);
void onAttachedToWindow(long nativeAwContents, int w, int h);
void onDetachedFromWindow(long nativeAwContents);
boolean isVisible(long nativeAwContents);
boolean isDisplayingInterstitialForTesting(long nativeAwContents);
void setDipScale(long nativeAwContents, float dipScale);
String getScheme(long nativeAwContents);
void updateScreenCoverage(int globalPercentage, String[] schemes, int[] schemePercentages);
void onInputEvent(long nativeAwContents);
// Returns null if save state fails.
byte[] getOpaqueState(long nativeAwContents);
// Returns false if restore state fails.
boolean restoreFromOpaqueState(long nativeAwContents, byte[] state);
long releasePopupAwContents(long nativeAwContents);
void focusFirstNode(long nativeAwContents);
void setBackgroundColor(long nativeAwContents, int color);
long capturePicture(long nativeAwContents, int width, int height);
void enableOnNewPicture(long nativeAwContents, boolean enabled);
void insertVisualStateCallback(
long nativeAwContents, long requestId, VisualStateCallback callback);
void clearView(long nativeAwContents);
void setExtraHeadersForUrl(long nativeAwContents, String url, String extraHeaders);
void invokeGeolocationCallback(
long nativeAwContents, boolean value, String requestingFrame);
int getEffectivePriority(long nativeAwContents);
void setJsOnlineProperty(long nativeAwContents, boolean networkUp);
void trimMemory(long nativeAwContents, int level, boolean visible);
void createPdfExporter(long nativeAwContents, AwPdfExporter awPdfExporter);
void preauthorizePermission(long nativeAwContents, String origin, long resources);
void grantFileSchemeAccesstoChildProcess(long nativeAwContents);
void resumeLoadingCreatedPopupWebContents(long nativeAwContents);
AwRenderProcess getRenderProcess(long nativeAwContents);
int addDocumentStartJavaScript(
long nativeAwContents, String script, String[] allowedOriginRules);
void removeDocumentStartJavaScript(long nativeAwContents, int scriptId);
String addWebMessageListener(
long nativeAwContents,
WebMessageListenerHolder listener,
String jsObjectName,
String[] allowedOrigins);
void removeWebMessageListener(long nativeAwContents, String jsObjectName);
@JniType("std::vector")
WebMessageListenerInfo[] getWebMessageListenerInfos(long nativeAwContents);
@JniType("std::vector")
StartupJavascriptInfo[] getDocumentStartupJavascripts(long nativeAwContents);
void onConfigurationChanged(long nativeAwContents);
void flushBackForwardCache(long nativeAwContents, int reason);
void cancelAllPrerendering(long nativeAwContents);
}
}