Adds haptic build-up during LPNH behind feature flag

Flag: Omnient flags + ENABLE_SEARCH_HAPTIC_HINT
Test: Manual
Bug: 303023676
Change-Id: Ic43d1f34a05622328add6727f36406ce75d5a115
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 8d19040..34bfdb7 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -64,12 +64,6 @@
 public class LauncherAppState implements SafeCloseable {
 
     public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
-    public static final String KEY_ICON_STATE = "pref_icon_shape_path";
-    public static final String KEY_ALL_APPS_OVERVIEW_THRESHOLD = "pref_all_apps_overview_threshold";
-    public static final String KEY_LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE =
-            "pref_long_press_nav_handle_slop_multiplier";
-    public static final String KEY_LONG_PRESS_NAV_HANDLE_TIMEOUT_MS =
-            "pref_long_press_nav_handle_timeout_ms";
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 9a0d02a..c6a9283 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -297,31 +297,59 @@
         @JvmField
         val ICON_STATE =
             nonRestorableItem(
-                LauncherAppState.KEY_ICON_STATE,
+                    "pref_icon_shape_path",
                 "",
                 EncryptionType.MOVE_TO_DEVICE_PROTECTED
             )
         @JvmField
         val ALL_APPS_OVERVIEW_THRESHOLD =
             nonRestorableItem(
-                LauncherAppState.KEY_ALL_APPS_OVERVIEW_THRESHOLD,
+                    "pref_all_apps_overview_threshold",
                 180,
                 EncryptionType.MOVE_TO_DEVICE_PROTECTED
             )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE =
-            nonRestorableItem(
-                LauncherAppState.KEY_LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE,
-                100,
-                EncryptionType.MOVE_TO_DEVICE_PROTECTED
-            )
+                nonRestorableItem(
+                        "pref_long_press_nav_handle_slop_multiplier",
+                        100,
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_TIMEOUT_MS =
-            nonRestorableItem(
-                LauncherAppState.KEY_LONG_PRESS_NAV_HANDLE_TIMEOUT_MS,
-                ViewConfiguration.getLongPressTimeout(),
-                EncryptionType.MOVE_TO_DEVICE_PROTECTED
-            )
+                nonRestorableItem(
+                        "pref_long_press_nav_handle_timeout_ms",
+                        ViewConfiguration.getLongPressTimeout(),
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
+        @JvmField
+        val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT =
+                nonRestorableItem(
+                        "pref_long_press_nav_handle_haptic_hint_start_scale_percent",
+                        0,
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
+        @JvmField
+        val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT =
+                nonRestorableItem(
+                        "pref_long_press_nav_handle_haptic_hint_end_scale_percent",
+                        50,
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
+        @JvmField
+        val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT =
+                nonRestorableItem(
+                        "pref_long_press_nav_handle_haptic_hint_scale_exponent",
+                        2,
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
+        @JvmField
+        val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS =
+                nonRestorableItem(
+                        "pref_long_press_nav_handle_haptic_hint_iterations",
+                        40,
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
         @JvmField
         val THEMED_ICONS =
             backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index c09a5b9..278e4e5 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -373,7 +373,9 @@
                         new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
                                 0, SWIPE_DRAG_COMMIT_THRESHOLD));
             }
-            builder.addEndListener(mVibratorWrapper::cancelVibrate);
+            builder.addEndListener((unused) -> {
+                mVibratorWrapper.cancelVibrate();
+            });
         }
 
         float targetProgress = toState.getVerticalProgress(mLauncher);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5b44069..3f3b3ce 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -262,6 +262,10 @@
             getReleaseFlag(282993230, "ENABLE_LONG_PRESS_NAV_HANDLE", TEAMFOOD,
                     "Enables long pressing on the bottom bar nav handle to trigger events.");
 
+    public static final BooleanFlag ENABLE_SEARCH_HAPTIC_HINT =
+            getReleaseFlag(303023676, "ENABLE_SEARCH_HAPTIC_HINT", TEAMFOOD,
+                    "Enables haptic hint when long pressing on the bottom bar nav handle.");
+
     // TODO(Block 17): Clean up flags
     public static final BooleanFlag ENABLE_TASKBAR_PINNING = getDebugFlag(270396583,
             "ENABLE_TASKBAR_PINNING", DISABLED,
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 91945ca..80a9bae 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -18,6 +18,10 @@
 import static android.os.VibrationEffect.createPredefined;
 import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
 
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
@@ -35,10 +39,9 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.PendingAnimation;
-
-import java.util.function.Consumer;
+import com.android.launcher3.config.FeatureFlags;
 
 /**
  * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
@@ -70,7 +73,7 @@
     private final VibrationEffect mBumpEffect;
 
     @Nullable
-    private final VibrationEffect mAssistEffect;
+    private final VibrationEffect mSearchEffect;
 
     private long mLastDragTime;
     private final int mThresholdUntilNextDragCallMillis;
@@ -80,12 +83,14 @@
      */
     public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
 
+    private final Context mContext;
     private final Vibrator mVibrator;
     private final boolean mHasVibrator;
 
     private boolean mIsHapticFeedbackEnabled;
 
     private VibratorWrapper(Context context) {
+        mContext = context;
         mVibrator = context.getSystemService(Vibrator.class);
         mHasVibrator = mVibrator.hasVibrator();
         if (mHasVibrator) {
@@ -133,14 +138,20 @@
         if (Utilities.ATLEAST_R && mVibrator.areAllPrimitivesSupported(
                 VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
                 VibrationEffect.Composition.PRIMITIVE_TICK)) {
-            // quiet ramp, short pause, then sharp tick
-            mAssistEffect = VibrationEffect.startComposition()
-                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
-                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
-                    .compose();
+            if (FeatureFlags.ENABLE_SEARCH_HAPTIC_HINT.get()) {
+                mSearchEffect = VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f)
+                        .compose();
+            } else {
+                // quiet ramp, short pause, then sharp tick
+                mSearchEffect = VibrationEffect.startComposition()
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
+                        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
+                        .compose();
+            }
         } else {
             // fallback for devices without composition support
-            mAssistEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK);
+            mSearchEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK);
         }
     }
 
@@ -184,20 +195,10 @@
     }
 
     /**
-     * The assist haptic is used to be called when an assistant is invoked
-     */
-    public void vibrateForAssist() {
-        if (mAssistEffect != null) {
-            vibrate(mAssistEffect);
-        }
-    }
-
-    /**
      * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For
-     * example, when no animation is happening but a vibrator happens to be vibrating still. Need
-     * boolean parameter for {@link PendingAnimation#addEndListener(Consumer)}.
+     * example, when no animation is happening but a vibrator happens to be vibrating still.
      */
-    public void cancelVibrate(boolean unused) {
+    public void cancelVibrate() {
         UI_HELPER_EXECUTOR.execute(mVibrator::cancel);
         // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled
         mLastDragTime = 0;
@@ -233,4 +234,37 @@
             });
         }
     }
+
+    /** Indicates that search has been invoked. */
+    public void vibrateForSearch() {
+        if (mSearchEffect != null) {
+            vibrate(mSearchEffect);
+        }
+    }
+
+    /** Indicates that search will be invoked if the current gesture is maintained. */
+    public void vibrateForSearchHint() {
+        if (FeatureFlags.ENABLE_SEARCH_HAPTIC_HINT.get() && Utilities.ATLEAST_S
+                && mVibrator.areAllPrimitivesSupported(
+                VibrationEffect.Composition.PRIMITIVE_LOW_TICK)) {
+            float startScale = LauncherPrefs.get(mContext).get(
+                    LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT) / 100f;
+            float endScale = LauncherPrefs.get(mContext).get(
+                    LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT) / 100f;
+            int scaleExponent = LauncherPrefs.get(mContext).get(
+                    LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT);
+            int iterations = LauncherPrefs.get(mContext).get(
+                    LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS);
+
+            VibrationEffect.Composition composition = VibrationEffect.startComposition();
+            for (int i = 0; i < iterations; i++) {
+                float t = i / (iterations - 1f);
+                float scale = (float) Math.pow((1 - t) * startScale + t * endScale,
+                        scaleExponent);
+                composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale);
+            }
+
+            vibrate(composition.compose());
+        }
+    }
 }