/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package android.server.wm; import static android.server.wm.CtsWindowInfoUtils.tapOnWindow; import static android.server.wm.CtsWindowInfoUtils.tapOnWindowCenter; import static android.server.wm.CtsWindowInfoUtils.waitForWindowFocus; import static android.server.wm.CtsWindowInfoUtils.waitForWindowInfo; import static android.server.wm.CtsWindowInfoUtils.waitForWindowInfos; import static android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop; import static android.server.wm.CtsWindowInfoUtils.waitForWindowVisible; import static android.server.wm.MockImeHelper.createManagedMockImeSession; import static android.view.SurfaceControlViewHost.SurfacePackage; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import android.app.Activity; import android.app.ActivityManager; import android.app.Instrumentation; import android.app.UiAutomation; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.FeatureInfo; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Region; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresDevice; import android.server.wm.scvh.Components; import android.server.wm.shared.ICrossProcessSurfaceControlViewHostTestService; import android.util.ArrayMap; import android.view.Gravity; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.PopupWindow; import android.window.WindowInfosListenerForTest.WindowInfo; import androidx.test.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import com.android.compatibility.common.util.CtsTouchUtils; import com.android.cts.mockime.ImeEventStream; import com.android.cts.mockime.MockImeSession; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; /** * Ensure end-to-end functionality of SurfaceControlViewHost. * * Build/Install/Run: * atest CtsWindowManagerDeviceTestCases:SurfaceControlViewHostTests */ @Presubmit public class SurfaceControlViewHostTests extends ActivityManagerTestBase implements SurfaceHolder.Callback { public static class TestActivity extends Activity {} private final ActivityTestRule mActivityRule = new ActivityTestRule<>( TestActivity.class); private Instrumentation mInstrumentation; private CtsTouchUtils mCtsTouchUtils; private Activity mActivity; private SurfaceView mSurfaceView; private ViewGroup mViewParent; private SurfaceControlViewHost mVr; private View mEmbeddedView; private WindowManager.LayoutParams mEmbeddedLayoutParams; private volatile boolean mClicked = false; private volatile boolean mPopupClicked = false; private volatile PopupWindow mPopupWindow; private SurfaceControlViewHost.SurfacePackage mRemoteSurfacePackage; private final Map> mConnections = new ArrayMap<>(); private ICrossProcessSurfaceControlViewHostTestService mTestService = null; private static final long TIMEOUT_MS = 3000L; /* * Configurable state to control how the surfaceCreated callback * will initialize the embedded view hierarchy. */ int mEmbeddedViewWidth = 100; int mEmbeddedViewHeight = 100; private static final int DEFAULT_SURFACE_VIEW_WIDTH = 100; private static final int DEFAULT_SURFACE_VIEW_HEIGHT = 100; MockImeSession mImeSession; Consumer mSurfaceViewMotionConsumer = null; private CountDownLatch mSvCreatedLatch; class MotionConsumingSurfaceView extends SurfaceView { MotionConsumingSurfaceView(Context c) { super(c); } @Override public boolean onTouchEvent(MotionEvent ev) { if (mSurfaceViewMotionConsumer == null) { return false; } else { mSurfaceViewMotionConsumer.accept(ev); return true; } } } boolean mHostGotEvent = false; @Before public void setUp() throws Exception { super.setUp(); mClicked = false; mEmbeddedLayoutParams = null; mPopupWindow = null; mRemoteSurfacePackage = null; if (supportsInstallableIme()) { mImeSession = createManagedMockImeSession(this); } mInstrumentation = InstrumentationRegistry.getInstrumentation(); mCtsTouchUtils = new CtsTouchUtils(mInstrumentation.getTargetContext()); mActivity = mActivityRule.launchActivity(null); mInstrumentation.waitForIdleSync(); // Wait for device animation that shows above the activity to leave. waitForWindowOnTop(mActivity.getWindow()); // This is necessary to call waitForWindowInfos mInstrumentation.getUiAutomation().adoptShellPermissionIdentity( android.Manifest.permission.ACCESS_SURFACE_FLINGER); mSvCreatedLatch = new CountDownLatch(1); } @After public void tearDown() throws Throwable { for (FutureConnection connection : mConnections.values()) { mInstrumentation.getContext().unbindService(connection); } mConnections.clear(); mInstrumentation.getUiAutomation().dropShellPermissionIdentity(); } private void addSurfaceView(int width, int height) throws Throwable { addSurfaceView(width, height, true); } private void addSurfaceView(int width, int height, boolean onTop) throws Throwable { addSurfaceView(width, height, onTop, 0 /* leftMargin */, 0 /* topMargin */); } private void addSurfaceView(int width, int height, boolean onTop, int leftMargin, int topMargin) throws Throwable { mActivityRule.runOnUiThread(() -> { final FrameLayout content = new FrameLayout(mActivity); mSurfaceView = new MotionConsumingSurfaceView(mActivity); mSurfaceView.setBackgroundColor(Color.BLACK); mSurfaceView.setZOrderOnTop(onTop); final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( width, height, Gravity.LEFT | Gravity.TOP); lp.leftMargin = leftMargin; lp.topMargin = topMargin; content.addView(mSurfaceView, lp); mViewParent = content; mActivity.setContentView(content, new ViewGroup.LayoutParams(width + leftMargin, height + topMargin)); mSurfaceView.getHolder().addCallback(this); }); } private void addViewToSurfaceView(SurfaceView sv, View v, int width, int height) { mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), sv.getHostToken()); if (mEmbeddedLayoutParams == null) { mVr.setView(v, width, height); } else { mVr.setView(v, mEmbeddedLayoutParams); } sv.setChildSurfacePackage(mVr.getSurfacePackage()); assertEquals(v, mVr.getView()); } private void requestSurfaceViewFocus() throws Throwable { mActivityRule.runOnUiThread(() -> { mSurfaceView.setFocusableInTouchMode(true); mSurfaceView.requestFocusFromTouch(); }); } private void assertWindowFocused(final View view, boolean hasWindowFocus) { if (!waitForWindowFocus(view, hasWindowFocus)) { fail(); } } private void waitUntilViewDrawn(View view) throws Throwable { // We use frameCommitCallback because we need to ensure HWUI // has actually queued the frame. final CountDownLatch latch = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { view.getViewTreeObserver().registerFrameCommitCallback( latch::countDown); view.invalidate(); }); assertTrue(latch.await(1, TimeUnit.SECONDS)); } private void waitUntilEmbeddedViewDrawn() throws Throwable { waitUntilViewDrawn(mEmbeddedView); } private String getTouchableRegionFromDump() { final String output = runCommandAndPrintOutput("dumpsys window windows"); boolean foundWindow = false; for (String line : output.split("\\n")) { if (line.contains("SurfaceControlViewHostTests$TestActivity")) { foundWindow = true; } if (foundWindow && line.contains("touchable region")) { return line; } } return null; } private boolean waitForTouchableRegionChanged(String originalTouchableRegion) { int retries = 0; while (retries < 50) { if (getTouchableRegionFromDump() != originalTouchableRegion) { return true; } try { Thread.sleep(100); } catch (Exception e) { } } return false; } public static boolean waitForViewFocus(final View view, boolean hasViewFocus) { final CountDownLatch latch = new CountDownLatch(1); view.getHandler().post(() -> { if (view.hasFocus() == hasViewFocus) { latch.countDown(); return; } view.setOnFocusChangeListener((v, hasFocus) -> { if (hasViewFocus == hasFocus) { view.setOnFocusChangeListener(null); latch.countDown(); } }); }); try { if (!latch.await(3, TimeUnit.SECONDS)) { return false; } } catch (InterruptedException e) { return false; } return true; } @Override public void surfaceCreated(SurfaceHolder holder) { if (mTestService == null) { if (mEmbeddedView != null) { addViewToSurfaceView(mSurfaceView, mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight); } } else if (mRemoteSurfacePackage == null) { try { mRemoteSurfacePackage = mTestService.getSurfacePackage(mSurfaceView.getHostToken()); } catch (Exception e) { } mSurfaceView.setChildSurfacePackage(mRemoteSurfacePackage); } else { mSurfaceView.setChildSurfacePackage(mRemoteSurfacePackage); } mSvCreatedLatch.countDown(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Test public void testEmbeddedViewReceivesInput() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mClicked); } @Test public void testEmbeddedViewReceivesRawInputCoordinatesInDisplaySpace() throws Throwable { final UiAutomation uiAutomation = mInstrumentation.getUiAutomation(); final int viewX = DEFAULT_SURFACE_VIEW_WIDTH / 2; final int viewY = DEFAULT_SURFACE_VIEW_HEIGHT / 2; // Verify the input coordinates received by the embedded view in three different locations. for (int i = 0; i < 3; i++) { final List events = new ArrayList<>(); mEmbeddedView = new View(mActivity); mEmbeddedView.setOnTouchListener((v, e) -> events.add(MotionEvent.obtain(e))); // Add a margin to the SurfaceView to offset the embedded view's location on the screen. final int leftMargin = i * 20; final int topMargin = i * 10; addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, true /*onTop*/, leftMargin, topMargin); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); final int[] surfaceLocation = new int[2]; mSurfaceView.getLocationOnScreen(surfaceLocation); final int displayX = surfaceLocation[0] + viewX; final int displayY = surfaceLocation[1] + viewY; final long downTime = SystemClock.uptimeMillis(); mCtsTouchUtils.injectDownEvent(uiAutomation, downTime, displayX, displayY, null /*eventInjectionListener*/); mCtsTouchUtils.injectUpEvent(uiAutomation, downTime, true /*useCurrentEventTime*/, displayX, displayY, null /*eventInjectionListener*/); assertEquals("Expected to capture all injected events.", 2, events.size()); final float epsilon = 0.001f; events.forEach(e -> { assertEquals("Expected to get the x coordinate in View space.", viewX, e.getX(), epsilon); assertEquals("Expected to get the y coordinate in View space.", viewY, e.getY(), epsilon); assertEquals("Expected to get raw x coordinate in Display space.", displayX, e.getRawX(), epsilon); assertEquals("Expected to get raw y coordinate in Display space.", displayY, e.getRawY(), epsilon); }); } } private static int getGlEsVersion(Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo(); if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) { return getMajorVersion(configInfo.reqGlEsVersion); } else { return 1; // Lack of property means OpenGL ES version 1 } } /** @see FeatureInfo#getGlEsVersion() */ private static int getMajorVersion(int glEsVersion) { return ((glEsVersion & 0xffff0000) >> 16); } @Test @RequiresDevice public void testEmbeddedViewIsHardwareAccelerated() throws Throwable { // Hardware accel may not be supported on devices without GLES 2.0 if (getGlEsVersion(mActivity) < 2) { return; } mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); // If we don't support hardware acceleration on the main activity the embedded // view also won't be. if (!mSurfaceView.isHardwareAccelerated()) { return; } assertTrue(mEmbeddedView.isHardwareAccelerated()); } @Test public void testEmbeddedViewResizes() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); final int bigEdgeLength = mEmbeddedViewWidth * 3; // We make the SurfaceView more than twice as big as the embedded view // so that a touch in the middle of the SurfaceView won't land // on the embedded view. addSurfaceView(bigEdgeLength, bigEdgeLength); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertFalse(mClicked); mActivityRule.runOnUiThread(() -> { mVr.relayout(bigEdgeLength, bigEdgeLength); }); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // But after the click should hit. mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mClicked); } @Test public void testEmbeddedViewReleases() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mClicked); mActivityRule.runOnUiThread(() -> { mVr.release(); }); mInstrumentation.waitForIdleSync(); mClicked = false; mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertFalse(mClicked); } @Test public void testDisableInputTouch() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); mEmbeddedLayoutParams = new WindowManager.LayoutParams(mEmbeddedViewWidth, mEmbeddedViewHeight, WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.OPAQUE); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { mEmbeddedLayoutParams.flags |= FLAG_NOT_TOUCHABLE; mVr.relayout(mEmbeddedLayoutParams); }); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertFalse(mClicked); mActivityRule.runOnUiThread(() -> { mEmbeddedLayoutParams.flags &= ~FLAG_NOT_TOUCHABLE; mVr.relayout(mEmbeddedLayoutParams); }); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mClicked); } @Test public void testFocusable() throws Throwable { mEmbeddedView = new Button(mActivity); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // When surface view is focused, it should transfer focus to the embedded view. requestSurfaceViewFocus(); assertWindowFocused(mEmbeddedView, true); // assert host does not have focus assertWindowFocused(mSurfaceView, false); // When surface view is no longer focused, it should transfer focus back to the host window. mActivityRule.runOnUiThread(() -> mSurfaceView.setFocusable(false)); assertWindowFocused(mEmbeddedView, false); // assert host has focus assertWindowFocused(mSurfaceView, true); } @Test public void testFocusWithTouch() throws Throwable { mEmbeddedView = new Button(mActivity); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // Tap where the embedded window is placed to ensure focus is given via touch assertTrue("Failed to tap on embedded", tapOnWindowCenter(mInstrumentation, () -> mEmbeddedView.getWindowToken())); assertWindowFocused(mEmbeddedView, true); // assert host does not have focus assertWindowFocused(mSurfaceView, false); // Tap where the host window is placed to ensure focus is given back to host when touched assertTrue("Failed to tap on host", tapOnWindowCenter(mInstrumentation, () -> mViewParent.getWindowToken())); assertWindowFocused(mEmbeddedView, false); // assert host does not have focus assertWindowFocused(mViewParent, true); } @Test public void testChildWindowFocusable() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setBackgroundColor(Color.BLUE); View embeddedViewChild = new Button(mActivity); embeddedViewChild.setBackgroundColor(Color.RED); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); mActivityRule.runOnUiThread(() -> { final WindowManager.LayoutParams embeddedViewChildParams = new WindowManager.LayoutParams(25, 25, WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.OPAQUE); embeddedViewChildParams.token = mEmbeddedView.getWindowToken(); WindowManager wm = mActivity.getSystemService(WindowManager.class); wm.addView(embeddedViewChild, embeddedViewChildParams); }); waitUntilViewDrawn(embeddedViewChild); assertTrue("Failed to tap on embedded child", tapOnWindowCenter(mInstrumentation, () -> embeddedViewChild.getWindowToken())); // When tapping on the child embedded window, it should gain focus. assertWindowFocused(embeddedViewChild, true); // assert parent embedded window does not have focus. assertWindowFocused(mEmbeddedView, false); // assert host does not have focus assertWindowFocused(mSurfaceView, false); assertTrue("Failed to tap on embedded parent", tapOnWindow(mInstrumentation, () -> mEmbeddedView.getWindowToken(), null /* offset */)); // When tapping on the parent embedded window, it should gain focus. assertWindowFocused(mEmbeddedView, true); // assert child embedded window does not have focus. assertWindowFocused(embeddedViewChild, false); // assert host does not have focus assertWindowFocused(mSurfaceView, false); } @Test public void testFocusWithTouchCrossProcess() throws Throwable { mTestService = getService(); assertNotNull(mTestService); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mSvCreatedLatch.await(5, TimeUnit.SECONDS); // Tap where the embedded window is placed to ensure focus is given via touch assertTrue("Failed to tap on embedded", tapOnWindowCenter(mInstrumentation, () -> { try { return mTestService.getWindowToken(); } catch (RemoteException e) { return null; } })); assertTrue(mTestService.waitForFocus(true)); // assert host does not have focus assertWindowFocused(mSurfaceView, false); // Tap where the host window is placed to ensure focus is given back to host when touched assertTrue("Failed to tap on host", tapOnWindowCenter(mInstrumentation, () -> mViewParent.getWindowToken())); assertTrue(mTestService.waitForFocus(false)); // assert host does not have focus assertWindowFocused(mViewParent, true); } @Test public void testWindowResumes_FocusTransfersToEmbedded() throws Throwable { mEmbeddedView = new Button(mActivity); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // When surface view is focused, it should transfer focus to the embedded view. requestSurfaceViewFocus(); assertWindowFocused(mEmbeddedView, true); // assert host does not have focus assertWindowFocused(mSurfaceView, false); WindowManager wm = mActivity.getSystemService(WindowManager.class); View childView = new Button(mActivity); mActivityRule.runOnUiThread(() -> { final WindowManager.LayoutParams childWindowParams = new WindowManager.LayoutParams(25, 25, WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.OPAQUE); wm.addView(childView, childWindowParams); }); waitUntilViewDrawn(childView); assertWindowFocused(childView, true); // Neither host or embedded should be focus assertWindowFocused(mSurfaceView, false); assertWindowFocused(mEmbeddedView, false); mActivityRule.runOnUiThread(() -> wm.removeView(childView)); mInstrumentation.waitForIdleSync(); assertWindowFocused(mEmbeddedView, true); assertWindowFocused(mSurfaceView, false); } @Test public void testImeVisible() throws Throwable { assumeTrue(MSG_NO_MOCK_IME, supportsInstallableIme()); EditText editText = new EditText(mActivity); mEmbeddedView = editText; editText.setBackgroundColor(Color.BLUE); editText.setPrivateImeOptions("Hello reader! This is a random string"); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // When surface view is focused, it should transfer focus to the embedded view. requestSurfaceViewFocus(); assertWindowFocused(mEmbeddedView, true); // assert host does not have focus assertWindowFocused(mSurfaceView, false); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); final ImeEventStream stream = mImeSession.openEventStream(); expectEvent(stream, editorMatcher("onStartInputView", editText.getPrivateImeOptions()), TIMEOUT_MS); } @Test public void testNotFocusable() throws Throwable { mEmbeddedView = new Button(mActivity); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mEmbeddedLayoutParams = new WindowManager.LayoutParams(mEmbeddedViewWidth, mEmbeddedViewHeight, WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.OPAQUE); mActivityRule.runOnUiThread(() -> { mEmbeddedLayoutParams.flags |= FLAG_NOT_FOCUSABLE; mVr.relayout(mEmbeddedLayoutParams); }); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // When surface view is focused, nothing should happen since the embedded view is not // focusable. requestSurfaceViewFocus(); assertWindowFocused(mEmbeddedView, false); // assert host has focus assertWindowFocused(mSurfaceView, true); } @Test public void testFocusBeforeAddingEmbedded() throws Throwable { addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); // Request focus to the SV before adding the embedded. requestSurfaceViewFocus(); mSvCreatedLatch.await(); assertTrue("Failed to wait for sv to gain focus", waitForViewFocus(mSurfaceView, true)); mEmbeddedView = new Button(mActivity); mActivityRule.runOnUiThread(() -> { addViewToSurfaceView(mSurfaceView, mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight); }); waitForWindowVisible(mEmbeddedView); assertWindowFocused(mEmbeddedView, true); assertWindowFocused(mSurfaceView, false); } private static class SurfaceCreatedCallback implements SurfaceHolder.Callback { private final CountDownLatch mSurfaceCreated; SurfaceCreatedCallback(CountDownLatch latch) { mSurfaceCreated = latch; } @Override public void surfaceCreated(SurfaceHolder holder) { mSurfaceCreated.countDown(); } @Override public void surfaceDestroyed(SurfaceHolder holder) {} @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} } @Test public void testCanCopySurfacePackage() throws Throwable { // Create a surface view and wait for its surface to be created. CountDownLatch surfaceCreated = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { final FrameLayout content = new FrameLayout(mActivity); mSurfaceView = new SurfaceView(mActivity); mSurfaceView.setZOrderOnTop(true); content.addView(mSurfaceView, new FrameLayout.LayoutParams( DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, Gravity.LEFT | Gravity.TOP)); mActivity.setContentView(content, new ViewGroup.LayoutParams(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT)); mSurfaceView.getHolder().addCallback(new SurfaceCreatedCallback(surfaceCreated)); // Create an embedded view. mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), mSurfaceView.getHostToken()); mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> mClicked = true); mVr.setView(mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight); }); assertTrue("Failed to wait for SurfaceView created", surfaceCreated.await(5, TimeUnit.SECONDS)); // Make a copy of the SurfacePackage and release the original package. SurfacePackage surfacePackage = mVr.getSurfacePackage(); SurfacePackage copy = new SurfacePackage(surfacePackage); surfacePackage.release(); CountDownLatch surfacePackageReparented = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { mSurfaceView.setChildSurfacePackage(copy); SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.addTransactionCommittedListener(Runnable::run, surfacePackageReparented::countDown); mSurfaceView.getRootSurfaceControl().applyTransactionOnDraw(t); }); assertTrue("Failed to wait for surface package to get reparented", surfacePackageReparented.await(5, TimeUnit.SECONDS)); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // Check if SurfacePackage copy remains valid even though the original package has // been released. mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mClicked); } @Test public void testTransferSurfacePackage() throws Throwable { // Create a surface view and wait for its surface to be created. CountDownLatch surfaceCreated = new CountDownLatch(1); CountDownLatch surface2Created = new CountDownLatch(1); CountDownLatch viewDetached = new CountDownLatch(1); AtomicReference surfacePackageRef = new AtomicReference<>(null); AtomicReference surfacePackageCopyRef = new AtomicReference<>(null); AtomicReference secondSurfaceRef = new AtomicReference<>(null); mActivityRule.runOnUiThread(() -> { final FrameLayout content = new FrameLayout(mActivity); mSurfaceView = new SurfaceView(mActivity); mSurfaceView.setZOrderOnTop(true); content.addView(mSurfaceView, new FrameLayout.LayoutParams(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, Gravity.LEFT | Gravity.TOP)); mActivity.setContentView(content, new ViewGroup.LayoutParams(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT)); mSurfaceView.getHolder().addCallback(new SurfaceCreatedCallback(surfaceCreated)); // Create an embedded view. mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), mSurfaceView.getHostToken()); mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> mClicked = true); mVr.setView(mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight); SurfacePackage surfacePackage = mVr.getSurfacePackage(); surfacePackageRef.set(surfacePackage); surfacePackageCopyRef.set(new SurfacePackage(surfacePackage)); // Assign the surface package to the first surface mSurfaceView.setChildSurfacePackage(surfacePackage); // Create the second surface view to which we'll assign the surface package copy SurfaceView secondSurface = new SurfaceView(mActivity); secondSurfaceRef.set(secondSurface); mSurfaceView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { viewDetached.countDown(); } }); secondSurface.getHolder().addCallback(new SurfaceCreatedCallback(surface2Created)); }); surfaceCreated.await(); // Add the second surface view and assign it the surface package copy mActivityRule.runOnUiThread(() -> { ViewGroup content = (ViewGroup) mSurfaceView.getParent(); content.addView(secondSurfaceRef.get(), new FrameLayout.LayoutParams(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, Gravity.TOP | Gravity.LEFT)); secondSurfaceRef.get().setZOrderOnTop(true); surfacePackageRef.get().release(); secondSurfaceRef.get().setChildSurfacePackage(surfacePackageCopyRef.get()); content.removeView(mSurfaceView); }); // Wait for the first surface to be removed surface2Created.await(); viewDetached.await(); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // Check if SurfacePackage copy remains valid even though the original package has // been released and the original surface view removed. mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, secondSurfaceRef.get()); assertTrue(mClicked); } @Test public void testCanReplaceSurfacePackage() throws Throwable { // Create a surface view and wait for its surface to be created. { CountDownLatch surfaceCreated = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { final FrameLayout content = new FrameLayout(mActivity); mSurfaceView = new SurfaceView(mActivity); mSurfaceView.setZOrderOnTop(true); content.addView(mSurfaceView, new FrameLayout.LayoutParams( DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, Gravity.LEFT | Gravity.TOP)); mActivity.setContentView(content, new ViewGroup.LayoutParams( DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT)); mSurfaceView.getHolder().addCallback(new SurfaceCreatedCallback(surfaceCreated)); // Create an embedded view without click handling. mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), mSurfaceView.getHostToken()); mEmbeddedView = new Button(mActivity); mVr.setView(mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight); }); surfaceCreated.await(); mSurfaceView.setChildSurfacePackage(mVr.getSurfacePackage()); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); } { CountDownLatch hostReady = new CountDownLatch(1); // Create a second surface view and wait for its surface to be created. mActivityRule.runOnUiThread(() -> { // Create an embedded view. mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), mSurfaceView.getHostToken()); mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> mClicked = true); mVr.setView(mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight); hostReady.countDown(); }); hostReady.await(); mSurfaceView.setChildSurfacePackage(mVr.getSurfacePackage()); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); } // Check to see if the click went through - this only would happen if the surface package // was replaced mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mClicked); } class MotionRecordingSurfaceView extends SurfaceView { boolean mGotEvent = false; MotionRecordingSurfaceView(Context c) { super(c); } public boolean onTouchEvent(MotionEvent e) { super.onTouchEvent(e); synchronized (this) { mGotEvent = true; } return true; } boolean gotEvent() { synchronized (this) { return mGotEvent; } } void reset() { synchronized (this) { mGotEvent = false; } } } class TouchPunchingView extends View { public TouchPunchingView(Context context) { super(context); } void punchHoleInTouchableRegion() { getRootSurfaceControl().setTouchableRegion(new Region()); } } private void addMotionRecordingSurfaceView(int width, int height) throws Throwable { mActivityRule.runOnUiThread(() -> { final FrameLayout content = new FrameLayout(mActivity); mSurfaceView = new MotionRecordingSurfaceView(mActivity); mSurfaceView.setZOrderOnTop(true); content.addView(mSurfaceView, new FrameLayout.LayoutParams( width, height, Gravity.LEFT | Gravity.TOP)); mActivity.setContentView(content, new ViewGroup.LayoutParams(width, height)); mSurfaceView.getHolder().addCallback(this); }); } class ForwardingSurfaceView extends SurfaceView { SurfaceControlViewHost.SurfacePackage mPackage; ForwardingSurfaceView(Context c) { super(c); } @Override protected void onDetachedFromWindow() { mPackage.notifyDetachedFromWindow(); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mPackage.notifyConfigurationChanged(newConfig); } @Override public void setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage p) { super.setChildSurfacePackage(p); mPackage = p; } } class DetachRecordingView extends View { boolean mDetached = false; DetachRecordingView(Context c) { super(c); } @Override protected void onDetachedFromWindow() { mDetached = true; } } class ConfigRecordingView extends View { CountDownLatch mLatch; ConfigRecordingView(Context c, CountDownLatch latch) { super(c); mLatch = latch; } @Override protected void onConfigurationChanged(Configuration newConfig) { mLatch.countDown(); } } private void addForwardingSurfaceView(int width, int height) throws Throwable { mActivityRule.runOnUiThread(() -> { final FrameLayout content = new FrameLayout(mActivity); mSurfaceView = new ForwardingSurfaceView(mActivity); mSurfaceView.setZOrderOnTop(true); content.addView(mSurfaceView, new FrameLayout.LayoutParams( width, height, Gravity.LEFT | Gravity.TOP)); mViewParent = content; mActivity.setContentView(content, new ViewGroup.LayoutParams(width, height)); mSurfaceView.getHolder().addCallback(this); }); } @Test public void testEmbeddedViewCanSetTouchableRegion() throws Throwable { TouchPunchingView tpv; mEmbeddedView = tpv = new TouchPunchingView(mActivity); addMotionRecordingSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); mInstrumentation.waitForIdleSync(); MotionRecordingSurfaceView mrsv = (MotionRecordingSurfaceView)mSurfaceView; assertFalse(mrsv.gotEvent()); mActivityRule.runOnUiThread(() -> { tpv.punchHoleInTouchableRegion(); }); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); mInstrumentation.waitForIdleSync(); assertTrue(mrsv.gotEvent()); } @Test public void forwardDetachedFromWindow() throws Throwable { DetachRecordingView drv = new DetachRecordingView(mActivity); mEmbeddedView = drv; addForwardingSurfaceView(100, 100); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); assertFalse(drv.mDetached); mActivityRule.runOnUiThread(() -> { mViewParent.removeView(mSurfaceView); }); mInstrumentation.waitForIdleSync(); assertTrue(drv.mDetached); } @Test public void forwardConfigurationChange() throws Throwable { if (!supportsOrientationRequest()) { return; } final CountDownLatch embeddedConfigLatch = new CountDownLatch(1); ConfigRecordingView crv = new ConfigRecordingView(mActivity, embeddedConfigLatch); mEmbeddedView = crv; addForwardingSurfaceView(100, 100); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); mActivityRule.runOnUiThread(() -> { int orientation = mActivity.getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else { orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } mActivity.setRequestedOrientation(orientation); }); embeddedConfigLatch.await(3, TimeUnit.SECONDS); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertEquals(mEmbeddedView.getResources().getConfiguration().orientation, mSurfaceView.getResources().getConfiguration().orientation); }); } @Test public void testEmbeddedViewReceivesInputOnBottom() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, false); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // We should receive no input until we punch a hole mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); mInstrumentation.waitForIdleSync(); assertFalse(mClicked); String originalRegion = getTouchableRegionFromDump(); mActivityRule.runOnUiThread(() -> { mSurfaceView.getRootSurfaceControl().setTouchableRegion(new Region(0,0,1,1)); }); mInstrumentation.waitForIdleSync(); // ViewRootImpl sends the touchable region to the WM via a one-way call, which is great // for performance...however not so good for testability, we have no way // to verify it has arrived! It doesn't make so much sense to bloat // the system image size with a completion callback for just this one test // so we settle for some inelegant spin-polling on the WM dump. // In the future when we revisit WM/Client interface and transactionalize // everything, we should have a standard way to wait on the completion of async // operations waitForTouchableRegionChanged(originalRegion); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); mInstrumentation.waitForIdleSync(); assertTrue(mClicked); } private ICrossProcessSurfaceControlViewHostTestService getService() throws Exception { return mConnections.computeIfAbsent("android.server.wm.scvh", this::connect).get(TIMEOUT_MS); } private static ComponentName repackage(String packageName, ComponentName baseComponent) { return new ComponentName(packageName, baseComponent.getClassName()); } private FutureConnection connect( String packageName) { FutureConnection connection = new FutureConnection<>( ICrossProcessSurfaceControlViewHostTestService.Stub::asInterface); Intent intent = new Intent(); intent.setComponent(repackage(packageName, Components.CrossProcessSurfaceControlViewHostTestService.COMPONENT)); assertTrue(mInstrumentation.getContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)); return connection; } @Test public void testHostInputTokenAllowsObscuredTouches() throws Throwable { mTestService = getService(); assertTrue(mTestService != null); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, false); assertTrue("Failed to wait for SV to get created", mSvCreatedLatch.await(5, TimeUnit.SECONDS)); mActivityRule.runOnUiThread(() -> { mSurfaceView.getRootSurfaceControl().setTouchableRegion(new Region()); }); // TODO(b/279051608): Add touchable regions in WindowInfo test so we can make sure the // touchable regions for the host have been set before proceeding. assertTrue("Failed to wait for host window to be visible", waitForWindowVisible(mSurfaceView)); assertTrue("Failed to wait for embedded window to be visible", waitForWindowVisible(mTestService.getWindowToken())); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); int retryCount = 0; boolean isViewTouchedAndObscured = mTestService.getViewIsTouchedAndObscured(); while (!isViewTouchedAndObscured && retryCount < 3) { retryCount++; Thread.sleep(100); isViewTouchedAndObscured = mTestService.getViewIsTouchedAndObscured(); } assertTrue(isViewTouchedAndObscured); } @Test public void testNoHostInputTokenDisallowsObscuredTouches() throws Throwable { mTestService = getService(); mRemoteSurfacePackage = mTestService.getSurfacePackage(new Binder()); assertTrue(mRemoteSurfacePackage != null); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, false); assertTrue("Failed to wait for SV to get created", mSvCreatedLatch.await(5, TimeUnit.SECONDS)); mActivityRule.runOnUiThread(() -> { mSurfaceView.getRootSurfaceControl().setTouchableRegion(new Region()); }); // TODO(b/279051608): Add touchable regions in WindowInfo test so we can make sure the // touchable regions for the host have been set before proceeding. assertTrue("Failed to wait for host window to be visible", waitForWindowVisible(mSurfaceView)); assertTrue("Failed to wait for embedded window to be visible", waitForWindowVisible(mTestService.getWindowToken())); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertFalse(mTestService.getViewIsTouched()); } @Test public void testPopupWindowReceivesInput() throws Throwable { mEmbeddedView = new Button(mActivity); mEmbeddedView.setOnClickListener((View v) -> { mClicked = true; }); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); mActivityRule.runOnUiThread(() -> { PopupWindow pw = new PopupWindow(); mPopupWindow = pw; Button popupButton = new Button(mActivity); popupButton.setOnClickListener((View v) -> { mPopupClicked = true; }); pw.setWidth(DEFAULT_SURFACE_VIEW_WIDTH); pw.setHeight(DEFAULT_SURFACE_VIEW_HEIGHT); pw.setContentView(popupButton); pw.showAsDropDown(mEmbeddedView); }); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); assertTrue(mPopupClicked); assertFalse(mClicked); mActivityRule.runOnUiThread(() -> { mPopupWindow.dismiss(); }); mInstrumentation.waitForIdleSync(); mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView); mInstrumentation.waitForIdleSync(); assertTrue(mClicked); } @Test public void testPopupWindowPosition() throws Throwable { mEmbeddedView = new View(mActivity); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); mActivityRule.runOnUiThread(() -> { View popupContent = new View(mActivity); popupContent.setBackgroundColor(Color.BLUE); mPopupWindow = new PopupWindow(); mPopupWindow.setWidth(50); mPopupWindow.setHeight(50); mPopupWindow.setContentView(popupContent); mPopupWindow.showAtLocation(mEmbeddedView, Gravity.BOTTOM | Gravity.RIGHT, 0, 0); }); Predicate> hasExpectedFrame = windowInfos -> { if (mPopupWindow == null) { return false; } IBinder parentWindowToken = mEmbeddedView.getWindowToken(); IBinder popupWindowToken = mPopupWindow.getContentView().getWindowToken(); if (parentWindowToken == null || popupWindowToken == null) { return false; } Rect parentBounds = null; Rect popupBounds = null; for (WindowInfo windowInfo : windowInfos) { if (!windowInfo.isVisible) { continue; } if (windowInfo.windowToken == parentWindowToken) { parentBounds = windowInfo.bounds; } else if (windowInfo.windowToken == popupWindowToken) { popupBounds = windowInfo.bounds; } } if (parentBounds == null) { return false; } var expectedBounds = new Rect(parentBounds.left + 50, parentBounds.top + 50, parentBounds.left + 100, parentBounds.top + 100); return expectedBounds.equals(popupBounds); }; assertTrue(waitForWindowInfos(hasExpectedFrame, 5, TimeUnit.SECONDS)); } @Test public void testFloatingWindowWrapContent() throws Throwable { mEmbeddedView = new View(mActivity); addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); View popupContent = new View(mActivity); popupContent.setBackgroundColor(Color.BLUE); popupContent.setLayoutParams(new ViewGroup.LayoutParams(50, 50)); FrameLayout popupView = new FrameLayout(mActivity); popupView.addView(popupContent); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.setTitle("FloatingWindow"); layoutParams.gravity = Gravity.TOP | Gravity.LEFT; layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.token = mEmbeddedView.getWindowToken(); mActivityRule.runOnUiThread(() -> { WindowManager windowManager = mActivity.getSystemService(WindowManager.class); windowManager.addView(popupView, layoutParams); }); Predicate hasExpectedDimensions = windowInfo -> windowInfo.bounds.width() == 50 && windowInfo.bounds.height() == 50; // We pass popupView::getWindowToken as a java.util.function.Supplier // because the popupView is initially unattached and doesn't have a // window token. The supplier is called each time the predicate is // tested, eventually returning the window token. assertTrue(waitForWindowInfo(hasExpectedDimensions, 5, TimeUnit.SECONDS, popupView::getWindowToken)); } @Test public void testFloatingWindowMatchParent() throws Throwable { mEmbeddedView = new View(mActivity); mEmbeddedViewWidth = 50; mEmbeddedViewHeight = 50; addSurfaceView(100, 100); mInstrumentation.waitForIdleSync(); View popupView = new FrameLayout(mActivity); popupView.setBackgroundColor(Color.BLUE); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.setTitle("FloatingWindow"); layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; layoutParams.token = mEmbeddedView.getWindowToken(); mActivityRule.runOnUiThread(() -> { WindowManager windowManager = mActivity.getSystemService(WindowManager.class); windowManager.addView(popupView, layoutParams); }); Predicate hasExpectedDimensions = windowInfo -> windowInfo.bounds.width() == 50 && windowInfo.bounds.height() == 50; assertTrue(waitForWindowInfo(hasExpectedDimensions, 5, TimeUnit.SECONDS, popupView::getWindowToken)); } class TouchTransferringView extends View { boolean mExpectsFirstMotion = true; boolean mExpectsCancel = false; boolean mGotCancel = false; TouchTransferringView(Context c) { super(c); } @Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); synchronized (this) { if (mExpectsFirstMotion) { assertEquals(action, MotionEvent.ACTION_DOWN); assertTrue(mVr.transferTouchGestureToHost()); mExpectsFirstMotion = false; mExpectsCancel = true; } else if (mExpectsCancel) { assertEquals(action, MotionEvent.ACTION_CANCEL); mExpectsCancel = false; mGotCancel = true; } this.notifyAll(); } return true; } void waitForEmbeddedTouch() { synchronized (this) { if (!mExpectsFirstMotion) { assertTrue(mExpectsCancel || mGotCancel); return; } try { this.wait(); } catch (Exception e) { } assertFalse(mExpectsFirstMotion); } } void waitForCancel() { synchronized (this) { if (!mExpectsCancel) { return; } try { this.wait(); } catch (Exception e) { } assertTrue(mGotCancel); } } } @Test public void testEmbeddedWindowCanTransferTouchGestureToHost() throws Throwable { // Inside the embedded view hierarchy, we set up a view that transfers touch // to the host upon receiving a touch event TouchTransferringView ttv = new TouchTransferringView(mActivity); mEmbeddedView = ttv; addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT); mInstrumentation.waitForIdleSync(); waitUntilEmbeddedViewDrawn(); // On the host SurfaceView, we set a motion consumer which expects to receive one event. mHostGotEvent = false; mSurfaceViewMotionConsumer = (ev) -> { synchronized (this) { mHostGotEvent = true; this.notifyAll(); } }; // Prepare to inject an event offset one pixel from the top of the SurfaceViews location // on-screen. final int[] viewOnScreenXY = new int[2]; mSurfaceView.getLocationOnScreen(viewOnScreenXY); final int injectedX = viewOnScreenXY[0] + 1; final int injectedY = viewOnScreenXY[1] + 1; final UiAutomation uiAutomation = mInstrumentation.getUiAutomation(); long downTime = SystemClock.uptimeMillis(); // We inject a down event mCtsTouchUtils.injectDownEvent(uiAutomation, downTime, injectedX, injectedY, null); // And this down event should arrive on the embedded view, which should transfer the touch // focus ttv.waitForEmbeddedTouch(); ttv.waitForCancel(); downTime = SystemClock.uptimeMillis(); // Now we inject an up event mCtsTouchUtils.injectUpEvent(uiAutomation, downTime, false, injectedX, injectedY, null); // This should arrive on the host now, since we have transferred the touch focus synchronized (this) { if (!mHostGotEvent) { try { this.wait(); } catch (Exception e) { } } } assertTrue(mHostGotEvent); } }