Use nearest region for all the nav buttons in 3-button folded mode

Bug: 230395757
Test: In 3-button folded mode, make sure that the touches that happen between nav buttons go to the nearest button. No regression in other modes.

Change-Id: Icb776a9a4ed4fc31d33dc3267c7053f2b0da0bfc
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index 72d7485..736706a 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -35,7 +35,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-    <FrameLayout
+    <com.android.launcher3.taskbar.navbutton.NearestTouchFrame
         android:id="@+id/navbuttons_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -62,7 +62,7 @@
             android:layout_height="match_parent"
             android:gravity="center_vertical"
             android:layout_gravity="end"/>
-    </FrameLayout>
+    </com.android.launcher3.taskbar.navbutton.NearestTouchFrame>
 
     <com.android.launcher3.taskbar.StashedHandleView
         android:id="@+id/stashed_handle"
diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml
index 0890a4e..6af7cf4 100644
--- a/quickstep/res/layout/transient_taskbar.xml
+++ b/quickstep/res/layout/transient_taskbar.xml
@@ -52,7 +52,7 @@
         android:elevation="@dimen/bubblebar_elevation"
         />
 
-    <FrameLayout
+    <com.android.launcher3.taskbar.navbutton.NearestTouchFrame
         android:id="@+id/navbuttons_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -83,7 +83,7 @@
             android:paddingTop="@dimen/taskbar_contextual_padding_top"
             android:gravity="center_vertical"
             android:layout_gravity="end"/>
-    </FrameLayout>
+    </com.android.launcher3.taskbar.navbutton.NearestTouchFrame>
 
     <com.android.launcher3.taskbar.StashedHandleView
         android:id="@+id/stashed_handle"
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
index 0a9dfff..3635827 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
+import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
 
 /**
  * Controller for managing buttons and status icons in taskbar in a desktop environment.
@@ -43,7 +44,7 @@
     private TaskbarControllers mControllers;
 
     public DesktopNavbarButtonsViewController(TaskbarActivityContext context,
-            @Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
+            @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) {
         super(context, navigationBarPanelContext, navButtonsView);
         mContext = context;
         mNavButtonsView = navButtonsView;
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index bd44a35..1a1c64d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -91,6 +91,7 @@
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory;
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter;
+import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
 import com.android.launcher3.util.DimensionUtils;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -151,7 +152,7 @@
     private final TaskbarActivityContext mContext;
     private final @Nullable Context mNavigationBarPanelContext;
     private final WindowManagerProxy mWindowManagerProxy;
-    private final FrameLayout mNavButtonsView;
+    private final NearestTouchFrame mNavButtonsView;
     private final LinearLayout mNavButtonContainer;
     // Used for IME+A11Y buttons
     private final ViewGroup mEndContextualContainer;
@@ -208,7 +209,7 @@
     private ImageView mRecentsButton;
 
     public NavbarButtonsViewController(TaskbarActivityContext context,
-            @Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
+            @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) {
         mContext = context;
         mNavigationBarPanelContext = navigationBarPanelContext;
         mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext);
@@ -517,6 +518,10 @@
         return (mState & FLAG_IME_VISIBLE) != 0;
     }
 
+    public boolean isImeRenderingNavButtons() {
+        return mIsImeRenderingNavButtons;
+    }
+
     /**
      * Returns true if the home button is disabled
      */
@@ -1003,6 +1008,8 @@
                 + mOnTaskbarBackgroundNavButtonColorOverride.value);
         pw.println(prefix + "\t\tmOnBackgroundNavButtonColorOverrideMultiplier="
                 + mOnBackgroundNavButtonColorOverrideMultiplier.value);
+
+        mNavButtonsView.dumpLogs(prefix + "\t", pw);
     }
 
     private static String getStateString(int flags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 9f65f81..2b88f02 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -104,6 +104,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
+import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -236,7 +237,7 @@
         mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
         TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
         TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim);
-        FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
+        NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
         StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
         BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
         StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 633383d..b8e6889 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -309,7 +309,12 @@
             controllers.bubbleControllers.isPresent &&
                 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
         var insetsIsTouchableRegion = true
-        if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
+        if (context.isPhoneButtonNavMode &&
+                (!controllers.navbarButtonsViewController.isImeVisible
+                        || !controllers.navbarButtonsViewController.isImeRenderingNavButtons)) {
+            insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
+            insetsIsTouchableRegion = false
+        } else if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
             debugTouchableRegion.lastSetTouchableReason = "Taskbar is invisible"
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 22f0131..672bc0d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -56,7 +56,7 @@
          */
         fun getUiLayoutter(
                 deviceProfile: DeviceProfile,
-                navButtonsView: FrameLayout,
+                navButtonsView: NearestTouchFrame,
                 imeSwitcher: ImageView?,
                 rotationButton: RotationButton?,
                 a11yButton: ImageView?,
@@ -78,6 +78,7 @@
             return when {
                 isPhoneNavMode -> {
                     if (!deviceProfile.isLandscape) {
+                        navButtonsView.setIsVertical(false)
                         PhonePortraitNavLayoutter(
                                 resources,
                                 navButtonContainer,
@@ -88,6 +89,7 @@
                                 a11yButton
                         )
                     } else if (surfaceRotation == ROTATION_90) {
+                        navButtonsView.setIsVertical(true)
                         PhoneLandscapeNavLayoutter(
                                 resources,
                                 navButtonContainer,
@@ -98,6 +100,7 @@
                                 a11yButton
                         )
                     } else {
+                        navButtonsView.setIsVertical(true)
                         PhoneSeascapeNavLayoutter(
                                 resources,
                                 navButtonContainer,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java b/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java
new file mode 100644
index 0000000..a477303
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 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 com.android.launcher3.taskbar.navbutton;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Redirects touches that aren't handled by any child view to the nearest
+ * clickable child. Only takes effect on <sw600dp.
+ */
+public class NearestTouchFrame extends FrameLayout {
+
+    private final List<View> mClickableChildren = new ArrayList<>();
+    private final List<View> mAttachedChildren = new ArrayList<>();
+    private final boolean mIsActive;
+
+    private boolean mIsVertical;
+    private View mTouchingChild;
+    private final Map<View, Rect> mTouchableRegions = new HashMap<>();
+    /**
+     * Used to sort all child views either by their left position or their top position,
+     * depending on if this layout is used horizontally or vertically, respectively
+     */
+    private final Comparator<View> mChildRegionComparator =
+            (view1, view2) -> {
+                int startingCoordView1 = mIsVertical ? view1.getTop() : view1.getLeft();
+                int startingCoordView2 = mIsVertical ? view2.getTop() : view2.getLeft();
+
+                return startingCoordView1 - startingCoordView2;
+            };
+
+    public NearestTouchFrame(Context context, AttributeSet attrs) {
+        this(context, attrs, context.getResources().getConfiguration());
+    }
+
+    public NearestTouchFrame(Context context, AttributeSet attrs, Configuration c) {
+        super(context, attrs);
+        mIsActive = c.smallestScreenWidthDp < 600;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mClickableChildren.clear();
+        mAttachedChildren.clear();
+        mTouchableRegions.clear();
+        addClickableChildren(this);
+        cacheClosestChildLocations();
+    }
+
+    /**
+     * Populates {@link #mTouchableRegions} with the regions where each clickable child is the
+     * closest for a given point on this layout.
+     */
+    private void cacheClosestChildLocations() {
+        if (getWidth() == 0 || getHeight() == 0) {
+            return;
+        }
+
+        // Sort by either top or left depending on mIsVertical, then take out all children
+        // that are not attached to window
+        mClickableChildren.sort(mChildRegionComparator);
+        mClickableChildren.stream()
+                .filter(View::isAttachedToWindow)
+                .forEachOrdered(mAttachedChildren::add);
+
+        // Cache bounds of children
+        // Mark coordinates where the actual child layout resides in this frame's window
+        for (int i = 0; i < mAttachedChildren.size(); i++) {
+            View child = mAttachedChildren.get(i);
+            if (!child.isAttachedToWindow()) {
+                continue;
+            }
+            Rect childRegion = getChildsBounds(child);
+
+            // We compute closest child from this child to the previous one
+            if (i == 0) {
+                // First child, nothing to the left/top of it
+                if (mIsVertical) {
+                    childRegion.top = 0;
+                } else {
+                    childRegion.left = 0;
+                }
+                mTouchableRegions.put(child, childRegion);
+                continue;
+            }
+
+            View previousChild = mAttachedChildren.get(i - 1);
+            Rect previousChildBounds = mTouchableRegions.get(previousChild);
+            int midPoint;
+            if (mIsVertical) {
+                int distance = childRegion.top - previousChildBounds.bottom;
+                midPoint = distance / 2;
+                childRegion.top -= midPoint;
+                previousChildBounds.bottom += midPoint - ((distance % 2) == 0 ? 1 : 0);
+            } else {
+                int distance = childRegion.left - previousChildBounds.right;
+                midPoint = distance / 2;
+                childRegion.left -= midPoint;
+                previousChildBounds.right += midPoint - ((distance % 2) == 0 ? 1 : 0);
+            }
+
+            if (i == mClickableChildren.size() - 1) {
+                // Last child, nothing to right/bottom of it
+                if (mIsVertical) {
+                    childRegion.bottom = getHeight();
+                } else {
+                    childRegion.right = getWidth();
+                }
+            }
+
+            mTouchableRegions.put(child, childRegion);
+        }
+    }
+
+    void setIsVertical(boolean isVertical) {
+        mIsVertical = isVertical;
+    }
+
+    private Rect getChildsBounds(View child) {
+        int left = child.getLeft();
+        int top = child.getTop();
+        int right = left + child.getWidth();
+        int bottom = top + child.getHeight();
+        return new Rect(left, top, right, bottom);
+    }
+
+    private void addClickableChildren(ViewGroup group) {
+        final int N = group.getChildCount();
+        for (int i = 0; i < N; i++) {
+            View child = group.getChildAt(i);
+            if (child.isClickable()) {
+                mClickableChildren.add(child);
+            } else if (child instanceof ViewGroup) {
+                addClickableChildren((ViewGroup) child);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mIsActive) {
+            int x = (int) event.getX();
+            int y = (int) event.getY();
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                mTouchingChild = mClickableChildren
+                        .stream()
+                        .filter(View::isAttachedToWindow)
+                        .filter(view -> mTouchableRegions.get(view).contains(x, y))
+                        .findFirst()
+                        .orElse(null);
+
+            }
+            if (mTouchingChild != null) {
+                // Translate the touch event to the view center of the touching child.
+                event.offsetLocation(mTouchingChild.getWidth() / 2 - x,
+                        mTouchingChild.getHeight() / 2 - y);
+                return mTouchingChild.getVisibility() == VISIBLE
+                        && mTouchingChild.dispatchTouchEvent(event);
+            }
+        }
+        return super.onTouchEvent(event);
+    }
+
+    public void dumpLogs(String prefix, PrintWriter pw) {
+        pw.println(prefix + "NearestTouchFrame:");
+
+        pw.println(String.format("%s\tmIsVertical=%s", prefix, mIsVertical));
+        pw.println(String.format("%s\tmTouchingChild=%s", prefix, mTouchingChild));
+        pw.println(String.format("%s\tmTouchableRegions=%s", prefix,
+                mTouchableRegions.keySet().stream()
+                        .map(key -> key + "=" + mTouchableRegions.get(key))
+                        .collect(Collectors.joining(", ", "{", "}"))));
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
index 9c7f014..9c7fdc6 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
@@ -27,7 +27,7 @@
 class NavButtonLayoutFactoryTest {
 
     private val mockDeviceProfile: DeviceProfile = mock()
-    private val mockParentButtonContainer: FrameLayout = mock()
+    private val mockParentButtonContainer: NearestTouchFrame = mock()
     private val mockNavLayout: LinearLayout = mock()
     private val mockStartContextualLayout: ViewGroup = mock()
     private val mockEndContextualLayout: ViewGroup = mock()