diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 12daa5993f..d7de8a7a66 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -40,6 +40,7 @@ + diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index b5afda388a..df95dc1275 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.Launcher; 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 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch 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 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch mRecentsView = l.getOverviewPanel(); mMotionPauseDetector = new MotionPauseDetector(l); mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop(); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext()); } @Override @@ -188,6 +191,11 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch // 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 4a1c3347c9..b6187244f2 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -32,6 +32,7 @@ import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS; 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.DeviceProfile.OnDeviceProfileChangeListener; 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 @@ public class AllAppsTransitionController 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 ALL_APPS_PROGRESS = new FloatProperty("allAppsProgress") { @@ -181,6 +188,7 @@ public class AllAppsTransitionController private boolean mIsTablet; private boolean mHasScaleEffect; + private final VibratorWrapper mVibratorWrapper; public AllAppsTransitionController(Launcher l) { mLauncher = l; @@ -193,6 +201,7 @@ public class AllAppsTransitionController setShiftRange(dp.allAppsShiftRange); mLauncher.addOnDeviceProfileChangeListener(this); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext()); } public float getShiftRange() { @@ -311,6 +320,11 @@ public class AllAppsTransitionController /** * 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, @@ -339,6 +353,20 @@ public class AllAppsTransitionController }); } + 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); @@ -356,7 +384,7 @@ public class AllAppsTransitionController 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); } @@ -494,4 +522,45 @@ public class AllAppsTransitionController } } } + + /** + * 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 7b2fff1967..b5879f9ce1 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -374,6 +374,8 @@ public final class FeatureFlags { "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 bfd0e1b74b..a53751fcb8 100644 --- a/src/com/android/launcher3/touch/AllAppsSwipeController.java +++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java @@ -129,10 +129,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { 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 932bcfc292..ceba0db384 100644 --- a/src/com/android/launcher3/util/VibratorWrapper.java +++ b/src/com/android/launcher3/util/VibratorWrapper.java @@ -17,7 +17,6 @@ package com.android.launcher3.util; 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.content.Context; 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 class VibratorWrapper { 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 @@ public class VibratorWrapper { 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 @@ public class VibratorWrapper { } }; 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; }