Implement diff haptics going into all apps

Notes:
* the reason why 0.6 -> 1 never happened was because of AllAppsSwipeController where
it had a clampToProgress. By changing lowerbound to 0 the progress actually shows fully 0->1

- composed the haptics in the constructor
- added new listener class in AATransitionController

Added featureflag

bug: 233751149
test: Manually - presubmit, ran “make -j7 Launcher3” from master branch  photo: https://screenshot.googleplex.com/8r5FZh6buzkQMjk
Change-Id: I5e1a24170fdbfdd35b8d8f24af0ec5e8586641a2
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 951be4e..0c7b48fe 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -40,6 +40,7 @@
     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.VIBRATE"/>
     <!-- for rotating surface by arbitrary degree -->
     <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index b5afda3..df95dc1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -62,6 +63,7 @@
     private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
     private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
 
+    private final VibratorWrapper mVibratorWrapper;
     private final RecentsView mRecentsView;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
@@ -82,6 +84,7 @@
         mRecentsView = l.getOverviewPanel();
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
+        mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext());
     }
 
     @Override
@@ -188,6 +191,11 @@
             // need to manually set the duration to a reasonable value.
             animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */));
         }
+        if (FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() &&
+                ((mFromState == NORMAL && mToState == ALL_APPS)
+                        || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) {
+            mVibratorWrapper.vibrateForDragBump();
+        }
     }
 
     private void onMotionPauseDetected() {
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 394a7d7..0e143c8 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -32,6 +32,7 @@
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.util.FloatProperty;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
@@ -47,17 +48,21 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.ScrimView;
 
 /**
@@ -78,6 +83,8 @@
     private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200;
 
     private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f;
+    private static final float SWIPE_DRAG_COMMIT_THRESHOLD =
+            1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -181,6 +188,7 @@
     private boolean mIsTablet;
 
     private boolean mHasScaleEffect;
+    private final VibratorWrapper mVibratorWrapper;
 
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
@@ -193,6 +201,7 @@
 
         setShiftRange(dp.allAppsShiftRange);
         mLauncher.addOnDeviceProfileChangeListener(this);
+        mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext());
     }
 
     public float getShiftRange() {
@@ -304,6 +313,11 @@
     /**
      * Creates an animation which updates the vertical transition progress and updates all the
      * dependent UI using various animation events
+     *
+     * This method also dictates where along the progress the haptics should be played. As the user
+     * scrolls up from workspace or down from AllApps, a drag haptic is being played until the
+     * commit point where it plays a commit haptic. Where we play the haptics differs when going
+     * from workspace -> allApps and vice versa.
      */
     @Override
     public void setStateWithAnimation(LauncherState toState,
@@ -332,6 +346,20 @@
             });
         }
 
+        if(FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() && config.userControlled
+                && Utilities.ATLEAST_S) {
+            if (toState == ALL_APPS) {
+                builder.addOnFrameListener(
+                        new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
+                                SWIPE_DRAG_COMMIT_THRESHOLD, 1));
+            } else {
+                builder.addOnFrameListener(
+                        new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
+                                0, SWIPE_DRAG_COMMIT_THRESHOLD));
+            }
+            builder.addEndListener(mVibratorWrapper::cancelVibrate);
+        }
+
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
             setAlphas(toState, config, builder);
@@ -349,7 +377,7 @@
 
         setAlphas(toState, config, builder);
 
-        if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
+        if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && !(Utilities.ATLEAST_S)) {
             mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
         }
@@ -487,4 +515,45 @@
             }
         }
     }
+
+    /**
+     * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start
+     * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller
+     * as it gives an accurate progress that dictates where the vibrator should vibrate.
+     * Note: once the user begins a gesture and does the commit haptic, there should not be anymore
+     * haptics played for that gesture.
+     */
+    private static class VibrationAnimatorUpdateListener implements
+            ValueAnimator.AnimatorUpdateListener {
+        private final VibratorWrapper mVibratorWrapper;
+        private final AllAppsTransitionController mController;
+        private final float mStartThreshold;
+        private final float mEndThreshold;
+        private boolean mHasCommitted;
+
+        VibrationAnimatorUpdateListener(AllAppsTransitionController controller,
+                                        VibratorWrapper vibratorWrapper, float startThreshold,
+                                        float endThreshold) {
+            mController = controller;
+            mVibratorWrapper = vibratorWrapper;
+            mStartThreshold = startThreshold;
+            mEndThreshold = endThreshold;
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            if (mHasCommitted) {
+                return;
+            }
+            float currentProgress =
+                    AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController);
+            if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) {
+                mVibratorWrapper.vibrateForDragTexture();
+            } else if (!(currentProgress == 0 || currentProgress == 1)) {
+                // This check guards against committing at the location of the start of the gesture
+                mVibratorWrapper.vibrateForDragCommit();
+                mHasCommitted = true;
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 4f4e65c..f78326d 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -375,6 +375,8 @@
             "ENABLE_LAUNCH_FROM_STAGED_APP", true,
             "Enable the ability to tap a staged app during split select to launch it in full screen"
     );
+    public static final BooleanFlag ENABLE_HAPTICS_ALL_APPS = getDebugFlag(
+            "ENABLE_HAPTICS_ALL_APPS", false, "Enables haptics opening/closing All apps");
 
     public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag(
             "ENABLE_FORCED_MONO_ICON", false,
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index bfd0e1b..a53751f 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -129,10 +129,7 @@
             Interpolators.clampToProgress(
                     Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f),
                     ALL_APPS_STATE_TRANSITION_ATOMIC, 1f);
-    public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL =
-            Interpolators.clampToProgress(
-                    Interpolators.mapToProgress(LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, 1f),
-                    ALL_APPS_STATE_TRANSITION_MANUAL, 1f);
+    public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR;
 
     // --------
 
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 932bcfc..ceba0db 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -17,7 +17,6 @@
 
 import static android.os.VibrationEffect.createPredefined;
 import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
-
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
@@ -28,12 +27,17 @@
 import android.database.ContentObserver;
 import android.media.AudioAttributes;
 import android.os.Build;
+import android.os.SystemClock;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.provider.Settings;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.anim.PendingAnimation;
+
+import java.util.function.Consumer;
 
 /**
  * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
@@ -52,6 +56,21 @@
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
 
+    private static final float DRAG_TEXTURE_SCALE = 0.03f;
+    private static final float DRAG_COMMIT_SCALE = 0.5f;
+    private static final float DRAG_BUMP_SCALE = 0.4f;
+    private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
+
+    @Nullable
+    private final VibrationEffect mDragEffect;
+    @Nullable
+    private final VibrationEffect mCommitEffect;
+    @Nullable
+    private final VibrationEffect mBumpEffect;
+
+    private long mLastDragTime;
+    private final int mThresholdUntilNextDragCallMillis;
+
     /**
      * Haptic when entering overview.
      */
@@ -62,7 +81,7 @@
 
     private boolean mIsHapticFeedbackEnabled;
 
-    public VibratorWrapper(Context context) {
+    private VibratorWrapper(Context context) {
         mVibrator = context.getSystemService(Vibrator.class);
         mHasVibrator = mVibrator.hasVibrator();
         if (mHasVibrator) {
@@ -75,12 +94,88 @@
                 }
             };
             resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
-                    false /* notifyForDescendents */, observer);
+                    false /* notifyForDescendants */, observer);
         } else {
             mIsHapticFeedbackEnabled = false;
         }
+
+        if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(
+                VibrationEffect.Composition.PRIMITIVE_LOW_TICK)) {
+
+            // Drag texture, Commit, and Bump should only be used for premium phones.
+            // Before using these haptics make sure check if the device can use it
+            VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
+            for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
+                dragEffect.addPrimitive(
+                        VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
+            }
+            mDragEffect = dragEffect.compose();
+            mCommitEffect = VibrationEffect.startComposition().addPrimitive(
+                    VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
+            mBumpEffect = VibrationEffect.startComposition().addPrimitive(
+                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose();
+            int primitiveDuration = mVibrator.getPrimitiveDurations(
+                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0];
+
+            mThresholdUntilNextDragCallMillis =
+                    DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100;
+        } else {
+            mDragEffect = null;
+            mCommitEffect = null;
+            mBumpEffect = null;
+            mThresholdUntilNextDragCallMillis = 0;
+        }
     }
 
+    /**
+     *  This is called when the user swipes to/from all apps. This is meant to be used in between
+     *  long animation progresses so that it gives a dragging texture effect. For a better
+     *  experience, this should be used in combination with vibrateForDragCommit().
+     */
+    public void vibrateForDragTexture() {
+        if (mDragEffect == null) {
+            return;
+        }
+        long currentTime = SystemClock.elapsedRealtime();
+        long elapsedTimeSinceDrag = currentTime - mLastDragTime;
+        if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) {
+            vibrate(mDragEffect);
+            mLastDragTime = currentTime;
+        }
+    }
+
+    /**
+     *  This is used when user reaches the commit threshold when swiping to/from from all apps.
+     */
+    public void vibrateForDragCommit() {
+        if (mCommitEffect != null) {
+            vibrate(mCommitEffect);
+        }
+        // resetting dragTexture timestamp to be able to play dragTexture again
+        mLastDragTime = 0;
+    }
+
+    /**
+     *  The bump haptic is used to be called at the end of a swipe and only if it the gesture is a
+     *  FLING going to/from all apps. Client can just call this method elsewhere just for the
+     *  effect.
+     */
+    public void vibrateForDragBump() {
+        if (mBumpEffect != null) {
+            vibrate(mBumpEffect);
+        }
+    }
+
+    /**
+     * 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)}.
+     */
+    public void cancelVibrate(boolean unused) {
+        UI_HELPER_EXECUTOR.execute(mVibrator::cancel);
+        // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled
+        mLastDragTime = 0;
+    }
     private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
         return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
     }