Cancel the back animation if the topCallback is removed or the WindowOnBackInvokedDispatcher is detached from the window.

We'll send an onBackCancelled() event whenever the top callback is in progress and has been removed from the callbacks.

Changes:
- We send onBackCancelled() and reset the ProgressAnimator when clear() is called.
- Added a check to avoid sending an onBackInvoked() to the app if the ProgressAnimator is not in progress.

Test: atest WindowOnBackInvokedDispatcherTest
Bug: 255324784
Change-Id: I0dcc1fb869070dd5bab6c0802c933783266594e5
This commit is contained in:
omarmt
2023-06-01 13:59:33 +00:00
parent 4c45ee8c1d
commit 75b025d1db
2 changed files with 70 additions and 9 deletions

View File

@@ -163,19 +163,25 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher {
// Re-populate the top callback to WM if the removed callback was previously the top one.
if (previousTopCallback == callback) {
// We should call onBackCancelled() when an active callback is removed from dispatcher.
if (mProgressAnimator.isBackAnimationInProgress()
&& callback instanceof OnBackAnimationCallback) {
// The ProgressAnimator will handle the new topCallback, so we don't want to call
// onBackCancelled() on it. We call immediately the callback instead.
OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback;
animatedCallback.onBackCancelled();
Log.d(TAG, "The callback was removed while a back animation was in progress, "
+ "an onBackCancelled() was dispatched.");
}
sendCancelledIfInProgress(callback);
setTopOnBackInvokedCallback(getTopCallback());
}
}
private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) {
boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
if (isInProgress && callback instanceof OnBackAnimationCallback) {
OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback;
animatedCallback.onBackCancelled();
if (DEBUG) {
Log.d(TAG, "sendCancelIfRunning: callback canceled");
}
} else {
Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress
+ "callback=" + callback);
}
}
@Override
public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) {
registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM);
@@ -188,9 +194,20 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher {
mImeDispatcher = null;
}
if (!mAllCallbacks.isEmpty()) {
OnBackInvokedCallback topCallback = getTopCallback();
if (topCallback != null) {
sendCancelledIfInProgress(topCallback);
} else {
// Should not be possible
Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty");
}
// Clear binder references in WM.
setTopOnBackInvokedCallback(null);
}
// We should also stop running animations since all callbacks have been removed.
// note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler.
Handler.getMain().post(mProgressAnimator::reset);
mAllCallbacks.clear();
mOnBackInvokedCallbacks.clear();
}
@@ -342,12 +359,17 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher {
@Override
public void onBackInvoked() throws RemoteException {
Handler.getMain().post(() -> {
boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
mProgressAnimator.reset();
final OnBackInvokedCallback callback = mCallbackRef.get();
if (callback == null) {
Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference.");
return;
}
if (callback instanceof OnBackAnimationCallback && !isInProgress) {
Log.w(TAG, "ProgressAnimator was not in progress, skip onBackInvoked().");
return;
}
callback.onBackInvoked();
});
}

View File

@@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -340,4 +341,42 @@ public class WindowOnBackInvokedDispatcherTest {
verify(mCallback1).onBackCancelled();
verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull());
}
@Test
public void onBackInvoked_calledAfterOnBackStarted() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo();
callbackInfo.getCallback().onBackStarted(mBackEvent);
waitForIdle();
verify(mCallback1).onBackStarted(any(BackEvent.class));
callbackInfo.getCallback().onBackInvoked();
waitForIdle();
verify(mCallback1).onBackInvoked();
verify(mCallback1, never()).onBackCancelled();
}
@Test
public void onDetachFromWindow_cancelCallbackAndIgnoreOnBackInvoked() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo();
callbackInfo.getCallback().onBackStarted(mBackEvent);
waitForIdle();
verify(mCallback1).onBackStarted(any(BackEvent.class));
// This should trigger mCallback1.onBackCancelled()
mDispatcher.detachFromWindow();
// This should be ignored by mCallback1
callbackInfo.getCallback().onBackInvoked();
waitForIdle();
verify(mCallback1, never()).onBackInvoked();
verify(mCallback1).onBackCancelled();
}
}