Add Arrows to Folder PageIndicatorDots for Accessibility Purposes.

Bug: 383974843
Test: Verified via go/web-hv that the hitbox sizes were correct. Measured the width and height within the folder for 1 through 7 pages, and everything looked great.Change title worked, and pressing on arrows to change page worked great.
Flag: com.android.launcher3.enable_launcher_visual_refresh

Change-Id: Ic095b2abe330544882467fb4803724e8d50a1798
diff --git a/quickstep/res/drawable/ic_chevron_end.xml b/res/drawable/ic_chevron_end.xml
similarity index 100%
rename from quickstep/res/drawable/ic_chevron_end.xml
rename to res/drawable/ic_chevron_end.xml
diff --git a/quickstep/res/drawable/ic_chevron_start.xml b/res/drawable/ic_chevron_start.xml
similarity index 100%
rename from quickstep/res/drawable/ic_chevron_start.xml
rename to res/drawable/ic_chevron_start.xml
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index bebe1a4..0963421 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -47,6 +47,7 @@
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pageindicators.Direction;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
 import com.android.launcher3.util.Thunk;
@@ -129,6 +130,8 @@
     public void setFolder(Folder folder) {
         mFolder = folder;
         mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
+        mPageIndicator.setArrowClickListener(direction -> snapToPageImmediately(
+                (Direction.END == direction) ? mCurrentPage + 1 : mCurrentPage - 1));
         initParentViews(folder);
     }
 
diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java
index 0640bf3..a6f76c4 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicator.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.pageindicators;
 
+import java.util.function.Consumer;
+
 /**
  * Base class for a page indicator.
  */
@@ -27,6 +29,14 @@
     void setMarkersCount(int numMarkers);
 
     /**
+     * This is only going to be used by the FolderPagedView's PageIndicator. A refactor is planned
+     * to separate the two purposes of this class, but in the meantime, this indicator will serve to
+     * let the folder snap to the page of its click, and also tell the PageIndicator not to draw
+     * arrows if the click listener is null (at least until after this is refactored).
+     */
+    void setArrowClickListener(Consumer<Direction> listener);
+
+    /**
      * Sets a flag indicating whether to pause scroll.
      * <p>Should be set to {@code true} while the screen is binding or new data is being applied,
      * and to {@code false} once done. This prevents animation conflicts due to scrolling during
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt b/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt
new file mode 100644
index 0000000..970d210
--- /dev/null
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2025 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.pageindicators
+
+interface PageIndicatorArrowClickListener {
+    fun onArrowClick(direction: Direction)
+}
+
+enum class Direction {
+    END,
+    START,
+}
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 37f5189..f0c9bd9 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -32,11 +32,13 @@
 import android.graphics.Paint.Style;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.IntProperty;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewOutlineProvider;
@@ -51,6 +53,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Themes;
 
+import java.util.function.Consumer;
+
 /**
  * {@link PageIndicator} which shows dots per page. The active page is shown with the current
  * accent color.
@@ -68,6 +72,11 @@
     private static final int ENTER_ANIMATION_STAGGERED_DELAY = 150;
     private static final int ENTER_ANIMATION_DURATION = 400;
 
+    private static final int LARGE_HEIGHT_MULTIPLIER = 12;
+    private static final int SMALL_HEIGHT_MULTIPLIER = 4;
+    private static final int LARGE_WIDTH_MULTIPLIER = 5;
+    private static final int SMALL_WIDTH_MULTIPLIER = 3;
+
     private static final int PAGE_INDICATOR_ALPHA = 255;
     private static final int DOT_ALPHA = 128;
     private static final float DOT_ALPHA_FRACTION = 0.5f;
@@ -75,6 +84,7 @@
     private static final int VISIBLE_ALPHA = 255;
     private static final int INVISIBLE_ALPHA = 0;
     private Paint mPaginationPaint;
+    private Consumer<Direction> mOnArrowClickListener;
 
     // This value approximately overshoots to 1.5 times the original size.
     private static final float ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f;
@@ -99,23 +109,27 @@
 
     private static final IntProperty<PageIndicatorDots> PAGINATION_ALPHA =
             new IntProperty<PageIndicatorDots>("pagination_alpha") {
-        @Override
-        public Integer get(PageIndicatorDots obj) {
-            return obj.mPaginationPaint.getAlpha();
-        }
+                @Override
+                public Integer get(PageIndicatorDots obj) {
+                    return obj.mPaginationPaint.getAlpha();
+                }
 
-        @Override
-        public void setValue(PageIndicatorDots obj, int alpha) {
-            obj.mPaginationPaint.setAlpha(alpha);
-            obj.invalidate();
-        }
-    };
+                @Override
+                public void setValue(PageIndicatorDots obj, int alpha) {
+                    obj.mPaginationPaint.setAlpha(alpha);
+                    obj.invalidate();
+                }
+            };
 
     private final Handler mDelayedPaginationFadeHandler = new Handler(Looper.getMainLooper());
     private final float mDotRadius;
     private final float mGapWidth;
     private final float mCircleGap;
     private final boolean mIsRtl;
+    private final VectorDrawable mArrowEnd;
+    private final VectorDrawable mArrowStart;
+    private final Rect mArrowEndBounds = new Rect();
+    private final Rect mArrowStartBounds = new Rect();
 
     private int mNumPages;
     private int mActivePage;
@@ -167,6 +181,8 @@
                 : DOT_GAP_FACTOR * mDotRadius;
         setOutlineProvider(new MyOutlineProver());
         mIsRtl = Utilities.isRtl(getResources());
+        mArrowEnd = (VectorDrawable) getResources().getDrawable(R.drawable.ic_chevron_end);
+        mArrowStart = (VectorDrawable) getResources().getDrawable(R.drawable.ic_chevron_start);
     }
 
     @Override
@@ -405,6 +421,11 @@
     }
 
     @Override
+    public void setArrowClickListener(Consumer<Direction> listener) {
+        mOnArrowClickListener = listener;
+    }
+
+    @Override
     public void setPauseScroll(boolean pause, boolean isTwoPanels) {
         mIsTwoPanels = isTwoPanels;
 
@@ -419,11 +440,16 @@
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // TODO(b/394355070): Verify Folder Entry Animation works correctly with visual updates
-        // Add extra spacing of mDotRadius on all sides so than entry animation could be run.
+        // Add extra spacing of mDotRadius on all sides so than entry animation could be run
+        // and so the hitboxes of arrows can be clicked easier.
         int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ?
-                MeasureSpec.getSize(widthMeasureSpec) : (int) ((mNumPages * 3 + 2) * mDotRadius);
+                MeasureSpec.getSize(widthMeasureSpec)
+                : (int) ((mNumPages * ((enableLauncherVisualRefresh())
+                        ? LARGE_WIDTH_MULTIPLIER : SMALL_WIDTH_MULTIPLIER) + 2) * mDotRadius);
         int height = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
-                ? MeasureSpec.getSize(heightMeasureSpec) : (int) (4 * mDotRadius);
+                ? MeasureSpec.getSize(heightMeasureSpec)
+                : (int) (((enableLauncherVisualRefresh())
+                        ? LARGE_HEIGHT_MULTIPLIER : SMALL_HEIGHT_MULTIPLIER) * mDotRadius);
         setMeasuredDimension(width, height);
     }
 
@@ -475,6 +501,18 @@
                 float bounceProgress = (posDif > 1) ? posDif - 1 : 0;
                 float bounceAdjustment = Math.abs(mCurrentPosition - boundedPosition) * diameter;
 
+                if (mOnArrowClickListener != null && boundedPosition >= 1) {
+                    // Here we draw the Left Arrow
+                    mArrowStart.setAlpha(alpha);
+                    int size = (int) (mGapWidth * 4);
+                    mArrowStartBounds.left = (int) (sTempRect.left - mGapWidth - size);
+                    mArrowStartBounds.top = (int) (y - size / 2);
+                    mArrowStartBounds.right = (int) (sTempRect.left - mGapWidth);
+                    mArrowStartBounds.bottom = (int) (y + size / 2);
+                    mArrowStart.setBounds(mArrowStartBounds);
+                    mArrowStart.draw(canvas);
+                }
+
                 // Here we draw the dots, one at a time from the left-most dot to the right-most dot
                 // 1.0 => 000000 000000111111 000000
                 // 1.3 => 000000 0000001111 11000000
@@ -520,6 +558,18 @@
                     // TODO(b/394355070) Verify RTL experience works correctly with visual updates
                     sTempRect.left = sTempRect.right + mGapWidth;
                 }
+
+                if (mOnArrowClickListener != null && boundedPosition <= mNumPages - 2) {
+                    // Here we draw the Right Arrow
+                    mArrowEnd.setAlpha(alpha);
+                    int size = (int) (mGapWidth * 4);
+                    mArrowEndBounds.left = (int) sTempRect.left;
+                    mArrowEndBounds.top = (int) (y - size / 2);
+                    mArrowEndBounds.right = (int) (int) (sTempRect.left + size);
+                    mArrowEndBounds.bottom = (int) (y + size / 2);
+                    mArrowEnd.setBounds(mArrowEndBounds);
+                    mArrowEnd.draw(canvas);
+                }
             } else {
                 // Here we draw the dots
                 mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION));
@@ -538,6 +588,34 @@
         }
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mOnArrowClickListener == null) {
+            // No - Op. Don't care about touch events
+        } else if (withinExpandedBounds(mArrowStartBounds, ev)) {
+            mOnArrowClickListener.accept(Direction.START);
+        } else if (withinExpandedBounds(mArrowEndBounds, ev)) {
+            mOnArrowClickListener.accept(Direction.END);
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    // For larger Touch box
+    private boolean withinExpandedBounds(Rect rect, MotionEvent ev) {
+        Rect scaledRect = new Rect();
+        scaledRect.set(rect);
+
+        float verticalAdjustment = (scaledRect.bottom - scaledRect.top) * 2;
+        scaledRect.top -= verticalAdjustment;
+        scaledRect.bottom += verticalAdjustment;
+
+        float horizontalAdjustment = (scaledRect.right - scaledRect.left) * 2;
+        scaledRect.left -= horizontalAdjustment;
+        scaledRect.right += horizontalAdjustment;
+
+        return scaledRect.contains((int) ev.getX(), (int) ev.getY());
+    }
+
     private RectF getActiveRect() {
         float startCircle = (int) mCurrentPosition;
         float delta = mCurrentPosition - startCircle;
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index e94f3a0..185c3ee 100644
--- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -26,9 +26,12 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.launcher3.pageindicators.Direction;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.views.ActivityContext;
 
+import java.util.function.Consumer;
+
 /**
  * Supports two indicator colors, dedicated for personal and work tabs.
  */
@@ -78,6 +81,11 @@
     }
 
     @Override
+    public void setArrowClickListener(Consumer<Direction> listener) {
+        // No-Op. All Apps doesn't need accessibility arrows for single click navigation.
+    }
+
+    @Override
     public boolean hasOverlappingRendering() {
         return false;
     }