Loading core/java/android/window/BackProgressAnimator.java +11 −0 Original line number Diff line number Diff line Loading @@ -212,6 +212,17 @@ public class BackProgressAnimator implements DynamicAnimation.OnAnimationUpdateL mBackCancelledFinishRunnable = null; } /** * Removes the finishCallback passed into {@link #onBackCancelled} */ public void removeOnBackInvokedFinishCallback() { if (mBackInvokedFlingAnim != null) { mBackInvokedFlingAnim.removeUpdateListener(mOnBackInvokedFlingUpdateListener); mBackInvokedFlingAnim.removeEndListener(mOnAnimationEndListener); } mBackInvokedFinishRunnable = null; } /** Returns true if the back animation is in progress. */ @VisibleForTesting(visibility = PACKAGE) public boolean isBackAnimationInProgress() { Loading core/java/android/window/WindowOnBackInvokedDispatcher.java +2 −0 Original line number Diff line number Diff line Loading @@ -243,6 +243,8 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { if (previousTopCallback == callback) { // We should call onBackCancelled() when an active callback is removed from // dispatcher. mProgressAnimator.removeOnBackCancelledFinishCallback(); mProgressAnimator.removeOnBackInvokedFinishCallback(); sendCancelledIfInProgress(callback); mHandler.post(mProgressAnimator::reset); setTopOnBackInvokedCallback(getTopCallback()); Loading core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +103 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ import android.view.IWindowSession; import android.view.ImeBackAnimationController; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; Loading @@ -61,6 +62,10 @@ import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Tests for {@link WindowOnBackInvokedDispatcherTest} Loading Loading @@ -117,6 +122,8 @@ public class WindowOnBackInvokedDispatcherTest { mDispatcher = new WindowOnBackInvokedDispatcher(mContext, Looper.getMainLooper()); mDispatcher.attachToWindow(mWindowSession, mWindow, null, mImeBackAnimationController); clearInvocations(mCallback1); clearInvocations(mCallback2); } private void waitForIdle() { Loading Loading @@ -472,6 +479,102 @@ public class WindowOnBackInvokedDispatcherTest { verifyImeCallackRegistrations(); } @Test public void onBackInvoked_notCalledAfterCallbackUnregistration() throws RemoteException, InterruptedException { // Setup a callback that unregisters itself after the gesture is finished but before the // fling animation has ended final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false); final AtomicInteger onBackInvokedCalled = new AtomicInteger(0); final CountDownLatch onBackCancelledCalled = new CountDownLatch(1); OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackProgressed(@NonNull BackEvent backEvent) { if (unregisterOnProgressUpdate.get()) { mDispatcher.unregisterOnBackInvokedCallback(this); } } @Override public void onBackInvoked() { onBackInvokedCalled.getAndIncrement(); } @Override public void onBackCancelled() { onBackCancelledCalled.countDown(); } }; mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, onBackAnimationCallback); OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); callbackInfo.getCallback().onBackStarted(mBackEvent); waitForIdle(); assertTrue(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); // simulate back gesture finished and onBackInvoked() called, which starts the fling slow // down animation. By setting unregisterOnProgressUpdate to true, the callback will // unregister itself as soon as it receives the first progress event (coming from the // generated fling slow down events) unregisterOnProgressUpdate.set(true); callbackInfo.getCallback().onBackInvoked(); waitForIdle(); onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS); // verify that onBackCancelled is called in this case instead of onBackInvoked assertEquals(0, onBackCancelledCalled.getCount()); assertEquals(0, onBackInvokedCalled.get()); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull()); assertFalse(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); } @Test public void onBackCancelled_calledOnceAfterCallbackUnregistration() throws RemoteException, InterruptedException { // Setup a callback that unregisters itself after the gesture is finished but before the // progress is animated back to 0f final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false); final AtomicInteger onBackInvokedCalled = new AtomicInteger(0); final CountDownLatch onBackCancelledCalled = new CountDownLatch(1); OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackProgressed(@NonNull BackEvent backEvent) { if (unregisterOnProgressUpdate.get()) { mDispatcher.unregisterOnBackInvokedCallback(this); } } @Override public void onBackInvoked() { onBackInvokedCalled.getAndIncrement(); } @Override public void onBackCancelled() { onBackCancelledCalled.countDown(); } }; mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, onBackAnimationCallback); OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); callbackInfo.getCallback().onBackStarted(mBackEvent); waitForIdle(); assertTrue(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); // simulate back gesture finished and onBackCancelled() called, which starts the progress // animation back to 0f. On the first progress emission, the callback will unregister itself unregisterOnProgressUpdate.set(true); callbackInfo.getCallback().onBackCancelled(); waitForIdle(); onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS); // verify that onBackCancelled is called exactly once in this case assertEquals(0, onBackCancelledCalled.getCount()); assertEquals(0, onBackInvokedCalled.get()); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull()); assertFalse(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); } private void verifyImeCallackRegistrations() throws RemoteException { // verify default callback is replaced with ImeBackAnimationController mDispatcher.registerOnBackInvokedCallbackUnchecked(mDefaultImeCallback, PRIORITY_DEFAULT); Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +25 −0 Original line number Diff line number Diff line Loading @@ -177,6 +177,31 @@ public class BackProgressAnimatorTest { assertEquals(1, finishCallbackCalled.getCount()); } @Test public void testOnBackInvokedFinishCallbackNotInvokedWhenRemoved() throws InterruptedException { // Give the animator some progress. final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); mMainThreadHandler.post( () -> mProgressAnimator.onBackProgressed(backEvent)); mTargetProgressCalled.await(1, TimeUnit.SECONDS); assertNotNull(mReceivedBackEvent); // Trigger back invoked animation CountDownLatch finishCallbackCalled = new CountDownLatch(1); InstrumentationRegistry.getInstrumentation().runOnMainSync( () -> mProgressAnimator.onBackInvoked(finishCallbackCalled::countDown)); // remove onBackCancelled finishCallback (while progress is still animating to 0) InstrumentationRegistry.getInstrumentation().runOnMainSync( () -> mProgressAnimator.removeOnBackInvokedFinishCallback()); // call reset (which triggers the finishCallback invocation, if one is present) InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mProgressAnimator.reset()); // verify that finishCallback is not invoked assertEquals(1, finishCallbackCalled.getCount()); } private void onGestureProgress(BackEvent backEvent) { if (mTargetProgress == backEvent.getProgress()) { mReceivedBackEvent = backEvent; Loading Loading
core/java/android/window/BackProgressAnimator.java +11 −0 Original line number Diff line number Diff line Loading @@ -212,6 +212,17 @@ public class BackProgressAnimator implements DynamicAnimation.OnAnimationUpdateL mBackCancelledFinishRunnable = null; } /** * Removes the finishCallback passed into {@link #onBackCancelled} */ public void removeOnBackInvokedFinishCallback() { if (mBackInvokedFlingAnim != null) { mBackInvokedFlingAnim.removeUpdateListener(mOnBackInvokedFlingUpdateListener); mBackInvokedFlingAnim.removeEndListener(mOnAnimationEndListener); } mBackInvokedFinishRunnable = null; } /** Returns true if the back animation is in progress. */ @VisibleForTesting(visibility = PACKAGE) public boolean isBackAnimationInProgress() { Loading
core/java/android/window/WindowOnBackInvokedDispatcher.java +2 −0 Original line number Diff line number Diff line Loading @@ -243,6 +243,8 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { if (previousTopCallback == callback) { // We should call onBackCancelled() when an active callback is removed from // dispatcher. mProgressAnimator.removeOnBackCancelledFinishCallback(); mProgressAnimator.removeOnBackInvokedFinishCallback(); sendCancelledIfInProgress(callback); mHandler.post(mProgressAnimator::reset); setTopOnBackInvokedCallback(getTopCallback()); Loading
core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +103 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ import android.view.IWindowSession; import android.view.ImeBackAnimationController; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; Loading @@ -61,6 +62,10 @@ import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Tests for {@link WindowOnBackInvokedDispatcherTest} Loading Loading @@ -117,6 +122,8 @@ public class WindowOnBackInvokedDispatcherTest { mDispatcher = new WindowOnBackInvokedDispatcher(mContext, Looper.getMainLooper()); mDispatcher.attachToWindow(mWindowSession, mWindow, null, mImeBackAnimationController); clearInvocations(mCallback1); clearInvocations(mCallback2); } private void waitForIdle() { Loading Loading @@ -472,6 +479,102 @@ public class WindowOnBackInvokedDispatcherTest { verifyImeCallackRegistrations(); } @Test public void onBackInvoked_notCalledAfterCallbackUnregistration() throws RemoteException, InterruptedException { // Setup a callback that unregisters itself after the gesture is finished but before the // fling animation has ended final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false); final AtomicInteger onBackInvokedCalled = new AtomicInteger(0); final CountDownLatch onBackCancelledCalled = new CountDownLatch(1); OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackProgressed(@NonNull BackEvent backEvent) { if (unregisterOnProgressUpdate.get()) { mDispatcher.unregisterOnBackInvokedCallback(this); } } @Override public void onBackInvoked() { onBackInvokedCalled.getAndIncrement(); } @Override public void onBackCancelled() { onBackCancelledCalled.countDown(); } }; mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, onBackAnimationCallback); OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); callbackInfo.getCallback().onBackStarted(mBackEvent); waitForIdle(); assertTrue(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); // simulate back gesture finished and onBackInvoked() called, which starts the fling slow // down animation. By setting unregisterOnProgressUpdate to true, the callback will // unregister itself as soon as it receives the first progress event (coming from the // generated fling slow down events) unregisterOnProgressUpdate.set(true); callbackInfo.getCallback().onBackInvoked(); waitForIdle(); onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS); // verify that onBackCancelled is called in this case instead of onBackInvoked assertEquals(0, onBackCancelledCalled.getCount()); assertEquals(0, onBackInvokedCalled.get()); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull()); assertFalse(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); } @Test public void onBackCancelled_calledOnceAfterCallbackUnregistration() throws RemoteException, InterruptedException { // Setup a callback that unregisters itself after the gesture is finished but before the // progress is animated back to 0f final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false); final AtomicInteger onBackInvokedCalled = new AtomicInteger(0); final CountDownLatch onBackCancelledCalled = new CountDownLatch(1); OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackProgressed(@NonNull BackEvent backEvent) { if (unregisterOnProgressUpdate.get()) { mDispatcher.unregisterOnBackInvokedCallback(this); } } @Override public void onBackInvoked() { onBackInvokedCalled.getAndIncrement(); } @Override public void onBackCancelled() { onBackCancelledCalled.countDown(); } }; mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, onBackAnimationCallback); OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); callbackInfo.getCallback().onBackStarted(mBackEvent); waitForIdle(); assertTrue(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); // simulate back gesture finished and onBackCancelled() called, which starts the progress // animation back to 0f. On the first progress emission, the callback will unregister itself unregisterOnProgressUpdate.set(true); callbackInfo.getCallback().onBackCancelled(); waitForIdle(); onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS); // verify that onBackCancelled is called exactly once in this case assertEquals(0, onBackCancelledCalled.getCount()); assertEquals(0, onBackInvokedCalled.get()); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull()); assertFalse(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); } private void verifyImeCallackRegistrations() throws RemoteException { // verify default callback is replaced with ImeBackAnimationController mDispatcher.registerOnBackInvokedCallbackUnchecked(mDefaultImeCallback, PRIORITY_DEFAULT); Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +25 −0 Original line number Diff line number Diff line Loading @@ -177,6 +177,31 @@ public class BackProgressAnimatorTest { assertEquals(1, finishCallbackCalled.getCount()); } @Test public void testOnBackInvokedFinishCallbackNotInvokedWhenRemoved() throws InterruptedException { // Give the animator some progress. final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); mMainThreadHandler.post( () -> mProgressAnimator.onBackProgressed(backEvent)); mTargetProgressCalled.await(1, TimeUnit.SECONDS); assertNotNull(mReceivedBackEvent); // Trigger back invoked animation CountDownLatch finishCallbackCalled = new CountDownLatch(1); InstrumentationRegistry.getInstrumentation().runOnMainSync( () -> mProgressAnimator.onBackInvoked(finishCallbackCalled::countDown)); // remove onBackCancelled finishCallback (while progress is still animating to 0) InstrumentationRegistry.getInstrumentation().runOnMainSync( () -> mProgressAnimator.removeOnBackInvokedFinishCallback()); // call reset (which triggers the finishCallback invocation, if one is present) InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mProgressAnimator.reset()); // verify that finishCallback is not invoked assertEquals(1, finishCallbackCalled.getCount()); } private void onGestureProgress(BackEvent backEvent) { if (mTargetProgress == backEvent.getProgress()) { mReceivedBackEvent = backEvent; Loading