Disable 3-button-nav buttons during back button hold

Bug: 373544911
Test: TaskbarNavButtonControllerTest
Test: Manual, i.e. verify that pressing buttons in 3-button-nav while the back button is pressed does not have any effect
Flag: com.android.window.flags.predictive_back_three_button_nav
Change-Id: I5abad5f2f74d09c790380a2eeb27aff3b780b925
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index fbd1b6e..d84057f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static android.view.KeyEvent.ACTION_UP;
 import static android.view.View.AccessibilityDelegate;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -72,7 +73,6 @@
 import android.graphics.drawable.RotateDrawable;
 import android.inputmethodservice.InputMethodService;
 import android.os.Handler;
-import android.os.SystemClock;
 import android.util.Property;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -862,17 +862,12 @@
             TaskbarNavButtonController navButtonController) {
         buttonView.setOnTouchListener((v, event) -> {
             if (event.getAction() == MotionEvent.ACTION_MOVE) return false;
-            long time = SystemClock.uptimeMillis();
-            int action = event.getAction();
-            KeyEvent keyEvent = new KeyEvent(time, time,
-                    action == MotionEvent.ACTION_DOWN ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
-                    KeyEvent.KEYCODE_BACK, 0);
-            if (event.getAction() == MotionEvent.ACTION_CANCEL) {
-                keyEvent.cancel();
-            }
-            navButtonController.executeBack(keyEvent);
-
-            if (action == MotionEvent.ACTION_UP) {
+            int motionEventAction = event.getAction();
+            int keyEventAction = motionEventAction == MotionEvent.ACTION_DOWN
+                    ? KeyEvent.ACTION_DOWN : ACTION_UP;
+            boolean isCancelled = event.getAction() == MotionEvent.ACTION_CANCEL;
+            navButtonController.sendBackKeyEvent(keyEventAction, isCancelled);
+            if (motionEventAction == MotionEvent.ACTION_UP) {
                 buttonView.performClick();
             }
             return false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index d4764c7..4881836 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -16,7 +16,8 @@
 
 package com.android.launcher3.taskbar;
 
-import static android.view.MotionEvent.ACTION_UP;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.ACTION_UP;
 
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
@@ -38,6 +39,7 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -78,6 +80,7 @@
     private long mLastScreenPinLongPress;
     private boolean mScreenPinned;
     private boolean mAssistantLongPressEnabled;
+    private int mLastSentBackAction = ACTION_UP;
 
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
@@ -85,6 +88,8 @@
 
         pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress);
         pw.println(prefix + "\tmScreenPinned=" + mScreenPinned);
+        pw.println(prefix + "\tmLastSentBackAction="
+                + KeyEvent.actionToString(mLastSentBackAction));
     }
 
     @Retention(RetentionPolicy.SOURCE)
@@ -141,6 +146,11 @@
         if (buttonType == BUTTON_SPACE) {
             return;
         }
+        if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN) {
+            Log.i(TAG, "Button click ignored while back button is pressed");
+            // prevent interactions with other buttons while back button is pressed
+            return;
+        }
         // Provide the same haptic feedback that the system offers for virtual keys.
         view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
         switch (buttonType) {
@@ -180,6 +190,13 @@
         if (buttonType == BUTTON_SPACE) {
             return false;
         }
+        if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN
+                && buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) {
+            // prevent interactions with other buttons while back button is pressed (except back
+            // and recents button for screen-unpin action).
+            Log.i(TAG, "Button long click ignored while back button is pressed");
+            return false;
+        }
 
         // Provide the same haptic feedback that the system offers for long press.
         // The haptic feedback from long pressing on the home button is handled by circle to search.
@@ -327,13 +344,27 @@
         mCallbacks.onToggleOverview();
     }
 
-    void executeBack(@Nullable KeyEvent keyEvent) {
+    void sendBackKeyEvent(int action, boolean cancelled) {
+        if (action == mLastSentBackAction) {
+            // There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events
+            return;
+        }
+        long time = SystemClock.uptimeMillis();
+        KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0);
+        if (cancelled) {
+            keyEvent.cancel();
+        }
+        executeBack(keyEvent);
+    }
+
+    private void executeBack(@Nullable KeyEvent keyEvent) {
         if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) {
             logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
             mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
                     GestureType.BACK);
         }
         mSystemUiProxy.onBackEvent(keyEvent);
+        mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP;
     }
 
     private void onImeSwitcherPress() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 4b04dba..6ebae49 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -15,9 +15,11 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,10 @@
 import static org.mockito.Mockito.when;
 
 import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.Flags;
 
@@ -43,8 +49,10 @@
 import com.android.systemui.contextualeducation.GestureType;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -76,6 +84,9 @@
     @Mock
     View mockView;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private int mHomePressCount;
     private int mOverviewToggleCount;
     private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() {
@@ -333,4 +344,46 @@
         verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
         verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
     }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testPredictiveBackInvoked() {
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+        verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_UP, false);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testPredictiveBackCancelled() {
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true);
+        verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_UP, true);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testButtonsDisabledWhileBackPressed() {
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.onButtonClick(BUTTON_HOME, mockView);
+        mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
+        mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView);
+        mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+        assertThat(mHomePressCount).isEqualTo(0);
+        verify(mockSystemUiProxy, never()).notifyAccessibilityButtonLongClicked();
+        assertThat(mOverviewToggleCount).isEqualTo(0);
+        verify(mockSystemUiProxy, never()).onImeSwitcherPressed();
+    }
+
+    private void verifyKeyEvent(KeyEvent keyEvent, int action, boolean isCancelled) {
+        assertEquals(isCancelled, keyEvent.isCanceled());
+        assertEquals(action, KeyEvent.ACTION_DOWN, keyEvent.getAction());
+    }
 }