Commit 0c79411e authored by Changwan Ryu's avatar Changwan Ryu Committed by Chromium LUCI CQ

Integrate DisplayCutoutController for WebView

Integrate the core logic into AwContents for Android P and above.

Note that we are now using DisplayMode to check the fullscreen status.

Adding basic instrumentation tests require some plumbing into test
activity and its rule since we need to hide action bar *before* setting
the content view.

Bug: 1094366
Change-Id: I7296038f894861f68c57c025caa6a70ea9b69ebc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2299461Reviewed-by: default avatarBo <boliu@chromium.org>
Commit-Queue: Changwan Ryu <changwan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#832119}
parent 1018f446
......@@ -496,7 +496,9 @@ public class AwContents implements SmartClipProvider {
private ContentCaptureConsumer mContentCaptureConsumer;
private AwDisplayCutoutController mDisplayCutoutController;
private final AwDisplayModeController mDisplayModeController;
private final Rect mCachedSafeAreaRect = new Rect();
private static class WebContentsInternalsHolder implements WebContents.InternalsHolder {
private final WeakReference<AwContents> mAwContentsRef;
......@@ -960,6 +962,25 @@ public class AwContents implements SmartClipProvider {
return windowAndroid.getDisplay().getDisplayHeight();
}
}, containerView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& AwFeatureList.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();
......@@ -1205,6 +1226,9 @@ public class AwContents implements SmartClipProvider {
mContainerView.requestLayout();
if (mAutofillProvider != null) mAutofillProvider.onContainerViewChanged(mContainerView);
mDisplayModeController.setCurrentContainerView(mContainerView);
if (mDisplayCutoutController != null) {
mDisplayCutoutController.setCurrentContainerView(mContainerView);
}
}
// This class destroys the WindowAndroid when after it is gc-ed.
......@@ -3004,6 +3028,8 @@ public class AwContents implements SmartClipProvider {
AwOnPreDrawListener listener = getOrCreateOnPreDrawListener(mContainerView);
listener.trackContents(this);
}
if (mDisplayCutoutController != null) mDisplayCutoutController.onAttachedToWindow();
}
private AwOnPreDrawListener getOrCreateOnPreDrawListener(ViewGroup viewGroup) {
......@@ -3085,6 +3111,7 @@ public class AwContents implements SmartClipProvider {
*/
public void onSizeChanged(int w, int h, int ow, int oh) {
mAwViewMethods.onSizeChanged(w, h, ow, oh);
if (mDisplayCutoutController != null) mDisplayCutoutController.onSizeChanged();
}
/**
......@@ -3378,6 +3405,11 @@ public class AwContents implements SmartClipProvider {
return AwContentsJni.get().getRenderProcess(mNativeAwContents, AwContents.this);
}
@VisibleForTesting
public AwDisplayCutoutController getDisplayCutoutController() {
return mDisplayCutoutController;
}
public int getDisplayMode() {
return mDisplayModeController.getDisplayMode();
}
......
......@@ -4,9 +4,7 @@
package org.chromium.android_webview;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Build;
import android.view.DisplayCutout;
......@@ -36,10 +34,6 @@ public class AwDisplayCutoutController {
public interface Delegate {
/** @return The DIP scale. */
float getDipScale();
/** @return The display width. */
int getDisplayWidth();
/** @return The display height. */
int getDisplayHeight();
/**
* Set display cutout safe area such that webpage can read safe-area-insets CSS properties.
* Note that this can be called with the same parameter repeatedly, and the embedder needs
......@@ -109,14 +103,6 @@ public class AwDisplayCutoutController {
private Delegate mDelegate;
private View mContainerView;
// Reuse these structures to minimize memory impact.
private static int[] sCachedLocationOnScreen = {0, 0};
private Insets mCachedViewInsets = new Insets();
private Rect mCachedViewRect = new Rect();
private Rect mCachedWindowRect = new Rect();
private Rect mCachedDisplayRect = new Rect();
private Matrix mCachedMatrix = new Matrix();
/**
* Constructor for AwDisplayCutoutController.
*
......@@ -204,78 +190,18 @@ public class AwDisplayCutoutController {
*/
@VisibleForTesting
public void onApplyWindowInsetsInternal(final Insets displayCutoutInsets) {
// Copy such that we can log the original value.
mCachedViewInsets.set(displayCutoutInsets);
getViewRectOnScreen(mContainerView, mCachedViewRect);
getViewRectOnScreen(mContainerView.getRootView(), mCachedWindowRect);
// Get display coordinates.
int displayWidth = mDelegate.getDisplayWidth();
int displayHeight = mDelegate.getDisplayHeight();
mCachedDisplayRect.set(0, 0, displayWidth, displayHeight);
float dipScale = mDelegate.getDipScale();
if (!mCachedViewRect.equals(mCachedDisplayRect)) {
// We apply window insets only when webview is occupying the entire window and display.
// Checking the window rect is more complicated and therefore not doing it for now, but
// there can still be cases where the window is a bit off.
if (DEBUG) {
Log.i(TAG, "WebView is not occupying the entire screen, so no insets applied.");
}
mCachedViewInsets.set(0, 0, 0, 0);
} else if (!mCachedViewRect.equals(mCachedWindowRect)) {
if (DEBUG) {
Log.i(TAG, "WebView is not occupying the entire window, so no insets applied.");
}
mCachedViewInsets.set(0, 0, 0, 0);
} else if (hasTransform()) {
if (DEBUG) {
Log.i(TAG, "WebView is rotated or scaled, so no insets applied.");
}
mCachedViewInsets.set(0, 0, 0, 0);
} else {
// We only apply this logic when webview is occupying the entire screen.
adjustInsetsForScale(mCachedViewInsets, dipScale);
}
// We only apply this logic when webview is occupying the entire screen.
adjustInsetsForScale(displayCutoutInsets, dipScale);
if (DEBUG) {
Log.i(TAG,
"onApplyWindowInsetsInternal. displayCutoutInsets: " + displayCutoutInsets
+ ", view rect: " + mCachedViewRect + ", display rect: "
+ mCachedDisplayRect + ", window rect: " + mCachedWindowRect
+ ", dip scale: " + dipScale + ", viewInsets: " + mCachedViewInsets);
}
mDelegate.setDisplayCutoutSafeArea(mCachedViewInsets);
}
private static void getViewRectOnScreen(View view, Rect rect) {
if (view == null) {
rect.set(0, 0, 0, 0);
return;
}
view.getLocationOnScreen(sCachedLocationOnScreen);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
rect.set(sCachedLocationOnScreen[0], sCachedLocationOnScreen[1],
sCachedLocationOnScreen[0] + width, sCachedLocationOnScreen[1] + height);
}
@SuppressLint("NewApi") // need this exception since we will try using Q API in P
private boolean hasTransform() {
mCachedMatrix.reset(); // set to identity
// Check if a view coordinates transforms to screen coordinates that is not an identity
// matrix, which means that view is rotated or scaled in regards to the screen.
// This API got hidden from L, and readded in API 29 (Q). It seems that we can call this
// on P most of the time, but adding try-catch just in case.
try {
mContainerView.transformMatrixToGlobal(mCachedMatrix);
} catch (Throwable e) {
return true;
"onApplyWindowInsetsInternal. insets: " + displayCutoutInsets
+ ", dip scale: " + dipScale);
}
return !mCachedMatrix.isIdentity();
// Note that internally we apply this logic only when the display is in fullscreen mode.
// See AwDisplayModeController for more details on how we check the fullscreen mode.
mDelegate.setDisplayCutoutSafeArea(displayCutoutInsets);
}
private void onUpdateWindowInsets() {
......
......@@ -7,6 +7,7 @@ package org.chromium.android_webview.test;
import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
import android.content.Context;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.util.AndroidRuntimeException;
......@@ -120,9 +121,22 @@ public class AwActivityTestRule extends ActivityTestRule<AwTestRunnerActivity> {
TestThreadUtils.runOnUiThreadBlocking(() -> { mAwContentsDestroyedInTearDown.clear(); });
}
public boolean needsHideActionBar() {
return false;
}
private Intent getLaunchIntent() {
if (needsHideActionBar()) {
Intent intent = getActivityIntent();
intent.putExtra(AwTestRunnerActivity.FLAG_HIDE_ACTION_BAR, true);
return intent;
}
return null;
}
public AwTestRunnerActivity launchActivity() {
if (getActivity() == null) {
return launchActivity(null);
return launchActivity(getLaunchIntent());
}
return getActivity();
}
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test;
import android.app.Activity;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwDisplayCutoutController.Insets;
import org.chromium.android_webview.common.AwFeatures;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.test.util.TestWebServer;
/**
* Tests for DisplayCutout.
*/
@RunWith(AwJUnit4ClassRunner.class)
@MinAndroidSdkLevel(Build.VERSION_CODES.P)
@CommandLineFlags.Add({"enable-features=" + AwFeatures.WEBVIEW_DISPLAY_CUTOUT})
public class AwDisplayCutoutTest {
private static final String TEST_HTML = "<html><head><style>\n"
+ "body {\n"
+ " margin: 0;\n"
+ " padding: 0pt 0pt 0pt 0pt;\n"
+ "}\n"
+ "div {\n"
+ " margin: 0;\n"
+ " padding: env(safe-area-inset-top) "
+ " env(safe-area-inset-right)"
+ " env(safe-area-inset-bottom)"
+ " env(safe-area-inset-left);\n"
+ "}\n"
+ "</style></head><body>\n"
+ "<div id='text'>"
+ "On notched phones, there should be enough padding on the top"
+ " to not have this text appear under the statusbar/notch.\n"
+ "</div>\n"
+ "</body></html>";
@Rule
public AwActivityTestRule mActivityTestRule = new AwActivityTestRule() {
@Override
public boolean needsHideActionBar() {
// If action bar is showing, WebView cannot be fully occupying the screen.
return true;
}
};
private TestWebServer mWebServer;
private TestAwContentsClient mContentsClient;
private AwTestContainerView mContainerView;
private AwContents mAwContents;
@Before
public void setUp() throws Exception {
mWebServer = TestWebServer.start();
mContentsClient = new TestAwContentsClient();
mContainerView = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient);
mAwContents = mContainerView.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
// In pre-R, we need to explicitly set this to draw under notch.
TestThreadUtils.runOnUiThreadBlocking(() -> {
Activity activity = mActivityTestRule.getActivity();
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
});
}
@After
public void tearDown() {
mWebServer.shutdown();
}
private void setFullscreen(boolean fullscreen) {
TestThreadUtils.runOnUiThreadBlocking(() -> {
Activity activity = mActivityTestRule.getActivity();
View decor = activity.getWindow().getDecorView();
int systemUiVisibility = decor.getSystemUiVisibility();
int flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
if (fullscreen) {
activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
systemUiVisibility |= flags;
} else {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
systemUiVisibility &= flags;
}
decor.setSystemUiVisibility(systemUiVisibility);
});
}
@Test
@SmallTest
public void testNoSafeAreaSet() throws Throwable {
setFullscreen(true);
mActivityTestRule.loadHtmlSync(
mAwContents, mContentsClient.getOnPageFinishedHelper(), TEST_HTML);
// Reset safe area just in case we have a notch.
Insets insets = new Insets(0, 0, 0, 0);
TestThreadUtils.runOnUiThreadBlocking(() -> {
mAwContents.getDisplayCutoutController().onApplyWindowInsetsInternal(insets);
});
final String code = "window.getComputedStyle(document.getElementById('text'))"
+ ".getPropertyValue('padding-top')";
Assert.assertEquals("\"0px\"",
mActivityTestRule.executeJavaScriptAndWaitForResult(
mAwContents, mContentsClient, code));
}
@Test
@SmallTest
public void testSafeAreaSet() throws Throwable {
setFullscreen(true);
mActivityTestRule.loadHtmlSync(
mAwContents, mContentsClient.getOnPageFinishedHelper(), TEST_HTML);
Insets insets = new Insets(0, 130, 0, 0);
TestThreadUtils.runOnUiThreadBlocking(() -> {
mAwContents.getDisplayCutoutController().onApplyWindowInsetsInternal(insets);
});
final String code = "window.getComputedStyle(document.getElementById('text'))"
+ ".getPropertyValue('padding-top')";
Assert.assertNotEquals("\"0px\"",
mActivityTestRule.executeJavaScriptAndWaitForResult(
mAwContents, mContentsClient, code));
}
}
\ No newline at end of file
......@@ -93,24 +93,8 @@ public class AwDisplayCutoutControllerTest {
// Set up default values.
setWindowInsets(new Rect(20, 40, 60, 80));
mDipScale = 2.0f;
mViewWidth = 300;
mViewHeight = 400;
mDisplayWidth = 300;
mDisplayHeight = 400;
mGlobalTransformMatrix = new Matrix(); // identity matrix
// Set up the view.
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
int[] loc = (int[]) (invocation.getArguments()[0]);
loc[0] = mLocationOnScreen[0];
loc[1] = mLocationOnScreen[1];
return null;
}
})
.when(mView)
.getLocationOnScreen(any(int[].class));
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
......@@ -130,39 +114,8 @@ public class AwDisplayCutoutControllerTest {
.when(mView)
.requestApplyInsets();
when(mView.getMeasuredWidth()).thenReturn(mViewWidth);
when(mView.getMeasuredHeight()).thenReturn(mViewHeight);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Matrix matrix = (Matrix) (invocation.getArguments()[0]);
matrix.set(mGlobalTransformMatrix);
return null;
}
})
.when(mView)
.transformMatrixToGlobal(any(Matrix.class));
// Set up the root view.
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
int[] loc = (int[]) (invocation.getArguments()[0]);
loc[0] = mLocationOnScreen[0];
loc[1] = mLocationOnScreen[1];
return null;
}
})
.when(mRootView)
.getLocationOnScreen(any(int[].class));
when(mRootView.getMeasuredWidth()).thenReturn(mViewWidth);
when(mRootView.getMeasuredHeight()).thenReturn(mViewHeight);
when(mView.getRootView()).thenReturn(mRootView);
// Set up the delegate.
when(mDelegate.getDipScale()).thenReturn(mDipScale);
when(mDelegate.getDisplayWidth()).thenReturn(mDisplayWidth);
when(mDelegate.getDisplayHeight()).thenReturn(mDisplayHeight);
mInOrder = inOrder(mDelegate, mView, mAnotherView);
......@@ -195,70 +148,12 @@ public class AwDisplayCutoutControllerTest {
public void testOnApplyWindowInsets() {
mController.onApplyWindowInsets(mWindowInsets);
mInOrder.verify(mView).getLocationOnScreen(any(int[].class));
mInOrder.verify(mView).getMeasuredWidth();
mInOrder.verify(mView).getMeasuredHeight();
mInOrder.verify(mDelegate).getDisplayWidth();
mInOrder.verify(mDelegate).getDisplayHeight();
mInOrder.verify(mDelegate).getDipScale();
// Note that DIP of 2.0 is applied, so the values are halved.
mInOrder.verify(mDelegate).setDisplayCutoutSafeArea(eq(new Insets(10, 20, 30, 40)));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testOnApplyWindowInsets_NotOccupyingFullDisplay() {
// View is not occupying the entire display, so no insets applied.
when(mView.getMeasuredHeight()).thenReturn(mDisplayHeight / 2);
mController.onApplyWindowInsets(mWindowInsets);
mInOrder.verify(mView).getLocationOnScreen(any(int[].class));
mInOrder.verify(mView).getMeasuredWidth();
mInOrder.verify(mView).getMeasuredHeight();
mInOrder.verify(mDelegate).getDisplayWidth();
mInOrder.verify(mDelegate).getDisplayHeight();
mInOrder.verify(mDelegate).setDisplayCutoutSafeArea(eq(new Insets(0, 0, 0, 0)));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testOnApplyWindowInsets_NotOccupyingFullWindow() {
// View is not occupying the entire window, so no insets applied.
when(mRootView.getMeasuredHeight()).thenReturn(mViewHeight / 2);
mController.onApplyWindowInsets(mWindowInsets);
mInOrder.verify(mView).getLocationOnScreen(any(int[].class));
mInOrder.verify(mView).getMeasuredWidth();
mInOrder.verify(mView).getMeasuredHeight();
mInOrder.verify(mDelegate).getDisplayWidth();
mInOrder.verify(mDelegate).getDisplayHeight();
mInOrder.verify(mDelegate).setDisplayCutoutSafeArea(eq(new Insets(0, 0, 0, 0)));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testOnApplyWindowInsets_ParentLayoutRotated() {
mGlobalTransformMatrix.postRotate(30.0f);
mController.onApplyWindowInsets(mWindowInsets);
mInOrder.verify(mView).getLocationOnScreen(any(int[].class));
mInOrder.verify(mView).getMeasuredWidth();
mInOrder.verify(mView).getMeasuredHeight();
mInOrder.verify(mDelegate).getDisplayWidth();
mInOrder.verify(mDelegate).getDisplayHeight();
mInOrder.verify(mDelegate).setDisplayCutoutSafeArea(eq(new Insets(0, 0, 0, 0)));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
......@@ -267,11 +162,6 @@ public class AwDisplayCutoutControllerTest {
mInOrder.verify(mView).requestApplyInsets();
mInOrder.verify(mView).getLocationOnScreen(any(int[].class));
mInOrder.verify(mView).getMeasuredWidth();
mInOrder.verify(mView).getMeasuredHeight();
mInOrder.verify(mDelegate).getDisplayWidth();
mInOrder.verify(mDelegate).getDisplayHeight();
mInOrder.verify(mDelegate).getDipScale();
// Note that DIP of 2.0 is applied, so the values are halved.
......@@ -286,11 +176,6 @@ public class AwDisplayCutoutControllerTest {
mInOrder.verify(mView).requestApplyInsets();
mInOrder.verify(mView).getLocationOnScreen(any(int[].class));
mInOrder.verify(mView).getMeasuredWidth();
mInOrder.verify(mView).getMeasuredHeight();
mInOrder.verify(mDelegate).getDisplayWidth();
mInOrder.verify(mDelegate).getDisplayHeight();
mInOrder.verify(mDelegate).getDipScale();
// Note that DIP of 2.0 is applied, so the values are halved.
......
......@@ -251,6 +251,7 @@ instrumentation_test_apk("webview_instrumentation_test_apk") {
"../javatests/src/org/chromium/android_webview/test/AwContentsRenderTest.java",
"../javatests/src/org/chromium/android_webview/test/AwContentsStaticsTest.java",
"../javatests/src/org/chromium/android_webview/test/AwContentsTest.java",
"../javatests/src/org/chromium/android_webview/test/AwDisplayCutoutTest.java",
"../javatests/src/org/chromium/android_webview/test/AwFormDatabaseTest.java",
"../javatests/src/org/chromium/android_webview/test/AwImeTest.java",
"../javatests/src/org/chromium/android_webview/test/AwJavaBridgeTest.java",
......
......@@ -19,6 +19,7 @@ import org.chromium.base.StrictModeContext;
* This is a lightweight activity for tests that only require WebView functionality.
*/
public class AwTestRunnerActivity extends Activity {
public static final String FLAG_HIDE_ACTION_BAR = "hide_action_bar";
private LinearLayout mLinearLayout;
private Intent mLastSentIntent;
......@@ -29,7 +30,7 @@ public class AwTestRunnerActivity extends Activity {
super.onCreate(savedInstanceState);
AwShellResourceProvider.registerResources(this);
try (StrictModeContext ctx = StrictModeContext.allowDiskReads()) {
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
AwBrowserProcess.loadLibrary(null);
}
......@@ -39,9 +40,24 @@ public class AwTestRunnerActivity extends Activity {
mLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
hideActionBarIfNecessary();
setContentView(mLinearLayout);
}
private void hideActionBarIfNecessary() {
Intent intent = getIntent();
if (intent == null) return;
Bundle extras = intent.getExtras();
if (extras == null) return;
Boolean hideActionBar = extras.getBoolean(FLAG_HIDE_ACTION_BAR);
if (hideActionBar == null || !hideActionBar) return;
// This should be called before setContentView in onCreate() to take an
// effect.
getActionBar().hide();
}
public int getRootLayoutWidth() {
return mLinearLayout.getWidth();
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment