Add CUSTOM_LPNH_THRESHOLDS feature flag to customize LPNH

When this flag is enabled, new Developer Options appear which
allow you to customize the slop and timeout to invoke LPNH.

The slop is defined as a multiplier to the default edge slop
(which I discovered while looking into this - it appears to
be intended for touches along the device edge and is 50%
larger than the default touch slop; currently used by Back).

Timeout is defined in milliseconds, defaulting to 400, at least
on my device.

Bug: 301680992
Bug: 300955321
Flag: CUSTOM_LPNH_THRESHOLDS - should be no-op with default off
Test: Manual with flag on and adjusting sliders, and flag off
Change-Id: Iabc7b3706f4fdd6f0392c858b81a856e375ffd51
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
index a76eb43..cb65c53 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
@@ -24,6 +24,8 @@
 import static android.view.View.VISIBLE;
 
 import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_TIMEOUT_MS;
 import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
@@ -63,6 +65,7 @@
 import androidx.preference.SeekBarPreference;
 import androidx.preference.SwitchPreference;
 
+import com.android.launcher3.ConstantItem;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
@@ -111,6 +114,9 @@
         if (FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) {
             addAllAppsFromOverviewCatergory();
         }
+        if (FeatureFlags.CUSTOM_LPNH_THRESHOLDS.get()) {
+            addCustomLpnhCatergory();
+        }
 
         if (getActivity() != null) {
             getActivity().setTitle("Developer Options");
@@ -400,29 +406,52 @@
 
     private void addAllAppsFromOverviewCatergory() {
         PreferenceCategory category = newCategory("All Apps from Overview Config");
+        category.addPreference(createSeekBarPreference("Threshold to open All Apps from Overview",
+                105, 500, 100, ALL_APPS_OVERVIEW_THRESHOLD));
+    }
 
-        SeekBarPreference thresholdPref = new SeekBarPreference(getContext());
-        thresholdPref.setTitle("Threshold to open All Apps from Overview");
-        thresholdPref.setSingleLineTitle(false);
+    private void addCustomLpnhCatergory() {
+        PreferenceCategory category = newCategory("Long Press Nav Handle Config");
+        category.addPreference(createSeekBarPreference("Slop multiplier (applied to edge slop, "
+                        + "which is generally already 50% higher than touch slop)",
+                25, 200, 100, LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE));
+        category.addPreference(createSeekBarPreference("Trigger milliseconds",
+                100, 500, 1, LONG_PRESS_NAV_HANDLE_TIMEOUT_MS));
+    }
 
-        // These values are 100x swipe up shift value (100 = where overview sits).
-        thresholdPref.setMax(500);
-        thresholdPref.setMin(105);
-        thresholdPref.setUpdatesContinuously(true);
-        thresholdPref.setIconSpaceReserved(false);
+    /**
+     * Create a preference with text and a seek bar. Should be added to a PreferenceCategory.
+     *
+     * @param title text to show for this seek bar
+     * @param min min value for the seek bar
+     * @param max max value for the seek bar
+     * @param scale how much to divide the value to convert int to float
+     * @param launcherPref used to store the current value
+     */
+    private SeekBarPreference createSeekBarPreference(String title, int min, int max, int scale,
+            ConstantItem<Integer> launcherPref) {
+        SeekBarPreference seekBarPref = new SeekBarPreference(getContext());
+        seekBarPref.setTitle(title);
+        seekBarPref.setSingleLineTitle(false);
+
+        seekBarPref.setMax(max);
+        seekBarPref.setMin(min);
+        seekBarPref.setUpdatesContinuously(true);
+        seekBarPref.setIconSpaceReserved(false);
         // Don't directly save to shared prefs, use LauncherPrefs instead.
-        thresholdPref.setPersistent(false);
-        thresholdPref.setOnPreferenceChangeListener((preference, newValue) -> {
-            LauncherPrefs.get(getContext()).put(ALL_APPS_OVERVIEW_THRESHOLD, newValue);
-            preference.setSummary(String.valueOf((int) newValue / 100f));
+        seekBarPref.setPersistent(false);
+        seekBarPref.setOnPreferenceChangeListener((preference, newValue) -> {
+            LauncherPrefs.get(getContext()).put(launcherPref, newValue);
+            preference.setSummary(String.valueOf(scale == 1 ? newValue
+                    : (int) newValue / (float) scale));
             return true;
         });
-        int value = LauncherPrefs.get(getContext()).get(ALL_APPS_OVERVIEW_THRESHOLD);
-        thresholdPref.setValue(value);
+        int value = LauncherPrefs.get(getContext()).get(launcherPref);
+        seekBarPref.setValue(value);
         // For some reason the initial value is not triggering the summary update, so call manually.
-        thresholdPref.getOnPreferenceChangeListener().onPreferenceChange(thresholdPref, value);
+        seekBarPref.getOnPreferenceChangeListener().onPreferenceChange(seekBarPref, value);
 
-        category.addPreference(thresholdPref);
+        return seekBarPref;
     }
 
     private String toName(String action) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index addcfb8..ad9523a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -15,14 +15,19 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_TIMEOUT_MS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -37,19 +42,31 @@
     private final float mNavHandleWidth;
     private final float mScreenWidth;
 
+    // Below are only used if CUSTOM_LPNH_THRESHOLDS is enabled.
+    private final float mCustomTouchSlopSquared;
+    private final int mCustomLongPressTimeout;
+    private final Runnable mTriggerCustomLongPress = this::triggerCustomLongPress;
+    private MotionEvent mCurrentCustomDownEvent;
+
     public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor) {
         super(delegate, inputMonitor);
         mNavHandleWidth = context.getResources().getDimensionPixelSize(
                 R.dimen.navigation_home_handle_width);
         mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
+        float customSlopMultiplier =
+                LauncherPrefs.get(context).get(LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE) / 100f;
+        float customTouchSlop =
+                ViewConfiguration.get(context).getScaledEdgeSlop() * customSlopMultiplier;
+        mCustomTouchSlopSquared = customTouchSlop * customTouchSlop;
+        mCustomLongPressTimeout = LauncherPrefs.get(context).get(LONG_PRESS_NAV_HANDLE_TIMEOUT_MS);
 
         mNavHandleLongPressHandler = NavHandleLongPressHandler.newInstance(context);
 
         mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
             @Override
             public void onLongPress(MotionEvent motionEvent) {
-                if (isInArea(motionEvent.getRawX())) {
+                if (isInNavBarHorizontalArea(motionEvent.getRawX())) {
                     Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable();
                     if (longPressRunnable != null) {
                         setActive(motionEvent);
@@ -68,13 +85,51 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
-        mLongPressDetector.onTouchEvent(ev);
+        if (!FeatureFlags.CUSTOM_LPNH_THRESHOLDS.get()) {
+            mLongPressDetector.onTouchEvent(ev);
+        } else {
+            switch (ev.getAction()) {
+                case MotionEvent.ACTION_DOWN -> {
+                    if (mCurrentCustomDownEvent != null) {
+                        mCurrentCustomDownEvent.recycle();
+                    }
+                    mCurrentCustomDownEvent = MotionEvent.obtain(ev);
+                    if (isInNavBarHorizontalArea(ev.getRawX())) {
+                        MAIN_EXECUTOR.getHandler().postDelayed(mTriggerCustomLongPress,
+                                mCustomLongPressTimeout);
+                    }
+                }
+                case MotionEvent.ACTION_MOVE -> {
+                    double touchDeltaSquared =
+                            Math.pow(ev.getX() - mCurrentCustomDownEvent.getX(), 2)
+                            + Math.pow(ev.getY() - mCurrentCustomDownEvent.getY(), 2);
+                    if (touchDeltaSquared > mCustomTouchSlopSquared) {
+                        cancelCustomLongPress();
+                    }
+                }
+                case MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> cancelCustomLongPress();
+            }
+        }
+
         if (mState != STATE_ACTIVE) {
             mDelegate.onMotionEvent(ev);
         }
     }
 
-    protected boolean isInArea(float x) {
+    private void triggerCustomLongPress() {
+        Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable();
+        if (longPressRunnable != null) {
+            setActive(mCurrentCustomDownEvent);
+
+            MAIN_EXECUTOR.post(longPressRunnable);
+        }
+    }
+
+    private void cancelCustomLongPress() {
+        MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerCustomLongPress);
+    }
+
+    private boolean isInNavBarHorizontalArea(float x) {
         float areaFromMiddle = mNavHandleWidth / 2.0f;
         float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
 
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index ab41a31..8d19040 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -66,6 +66,10 @@
     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 4db2fca..324453e 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -20,6 +20,7 @@
 import android.content.SharedPreferences
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import android.util.Log
+import android.view.ViewConfiguration
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN
 import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
@@ -309,6 +310,20 @@
                 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
+            )
+        @JvmField
+        val LONG_PRESS_NAV_HANDLE_TIMEOUT_MS =
+            nonRestorableItem(
+                LauncherAppState.KEY_LONG_PRESS_NAV_HANDLE_TIMEOUT_MS,
+                ViewConfiguration.getLongPressTimeout(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
+        @JvmField
         val THEMED_ICONS =
             backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
         @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 0b8bdeb..ec784e4 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -119,6 +119,10 @@
             getDebugFlag(275132633, "ENABLE_ALL_APPS_FROM_OVERVIEW", DISABLED,
                     "Allow entering All Apps from Overview (e.g. long swipe up from app)");
 
+    public static final BooleanFlag CUSTOM_LPNH_THRESHOLDS =
+            getDebugFlag(301680992, "CUSTOM_LPNH_THRESHOLDS", DISABLED,
+                    "Add dev options to customize the LPNH trigger slop and milliseconds");
+
     public static final BooleanFlag ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS = getReleaseFlag(
             270394468, "ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS", ENABLED,
             "Enable option to show keyboard when going to all-apps");