/* * Copyright (C) 2022 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.view.Display.DEFAULT_DISPLAY; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; import static org.junit.Assert.assertTrue; import android.app.Instrumentation; import android.view.KeyEvent; import android.view.MotionEvent; import android.window.BackEvent; import android.window.OnBackAnimationCallback; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.uiautomator.UiDevice; import com.android.compatibility.common.util.CddTest; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Integration test for back navigation */ public class OnBackInvokedCallbackGestureTest extends ActivityManagerTestBase { private static final int PROGRESS_SWIPE_STEPS = 10; private Instrumentation mInstrumentation; private UiDevice mUiDevice; private BackInvocationTracker mTracker = new BackInvocationTracker(); private BackNavigationActivity mActivity; private final OnBackAnimationCallback mAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackStarted(BackEvent e) { mTracker.trackBackStarted(); } @Override public void onBackInvoked() { mTracker.trackBackInvoked(); } @Override public void onBackCancelled() { mTracker.trackBackCancelled(); } @Override public void onBackProgressed(BackEvent e) { mTracker.trackBackProgressed(e); } }; @Before public void setup() throws Exception { super.setUp(); mInstrumentation = InstrumentationRegistry.getInstrumentation(); mUiDevice = UiDevice.getInstance(mInstrumentation); enableAndAssumeGestureNavigationMode(); mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(); mTracker.reset(); final TestActivitySession activitySession = createManagedTestActivitySession(); activitySession.launchTestActivityOnDisplaySync( BackNavigationActivity.class, DEFAULT_DISPLAY); mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY); mInstrumentation.getUiAutomation().syncInputTransactions(); mActivity = activitySession.getActivity(); registerBackCallback(mActivity); } @Test @CddTest(requirements = { "7.2.3/H-0-5,H-0-6,H-0-8" }) public void invokesCallback_invoked() throws InterruptedException { int midHeight = mUiDevice.getDisplayHeight() / 2; int midWidth = mUiDevice.getDisplayWidth() / 2; final TouchHelper.SwipeSession touchSession = new TouchHelper.SwipeSession( DEFAULT_DISPLAY, true, false); long startDownTime = touchSession.beginSwipe(0, midHeight); // Inject another move event to trigger back start. TouchHelper.injectMotion(startDownTime, startDownTime, MotionEvent.ACTION_MOVE, 0, midHeight, DEFAULT_DISPLAY, true, false); assertInvoked(mTracker.mStartLatch); assertNotInvoked(mTracker.mProgressLatch); assertNotInvoked(mTracker.mInvokeLatch); assertNotInvoked(mTracker.mCancelLatch); touchSession.continueSwipe(midWidth, midHeight, PROGRESS_SWIPE_STEPS); assertInvoked(mTracker.mProgressLatch); assertNotInvoked(mTracker.mInvokeLatch); assertNotInvoked(mTracker.mCancelLatch); List events = mTracker.mProgressEvents; assertTrue(events.size() > 0); for (int i = 0; i < events.size() - 1; i++) { // Check that progress events report increasing progress values. // TODO(b/258817762): Verify more once the progress clamping behavior is implemented. BackEvent event = events.get(i); assertTrue(event.getProgress() <= events.get(i + 1).getProgress()); assertTrue(event.getTouchX() <= events.get(i + 1).getTouchX()); assertEquals(midHeight, midHeight, event.getTouchY()); assertEquals(BackEvent.EDGE_LEFT, event.getSwipeEdge()); } touchSession.finishSwipe(); assertInvoked(mTracker.mInvokeLatch); assertNotInvoked(mTracker.mCancelLatch); } @Test @CddTest(requirements = { "7.2.3/H-0-7" }) public void invokesCallback_cancelled() throws InterruptedException { int midHeight = mUiDevice.getDisplayHeight() / 2; int midWidth = mUiDevice.getDisplayWidth() / 2; final TouchHelper.SwipeSession touchSession = new TouchHelper.SwipeSession( DEFAULT_DISPLAY, true, false); long startDownTime = touchSession.beginSwipe(0, midHeight); // Inject another move event to trigger back start. TouchHelper.injectMotion(startDownTime, startDownTime, MotionEvent.ACTION_MOVE, 0, midHeight, DEFAULT_DISPLAY, true, false); touchSession.continueSwipe(midWidth, midHeight, PROGRESS_SWIPE_STEPS); assertInvoked(mTracker.mProgressLatch); mTracker.reset(); mTracker.mIsCancelRequested = true; touchSession.cancelSwipe(); assertInvoked(mTracker.mCancelLatch); assertNotInvoked(mTracker.mInvokeLatch); assertInvoked(mTracker.mCancelProgressLatch); List events = mTracker.mProgressEvents; assertTrue(events.size() > 0); assertTrue(events.get(events.size() - 1).getProgress() == 0); } @Test @CddTest(requirements = { "7.2.3/H-0-5,H-0-6" }) public void invokesCallbackInButtonsNav_invoked() throws InterruptedException { long downTime = TouchHelper.injectKeyActionDown(KeyEvent.KEYCODE_BACK, /* longpress = */ false, /* sync = */ true); assertInvoked(mTracker.mStartLatch); assertNotInvoked(mTracker.mProgressLatch); assertNotInvoked(mTracker.mInvokeLatch); assertNotInvoked(mTracker.mCancelLatch); TouchHelper.injectKeyActionUp(KeyEvent.KEYCODE_BACK, /* downTime = */ downTime, /* cancelled = */ false, /* sync = */ true); assertInvoked(mTracker.mInvokeLatch); assertNotInvoked(mTracker.mProgressLatch); assertNotInvoked(mTracker.mCancelLatch); } @Test @CddTest(requirements = { "7.2.3/H-0-7" }) public void invokesCallbackInButtonsNav_cancelled() throws InterruptedException { long downTime = TouchHelper.injectKeyActionDown(KeyEvent.KEYCODE_BACK, /* longpress = */ false, /* sync = */ true); TouchHelper.injectKeyActionUp(KeyEvent.KEYCODE_BACK, /* downTime = */ downTime, /* cancelled = */ true, /* sync = */ true); assertInvoked(mTracker.mCancelLatch); assertNotInvoked(mTracker.mProgressLatch); assertNotInvoked(mTracker.mInvokeLatch); } @Test public void constructsEvent() { final float x = 200; final float y = 300; final float progress = 0.5f; final int swipeEdge = BackEvent.EDGE_RIGHT; BackEvent event = new BackEvent(x, y, progress, swipeEdge); assertEquals(x, event.getTouchX()); assertEquals(y, event.getTouchY()); assertEquals(progress, event.getProgress()); assertEquals(swipeEdge, event.getSwipeEdge()); } private void assertInvoked(CountDownLatch latch) throws InterruptedException { assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); } private void assertNotInvoked(CountDownLatch latch) { assertTrue(latch.getCount() >= 1); } private void registerBackCallback(BackNavigationActivity activity) { CountDownLatch backRegisteredLatch = new CountDownLatch(1); activity.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 0, mAnimationCallback); backRegisteredLatch.countDown(); try { if (!backRegisteredLatch.await(100, TimeUnit.MILLISECONDS)) { fail("Back callback was not registered on the Activity thread. This might be " + "an error with the test itself."); } } catch (InterruptedException e) { fail(e.getMessage()); } } /** Helper class to track {@link android.window.OnBackAnimationCallback} invocations. */ static class BackInvocationTracker { private CountDownLatch mStartLatch; private CountDownLatch mInvokeLatch; private CountDownLatch mProgressLatch; private CountDownLatch mCancelLatch; private CountDownLatch mCancelProgressLatch; private boolean mIsCancelRequested = false; private final ArrayList mProgressEvents = new ArrayList<>(); private void reset() { mStartLatch = new CountDownLatch(1); mInvokeLatch = new CountDownLatch(1); mProgressLatch = new CountDownLatch(PROGRESS_SWIPE_STEPS); mCancelLatch = new CountDownLatch(1); mCancelProgressLatch = new CountDownLatch(1); mIsCancelRequested = false; mProgressEvents.clear(); } private void trackBackStarted() { mStartLatch.countDown(); } private void trackBackProgressed(BackEvent e) { mProgressEvents.add(e); if (mIsCancelRequested && 0 == e.getProgress()) { // Ensure the progress could reach to 0 for cancel animation. mCancelProgressLatch.countDown(); } else { mProgressLatch.countDown(); } } private void trackBackCancelled() { mCancelLatch.countDown(); } private void trackBackInvoked() { mInvokeLatch.countDown(); } } }