Fixes Activity cannot receive ACTION_CANCEL after onBackStarted.
Upon the client-side receiving onBackStarted, isBackGestureInProgress
returns true, and subsequent motion events are consumed by DecorView.
Once the CANCEL event is handled, the gesture is marked as intercepted,
and subsequent motion events are ignored.
Flag: com.android.window.flags.intercept_motion_from_move_to_cancel
Bug: 404173501
Test: atest OnBackInvokedCallbackGestureTest
Test: manual test with sample app, verify the CANCEL event should be
dispatched to the target which receive DOWN.
Change-Id: I8654a9d277ee38bbc483b6190332abb8833bc510
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 7e0818a..8f6a8a1 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -19,8 +19,10 @@
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE;
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
import static android.view.flags.Flags.FLAG_TOOLKIT_VIEWGROUP_SET_REQUESTED_FRAME_RATE_API;
-import static android.view.flags.Flags.toolkitViewgroupSetRequestedFrameRateApi;
import static android.view.flags.Flags.scrollCaptureTargetZOrderFix;
+import static android.view.flags.Flags.toolkitViewgroupSetRequestedFrameRateApi;
+
+import static com.android.window.flags.Flags.interceptMotionFromMoveToCancel;
import android.animation.LayoutTransition;
import android.annotation.CallSuper;
@@ -88,7 +90,6 @@
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
-
/**
* <p>
* A <code>ViewGroup</code> is a special view that can contain other views
@@ -2674,7 +2675,8 @@
ViewRootImpl viewRootImpl = getViewRootImpl();
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
- final boolean isBackGestureInProgress = (viewRootImpl != null
+ final boolean isBackGestureInProgress = !interceptMotionFromMoveToCancel()
+ && (viewRootImpl != null
&& viewRootImpl.getOnBackInvokedDispatcher().isBackGestureInProgress());
if (!disallowIntercept || isBackGestureInProgress) {
// Allow back to intercept touch
diff --git a/core/java/android/window/BackTouchTracker.java b/core/java/android/window/BackTouchTracker.java
index bda4d2e..d8a1d38 100644
--- a/core/java/android/window/BackTouchTracker.java
+++ b/core/java/android/window/BackTouchTracker.java
@@ -52,6 +52,7 @@
private int mSwipeEdge;
private boolean mShouldUpdateStartLocation = false;
private TouchTrackerState mState = TouchTrackerState.INITIAL;
+ private boolean mIsInterceptedMotionEvent;
/**
* Updates the tracker with a new motion event.
@@ -117,6 +118,20 @@
return mState == TouchTrackerState.FINISHED;
}
+ /**
+ * Returns whether current app should not receive motion event.
+ */
+ public boolean isInterceptedMotionEvent() {
+ return mIsInterceptedMotionEvent;
+ }
+
+ /**
+ * Marks the app will not receive motion event from current gesture.
+ */
+ public void setMotionEventIntercepted() {
+ mIsInterceptedMotionEvent = true;
+ }
+
/** Sets the start location of the back gesture. */
public void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
mInitTouchX = touchX;
@@ -154,6 +169,7 @@
mState = TouchTrackerState.INITIAL;
mSwipeEdge = BackEvent.EDGE_LEFT;
mShouldUpdateStartLocation = false;
+ mIsInterceptedMotionEvent = false;
}
/** Creates a start {@link BackMotionEvent}. */
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 2911b0a..2660bcd 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -314,6 +314,24 @@
}
}
+ /**
+ * Returns whether current app should not receive motion event.
+ */
+ public boolean isInterceptedMotionEvent() {
+ synchronized (mLock) {
+ return mTouchTracker.isInterceptedMotionEvent();
+ }
+ }
+
+ /**
+ * Marks the app will not receive motion event from current gesture.
+ */
+ public void setMotionEventIntercepted() {
+ synchronized (mLock) {
+ mTouchTracker.setMotionEventIntercepted();
+ }
+ }
+
private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) {
boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
if (isInProgress && callback instanceof OnBackAnimationCallback) {
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 987b4c7..f2baa78 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -556,3 +556,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "intercept_motion_from_move_to_cancel"
+ namespace: "windowing_frontend"
+ description: "Ensure that the client receives ACTION_CANCEL when the back gesture is intercepted."
+ bug: "404173501"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index e20a52b..91368e2 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -42,6 +42,7 @@
import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
import static com.android.internal.policy.PhoneWindow.FEATURE_OPTIONS_PANEL;
+import static com.android.window.flags.Flags.interceptMotionFromMoveToCancel;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -428,11 +429,50 @@
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (interceptBackProgress(ev)) {
+ return true;
+ }
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
+ private boolean interceptBackProgress(MotionEvent ev) {
+ if (!interceptMotionFromMoveToCancel()) {
+ return false;
+ }
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl == null) {
+ return false;
+ }
+ viewRootImpl.getOnBackInvokedDispatcher().onMotionEvent(ev);
+ // Intercept touch if back gesture is in progress.
+ boolean isBackGestureInProgress = viewRootImpl.getOnBackInvokedDispatcher()
+ .isBackGestureInProgress();
+ if (!isBackGestureInProgress && mWearGestureInterceptionDetector != null) {
+ boolean wasIntercepting = mWearGestureInterceptionDetector.isIntercepting();
+ boolean intercepting = mWearGestureInterceptionDetector.onInterceptTouchEvent(ev);
+ if (wasIntercepting != intercepting) {
+ viewRootImpl.updateDecorViewGestureInterception(intercepting);
+ }
+ if (intercepting) {
+ isBackGestureInProgress = true;
+ }
+ }
+
+ if (!isBackGestureInProgress) {
+ return false;
+ }
+ // Intercept touch if back gesture is in progress.
+ if (!viewRootImpl.getOnBackInvokedDispatcher().isInterceptedMotionEvent()) {
+ viewRootImpl.getOnBackInvokedDispatcher().setMotionEventIntercepted();
+ ev.setAction(MotionEvent.ACTION_CANCEL);
+ // Return false to deliver the first CANCEL.
+ return false;
+ }
+ return true;
+ }
+
@Override
public boolean dispatchTrackballEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
@@ -511,22 +551,25 @@
}
}
- ViewRootImpl viewRootImpl = getViewRootImpl();
- if (viewRootImpl != null) {
- viewRootImpl.getOnBackInvokedDispatcher().onMotionEvent(event);
- // Intercept touch if back gesture is in progress.
- if (viewRootImpl.getOnBackInvokedDispatcher().isBackGestureInProgress()) {
- return true;
+ if (!interceptMotionFromMoveToCancel()) {
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.getOnBackInvokedDispatcher().onMotionEvent(event);
+ // Intercept touch if back gesture is in progress.
+ if (viewRootImpl.getOnBackInvokedDispatcher().isBackGestureInProgress()) {
+ return true;
+ }
}
- }
- if (viewRootImpl != null && mWearGestureInterceptionDetector != null) {
- boolean wasIntercepting = mWearGestureInterceptionDetector.isIntercepting();
- boolean intercepting = mWearGestureInterceptionDetector.onInterceptTouchEvent(event);
- if (wasIntercepting != intercepting) {
- viewRootImpl.updateDecorViewGestureInterception(intercepting);
- }
- if (intercepting) {
- return true;
+ if (viewRootImpl != null && mWearGestureInterceptionDetector != null) {
+ boolean wasIntercepting = mWearGestureInterceptionDetector.isIntercepting();
+ boolean intercepting = mWearGestureInterceptionDetector
+ .onInterceptTouchEvent(event);
+ if (wasIntercepting != intercepting) {
+ viewRootImpl.updateDecorViewGestureInterception(intercepting);
+ }
+ if (intercepting) {
+ return true;
+ }
}
}