Add scroll haptics for AbsListView and ScrollView

Bug: 287914819
Test: manual
Change-Id: I25a930b3305e6ddb34574abecf054241d93ad2e2
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index d554347..cc9fe65 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -29,6 +29,7 @@
         ":telecom_flags_core_java_lib{.generated_srcjars}",
         ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}",
         ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}",
+        ":android.widget.flags-aconfig-java{.generated_srcjars}",
     ],
     // Add aconfig-annotations-lib as a dependency for the optimization
     libs: ["aconfig-annotations-lib"],
@@ -185,3 +186,17 @@
     aconfig_declarations: "android.view.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Widget
+aconfig_declarations {
+    name: "android.widget.flags-aconfig",
+    package: "android.widget.flags",
+    srcs: ["core/java/android/widget/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.widget.flags-aconfig-java",
+    aconfig_declarations: "android.widget.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 03364b6..a116542 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -53,6 +53,7 @@
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
+import android.view.HapticScrollFeedbackProvider;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -91,6 +92,7 @@
 import android.view.inputmethod.SurroundingText;
 import android.view.inspector.InspectableProperty;
 import android.view.inspector.InspectableProperty.EnumEntry;
+import android.widget.flags.Flags;
 import android.widget.RemoteViews.InteractionHandler;
 
 import com.android.internal.R;
@@ -918,6 +920,8 @@
 
     private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
 
+    private HapticScrollFeedbackProvider mHapticScrollFeedbackProvider;
+
     public AbsListView(Context context) {
         super(context);
         setupDeviceConfigProperties();
@@ -4502,10 +4506,6 @@
                 final float axisValue = (axis == -1) ? 0 : event.getAxisValue(axis);
                 final int delta = Math.round(axisValue * mVerticalScrollFactor);
                 if (delta != 0) {
-                    // Tracks whether or not we should attempt fling for this event.
-                    // Fling should not be attempted if the view is already at the limit of scroll,
-                    // since it conflicts with EdgeEffect.
-                    boolean shouldAttemptFling = true;
                     // If we're moving down, we want the top item. If we're moving up, bottom item.
                     final int motionIndex = delta > 0 ? 0 : getChildCount() - 1;
 
@@ -4518,10 +4518,12 @@
                     final int overscrollMode = getOverScrollMode();
 
                     if (!trackMotionScroll(delta, delta)) {
-                        if (shouldAttemptFling) {
-                            initDifferentialFlingHelperIfNotExists();
-                            mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
+                        if (Flags.platformWidgetHapticScrollFeedback()) {
+                            initHapticScrollFeedbackProviderIfNotExists();
+                            mHapticScrollFeedbackProvider.onScrollProgress(event, axis, delta);
                         }
+                        initDifferentialFlingHelperIfNotExists();
+                        mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
                         return true;
                     } else if (!event.isFromSource(InputDevice.SOURCE_MOUSE) && motionView != null
                             && (overscrollMode == OVER_SCROLL_ALWAYS
@@ -4530,7 +4532,13 @@
                         int motionViewRealTop = motionView.getTop();
                         float overscroll = (delta - (motionViewRealTop - motionViewPrevTop))
                                 / ((float) getHeight());
-                        if (delta > 0) {
+                        boolean hitTopLimit = delta > 0;
+                        if (Flags.platformWidgetHapticScrollFeedback()) {
+                            initHapticScrollFeedbackProviderIfNotExists();
+                            mHapticScrollFeedbackProvider.onScrollLimit(
+                                    event, axis, /* isStart= */ hitTopLimit);
+                        }
+                        if (hitTopLimit) {
                             mEdgeGlowTop.onPullDistance(overscroll, 0.5f);
                             mEdgeGlowTop.onRelease();
                         } else {
@@ -4696,6 +4704,12 @@
         }
     }
 
+    private void initHapticScrollFeedbackProviderIfNotExists() {
+        if (mHapticScrollFeedbackProvider == null) {
+            mHapticScrollFeedbackProvider = new HapticScrollFeedbackProvider(this);
+        }
+    }
+
     private void recycleVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index d330ebf..90b077b 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -33,6 +33,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.FocusFinder;
+import android.view.HapticScrollFeedbackProvider;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -47,6 +48,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.AnimationUtils;
 import android.view.inspector.InspectableProperty;
+import android.widget.flags.Flags;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -206,6 +208,8 @@
 
     private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
 
+    private HapticScrollFeedbackProvider mHapticScrollFeedbackProvider;
+
     /**
      * Sentinel value for no current active pointer.
      * Used by {@link #mActivePointerId}.
@@ -604,6 +608,12 @@
         }
     }
 
+    private void initHapticScrollFeedbackProviderIfNotExists() {
+        if (mHapticScrollFeedbackProvider == null) {
+            mHapticScrollFeedbackProvider = new HapticScrollFeedbackProvider(this);
+        }
+    }
+
     private void recycleVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
@@ -967,7 +977,7 @@
                     // Tracks whether or not we should attempt fling for this event.
                     // Fling should not be attempted if the view is already at the limit of scroll,
                     // since it conflicts with EdgeEffect.
-                    boolean shouldAttemptFling = true;
+                    boolean hitLimit = false;
                     final int range = getScrollRange();
                     int oldScrollY = mScrollY;
                     int newScrollY = oldScrollY - delta;
@@ -986,7 +996,7 @@
                             absorbed = true;
                         }
                         newScrollY = 0;
-                        shouldAttemptFling = false;
+                        hitLimit = true;
                     } else if (newScrollY > range) {
                         if (canOverscroll) {
                             mEdgeGlowBottom.onPullDistance(
@@ -996,11 +1006,21 @@
                             absorbed = true;
                         }
                         newScrollY = range;
-                        shouldAttemptFling = false;
+                        hitLimit = true;
                     }
                     if (newScrollY != oldScrollY) {
                         super.scrollTo(mScrollX, newScrollY);
-                        if (shouldAttemptFling) {
+                        if (hitLimit) {
+                            if (Flags.platformWidgetHapticScrollFeedback()) {
+                                initHapticScrollFeedbackProviderIfNotExists();
+                                mHapticScrollFeedbackProvider.onScrollLimit(
+                                        event, axis, /* isStart= */ newScrollY == 0);
+                            }
+                        } else {
+                            if (Flags.platformWidgetHapticScrollFeedback()) {
+                                initHapticScrollFeedbackProviderIfNotExists();
+                                mHapticScrollFeedbackProvider.onScrollProgress(event, axis, delta);
+                            }
                             initDifferentialFlingHelperIfNotExists();
                             mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
                         }
diff --git a/core/java/android/widget/flags/scroll_view_flags.aconfig b/core/java/android/widget/flags/scroll_view_flags.aconfig
new file mode 100644
index 0000000..f93ade2
--- /dev/null
+++ b/core/java/android/widget/flags/scroll_view_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.widget.flags"
+
+flag {
+    namespace: "widget"
+    name: "platform_widget_haptic_scroll_feedback"
+    description: "Enables haptic scroll feedback in platform widgets"
+    bug: "287914819"
+}
\ No newline at end of file