From 25cba56e6dc12a3d215f04ae44bf05af693c5c8b Mon Sep 17 00:00:00 2001 From: Juan Sebastian Martinez Date: Thu, 20 Jul 2023 16:52:16 +0000 Subject: [PATCH] Using View.performHapticFeedback on Back Panel Controller when the back gesture is handled Migration towards the new one-way API that can trigger haptic feedback from the UI thread. Calls to cancel vibrations and vibrations triggered in a separate thread (old API) have been replaced by calls to performHapticFeedback directly from the UI thread. Delaying cancellations and delayed haptics is no longer necessary. The migration is controlled by a feature flag at the moment. Test: Tests that handled back gesture-committed and -cancelled now verify different method calls from the VibratorHelper depending on the feature flag. Bug: 245528624 Change-Id: I5e3b8d67f26cd7002d2372f116ec2601053c4328 --- .../gestural/BackPanelController.kt | 664 +++++++++--------- .../gestural/BackPanelControllerTest.kt | 69 +- 2 files changed, 418 insertions(+), 315 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index 77e2847cbe76b..c4749e0938545 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -26,6 +26,7 @@ import android.os.VibrationEffect import android.util.Log import android.util.MathUtils import android.view.Gravity +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.VelocityTracker import android.view.ViewConfiguration @@ -36,6 +37,8 @@ import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -76,27 +79,24 @@ private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f private const val POP_ON_INACTIVE_VELOCITY = -1.5f internal val VIBRATE_ACTIVATED_EFFECT = - VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) internal val VIBRATE_DEACTIVATED_EFFECT = - VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) private const val DEBUG = false -class BackPanelController internal constructor( - context: Context, - private val windowManager: WindowManager, - private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, - private val vibratorHelper: VibratorHelper, - private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker -) : ViewController( - BackPanel( - context, - latencyTracker - ) -), NavigationEdgeBackPlugin { +class BackPanelController +internal constructor( + context: Context, + private val windowManager: WindowManager, + private val viewConfiguration: ViewConfiguration, + @Main private val mainHandler: Handler, + private val vibratorHelper: VibratorHelper, + private val configurationController: ConfigurationController, + private val latencyTracker: LatencyTracker, + private val featureFlags: FeatureFlags +) : ViewController(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { /** * Injectable instance to create a new BackPanelController. @@ -104,34 +104,37 @@ class BackPanelController internal constructor( * Necessary because EdgeBackGestureHandler sometimes needs to create new instances of * BackPanelController, and we need to match EdgeBackGestureHandler's context. */ - class Factory @Inject constructor( - private val windowManager: WindowManager, - private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, - private val vibratorHelper: VibratorHelper, - private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker + class Factory + @Inject + constructor( + private val windowManager: WindowManager, + private val viewConfiguration: ViewConfiguration, + @Main private val mainHandler: Handler, + private val vibratorHelper: VibratorHelper, + private val configurationController: ConfigurationController, + private val latencyTracker: LatencyTracker, + private val featureFlags: FeatureFlags ) { - /** Construct a [BackPanelController]. */ + /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { - val backPanelController = BackPanelController( + val backPanelController = + BackPanelController( context, windowManager, viewConfiguration, mainHandler, vibratorHelper, configurationController, - latencyTracker - ) + latencyTracker, + featureFlags + ) backPanelController.init() return backPanelController } } - @VisibleForTesting - internal var params: EdgePanelParams = EdgePanelParams(resources) - @VisibleForTesting - internal var currentState: GestureState = GestureState.GONE + @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources) + @VisibleForTesting internal var currentState: GestureState = GestureState.GONE private var previousState: GestureState = GestureState.GONE // Screen attributes @@ -167,7 +170,6 @@ class BackPanelController internal constructor( private val elapsedTimeSinceEntry get() = SystemClock.uptimeMillis() - gestureEntryTime - private var pastThresholdWhileEntryOrInactiveTime = 0L private var entryToActiveDelay = 0F private val entryToActiveDelayCalculation = { @@ -206,24 +208,25 @@ class BackPanelController internal constructor( COMMITTED, /* back action currently cancelling, arrow soon to be GONE */ - CANCELLED; + CANCELLED } /** * Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The * runnable is not called if the animation is cancelled */ - inner class DelayedOnAnimationEndListener internal constructor( - private val handler: Handler, - private val runnableDelay: Long, - val runnable: Runnable, + inner class DelayedOnAnimationEndListener + internal constructor( + private val handler: Handler, + private val runnableDelay: Long, + val runnable: Runnable, ) : DynamicAnimation.OnAnimationEndListener { override fun onAnimationEnd( - animation: DynamicAnimation<*>, - canceled: Boolean, - value: Float, - velocity: Float + animation: DynamicAnimation<*>, + canceled: Boolean, + value: Float, + velocity: Float ) { animation.removeEndListener(this) @@ -239,45 +242,43 @@ class BackPanelController internal constructor( internal fun run() = runnable.run() } - private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) { - updateArrowState(GestureState.COMMITTED) - } - + private val onEndSetCommittedStateListener = + DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) } private val onEndSetGoneStateListener = - DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) { - cancelFailsafe() - updateArrowState(GestureState.GONE) - } - - private val onAlphaEndSetGoneStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) { - updateRestingArrowDimens() - if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) { - scheduleFailsafe() + DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) { + cancelFailsafe() + updateArrowState(GestureState.GONE) + } + + private val onAlphaEndSetGoneStateListener = + DelayedOnAnimationEndListener(mainHandler, 0L) { + updateRestingArrowDimens() + if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) { + scheduleFailsafe() + } } - } // Minimum of the screen's width or the predefined threshold private var fullyStretchedThreshold = 0f - /** - * Used for initialization and configuration changes - */ + /** Used for initialization and configuration changes */ private fun updateConfiguration() { params.update(resources) mView.updateArrowPaint(params.arrowThickness) minFlingDistance = viewConfiguration.scaledTouchSlop * 3 } - private val configurationListener = object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - updateConfiguration() - } + private val configurationListener = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateConfiguration() + } - override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { - updateArrowDirection(isLayoutRtl) + override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { + updateArrowDirection(isLayoutRtl) + } } - } override fun onViewAttached() { updateConfiguration() @@ -320,8 +321,9 @@ class BackPanelController internal constructor( MotionEvent.ACTION_UP -> { when (currentState) { GestureState.ENTRY -> { - if (isFlungAwayFromEdge(endX = event.x) || - previousXTranslation > params.staticTriggerThreshold + if ( + isFlungAwayFromEdge(endX = event.x) || + previousXTranslation > params.staticTriggerThreshold ) { updateArrowState(GestureState.FLUNG) } else { @@ -342,14 +344,16 @@ class BackPanelController internal constructor( } } GestureState.ACTIVE -> { - if (previousState == GestureState.ENTRY && - elapsedTimeSinceEntry - < MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING + if ( + previousState == GestureState.ENTRY && + elapsedTimeSinceEntry < + MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING ) { updateArrowState(GestureState.FLUNG) - } else if (previousState == GestureState.INACTIVE && - elapsedTimeSinceInactive - < MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING + } else if ( + previousState == GestureState.INACTIVE && + elapsedTimeSinceInactive < + MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING ) { // A delay is added to allow the background to transition back to ACTIVE // since it was briefly in INACTIVE. Without this delay, setting it @@ -390,10 +394,10 @@ class BackPanelController internal constructor( } /** - * Returns false until the current gesture exceeds the touch slop threshold, - * and returns true thereafter (we reset on the subsequent back gesture). - * The moment it switches from false -> true is important, - * because that's when we switch state, from GONE -> ENTRY. + * Returns false until the current gesture exceeds the touch slop threshold, and returns true + * thereafter (we reset on the subsequent back gesture). The moment it switches from false -> + * true is important, because that's when we switch state, from GONE -> ENTRY. + * * @return whether the current gesture has moved past a minimum threshold. */ private fun dragSlopExceeded(curX: Float, startX: Float): Boolean { @@ -416,7 +420,8 @@ class BackPanelController internal constructor( val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold when (currentState) { GestureState.ENTRY -> { - if (isPastThresholdToActive( + if ( + isPastThresholdToActive( isPastThreshold = isPastStaticThreshold, dynamicDelay = entryToActiveDelayCalculation ) @@ -428,8 +433,10 @@ class BackPanelController internal constructor( val isPastDynamicReactivationThreshold = totalTouchDeltaInactive >= params.reactivationTriggerThreshold - if (isPastThresholdToActive( - isPastThreshold = isPastStaticThreshold && + if ( + isPastThresholdToActive( + isPastThreshold = + isPastStaticThreshold && isPastDynamicReactivationThreshold && isWithinYActivationThreshold, delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION @@ -489,19 +496,19 @@ class BackPanelController internal constructor( // Add a slop to to prevent small jitters when arrow is at edge in // emitting small values that cause the arrow to poke out slightly val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat() - totalTouchDeltaInactive = totalTouchDeltaInactive - .plus(xDelta) - .coerceAtLeast(minimumDelta) + totalTouchDeltaInactive = + totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta) } updateArrowStateOnMove(yTranslation, xTranslation) - val gestureProgress = when (currentState) { - GestureState.ACTIVE -> fullScreenProgress(xTranslation) - GestureState.ENTRY -> staticThresholdProgress(xTranslation) - GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive) - else -> null - } + val gestureProgress = + when (currentState) { + GestureState.ACTIVE -> fullScreenProgress(xTranslation) + GestureState.ENTRY -> staticThresholdProgress(xTranslation) + GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive) + else -> null + } gestureProgress?.let { when (currentState) { @@ -517,27 +524,30 @@ class BackPanelController internal constructor( } private fun setArrowStrokeAlpha(gestureProgress: Float?) { - val strokeAlphaProgress = when (currentState) { - GestureState.ENTRY -> gestureProgress - GestureState.INACTIVE -> gestureProgress - GestureState.ACTIVE, - GestureState.FLUNG, - GestureState.COMMITTED -> 1f - GestureState.CANCELLED, - GestureState.GONE -> 0f - } + val strokeAlphaProgress = + when (currentState) { + GestureState.ENTRY -> gestureProgress + GestureState.INACTIVE -> gestureProgress + GestureState.ACTIVE, + GestureState.FLUNG, + GestureState.COMMITTED -> 1f + GestureState.CANCELLED, + GestureState.GONE -> 0f + } - val indicator = when (currentState) { - GestureState.ENTRY -> params.entryIndicator - GestureState.INACTIVE -> params.preThresholdIndicator - GestureState.ACTIVE -> params.activeIndicator - else -> params.preThresholdIndicator - } + val indicator = + when (currentState) { + GestureState.ENTRY -> params.entryIndicator + GestureState.INACTIVE -> params.preThresholdIndicator + GestureState.ACTIVE -> params.activeIndicator + else -> params.preThresholdIndicator + } strokeAlphaProgress?.let { progress -> - indicator.arrowDimens.alphaSpring?.get(progress)?.takeIf { it.isNewState }?.let { - mView.popArrowAlpha(0f, it.value) - } + indicator.arrowDimens.alphaSpring + ?.get(progress) + ?.takeIf { it.isNewState } + ?.let { mView.popArrowAlpha(0f, it.value) } } } @@ -546,15 +556,16 @@ class BackPanelController internal constructor( val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f val rubberbandAmount = 15f val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount)) - val yPosition = params.verticalTranslationInterpolator.getInterpolation(yProgress) * + val yPosition = + params.verticalTranslationInterpolator.getInterpolation(yProgress) * maxYOffset * sign(yOffset) mView.animateVertically(yPosition) } /** - * Tracks the relative position of the drag from the time after the arrow is activated until - * the arrow is fully stretched (between 0.0 - 1.0f) + * Tracks the relative position of the drag from the time after the arrow is activated until the + * arrow is fully stretched (between 0.0 - 1.0f) */ private fun fullScreenProgress(xTranslation: Float): Float { val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold @@ -575,35 +586,32 @@ class BackPanelController internal constructor( private fun stretchActiveBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = params.horizontalTranslationInterpolator - .getInterpolation(progress), - arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), - backgroundWidthStretchAmount = params.activeWidthInterpolator - .getInterpolation(progress), - backgroundAlphaStretchAmount = 1f, - backgroundHeightStretchAmount = 1f, - arrowAlphaStretchAmount = 1f, - edgeCornerStretchAmount = 1f, - farCornerStretchAmount = 1f, - fullyStretchedDimens = params.fullyStretchedIndicator + horizontalTranslationStretchAmount = + params.horizontalTranslationInterpolator.getInterpolation(progress), + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = + params.activeWidthInterpolator.getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + backgroundHeightStretchAmount = 1f, + arrowAlphaStretchAmount = 1f, + edgeCornerStretchAmount = 1f, + farCornerStretchAmount = 1f, + fullyStretchedDimens = params.fullyStretchedIndicator ) } private fun stretchEntryBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = 0f, - arrowStretchAmount = params.arrowAngleInterpolator - .getInterpolation(progress), - backgroundWidthStretchAmount = params.entryWidthInterpolator - .getInterpolation(progress), - backgroundHeightStretchAmount = params.heightInterpolator - .getInterpolation(progress), - backgroundAlphaStretchAmount = 1f, - arrowAlphaStretchAmount = params.entryIndicator.arrowDimens - .alphaInterpolator?.get(progress)?.value ?: 0f, - edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), - farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), - fullyStretchedDimens = params.preThresholdIndicator + horizontalTranslationStretchAmount = 0f, + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress), + backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + arrowAlphaStretchAmount = + params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f, + edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), + farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), + fullyStretchedDimens = params.preThresholdIndicator ) } @@ -612,31 +620,32 @@ class BackPanelController internal constructor( val interpolator = run { val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop if (isPastSlop) { - if (totalTouchDeltaInactive > 0) { - params.entryWidthInterpolator + if (totalTouchDeltaInactive > 0) { + params.entryWidthInterpolator + } else { + params.entryWidthTowardsEdgeInterpolator + } } else { - params.entryWidthTowardsEdgeInterpolator + previousPreThresholdWidthInterpolator } - } else { - previousPreThresholdWidthInterpolator - }.also { previousPreThresholdWidthInterpolator = it } + .also { previousPreThresholdWidthInterpolator = it } } return interpolator.getInterpolation(progress).coerceAtLeast(0f) } private fun stretchInactiveBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = 0f, - arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), - backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress), - backgroundHeightStretchAmount = params.heightInterpolator - .getInterpolation(progress), - backgroundAlphaStretchAmount = 1f, - arrowAlphaStretchAmount = params.preThresholdIndicator.arrowDimens - .alphaInterpolator?.get(progress)?.value ?: 0f, - edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), - farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), - fullyStretchedDimens = params.preThresholdIndicator + horizontalTranslationStretchAmount = 0f, + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress), + backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + arrowAlphaStretchAmount = + params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value + ?: 0f, + edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), + farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), + fullyStretchedDimens = params.preThresholdIndicator ) } @@ -647,11 +656,12 @@ class BackPanelController internal constructor( override fun setIsLeftPanel(isLeftPanel: Boolean) { mView.isLeftPanel = isLeftPanel - layoutParams.gravity = if (isLeftPanel) { - Gravity.LEFT or Gravity.TOP - } else { - Gravity.RIGHT or Gravity.TOP - } + layoutParams.gravity = + if (isLeftPanel) { + Gravity.LEFT or Gravity.TOP + } else { + Gravity.RIGHT or Gravity.TOP + } } override fun setInsets(insetLeft: Int, insetRight: Int) = Unit @@ -667,12 +677,14 @@ class BackPanelController internal constructor( private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean { val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX - val flingVelocity = velocityTracker?.run { - computeCurrentVelocity(PX_PER_SEC) - xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) - } ?: 0f + val flingVelocity = + velocityTracker?.run { + computeCurrentVelocity(PX_PER_SEC) + xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) + } + ?: 0f val isPastFlingVelocityThreshold = - flingVelocity > viewConfiguration.scaledMinimumFlingVelocity + flingVelocity > viewConfiguration.scaledMinimumFlingVelocity return flingDistance > minFlingDistance && isPastFlingVelocityThreshold } @@ -699,8 +711,8 @@ class BackPanelController internal constructor( } private fun playWithBackgroundWidthAnimation( - onEnd: DelayedOnAnimationEndListener, - delay: Long = 0L + onEnd: DelayedOnAnimationEndListener, + delay: Long = 0L ) { if (delay == 0L) { updateRestingArrowDimens() @@ -724,104 +736,103 @@ class BackPanelController internal constructor( fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold) } - /** - * Updates resting arrow and background size not accounting for stretch - */ + /** Updates resting arrow and background size not accounting for stretch */ private fun updateRestingArrowDimens() { when (currentState) { GestureState.GONE, GestureState.ENTRY -> { mView.setSpring( - arrowLength = params.entryIndicator.arrowDimens.lengthSpring, - arrowHeight = params.entryIndicator.arrowDimens.heightSpring, - scale = params.entryIndicator.scaleSpring, - verticalTranslation = params.entryIndicator.verticalTranslationSpring, - horizontalTranslation = params.entryIndicator.horizontalTranslationSpring, - backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring, - backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.entryIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.entryIndicator.arrowDimens.lengthSpring, + arrowHeight = params.entryIndicator.arrowDimens.heightSpring, + scale = params.entryIndicator.scaleSpring, + verticalTranslation = params.entryIndicator.verticalTranslationSpring, + horizontalTranslation = params.entryIndicator.horizontalTranslationSpring, + backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring, + backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.entryIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.INACTIVE -> { mView.setSpring( - arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring, - arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring, - horizontalTranslation = params.preThresholdIndicator - .horizontalTranslationSpring, - scale = params.preThresholdIndicator.scaleSpring, - backgroundWidth = params.preThresholdIndicator.backgroundDimens - .widthSpring, - backgroundHeight = params.preThresholdIndicator.backgroundDimens - .heightSpring, - backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring, + arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring, + horizontalTranslation = + params.preThresholdIndicator.horizontalTranslationSpring, + scale = params.preThresholdIndicator.scaleSpring, + backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.ACTIVE -> { mView.setSpring( - arrowLength = params.activeIndicator.arrowDimens.lengthSpring, - arrowHeight = params.activeIndicator.arrowDimens.heightSpring, - scale = params.activeIndicator.scaleSpring, - horizontalTranslation = params.activeIndicator.horizontalTranslationSpring, - backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.activeIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.activeIndicator.arrowDimens.lengthSpring, + arrowHeight = params.activeIndicator.arrowDimens.heightSpring, + scale = params.activeIndicator.scaleSpring, + horizontalTranslation = params.activeIndicator.horizontalTranslationSpring, + backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.activeIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.FLUNG -> { mView.setSpring( - arrowLength = params.flungIndicator.arrowDimens.lengthSpring, - arrowHeight = params.flungIndicator.arrowDimens.heightSpring, - backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.flungIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.flungIndicator.arrowDimens.lengthSpring, + arrowHeight = params.flungIndicator.arrowDimens.heightSpring, + backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.flungIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.COMMITTED -> { mView.setSpring( - arrowLength = params.committedIndicator.arrowDimens.lengthSpring, - arrowHeight = params.committedIndicator.arrowDimens.heightSpring, - scale = params.committedIndicator.scaleSpring, - backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring, - backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.committedIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.committedIndicator.arrowDimens.lengthSpring, + arrowHeight = params.committedIndicator.arrowDimens.heightSpring, + scale = params.committedIndicator.scaleSpring, + backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring, + backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.committedIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.CANCELLED -> { mView.setSpring( - backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring) + backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring + ) } else -> {} } mView.setRestingDimens( - animate = !(currentState == GestureState.FLUNG || - currentState == GestureState.COMMITTED), - restingParams = EdgePanelParams.BackIndicatorDimens( - scale = when (currentState) { + animate = + !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED), + restingParams = + EdgePanelParams.BackIndicatorDimens( + scale = + when (currentState) { GestureState.ACTIVE, - GestureState.FLUNG, - -> params.activeIndicator.scale + GestureState.FLUNG, -> params.activeIndicator.scale GestureState.COMMITTED -> params.committedIndicator.scale else -> params.preThresholdIndicator.scale }, - scalePivotX = when (currentState) { + scalePivotX = + when (currentState) { GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE, @@ -830,7 +841,8 @@ class BackPanelController internal constructor( GestureState.FLUNG, GestureState.COMMITTED -> params.committedIndicator.scalePivotX }, - horizontalTranslation = when (currentState) { + horizontalTranslation = + when (currentState) { GestureState.GONE -> { params.activeIndicator.backgroundDimens.width?.times(-1) } @@ -843,7 +855,8 @@ class BackPanelController internal constructor( } else -> null }, - arrowDimens = when (currentState) { + arrowDimens = + when (currentState) { GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE -> params.entryIndicator.arrowDimens @@ -852,7 +865,8 @@ class BackPanelController internal constructor( GestureState.COMMITTED -> params.committedIndicator.arrowDimens GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens }, - backgroundDimens = when (currentState) { + backgroundDimens = + when (currentState) { GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE -> params.entryIndicator.backgroundDimens @@ -894,7 +908,7 @@ class BackPanelController internal constructor( GestureState.ACTIVE -> { backCallback.setTriggerBack(true) } - GestureState.GONE -> { } + GestureState.GONE -> {} } when (currentState) { @@ -913,18 +927,25 @@ class BackPanelController internal constructor( GestureState.ACTIVE -> { previousXTranslationOnActiveOffset = previousXTranslation updateRestingArrowDimens() - vibratorHelper.cancel() - mainHandler.postDelayed(10L) { - vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT) - } - val popVelocity = if (previousState == GestureState.INACTIVE) { - POP_ON_INACTIVE_TO_ACTIVE_VELOCITY + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + vibratorHelper.performHapticFeedback( + mView, + HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE + ) } else { - POP_ON_ENTRY_TO_ACTIVE_VELOCITY + vibratorHelper.cancel() + mainHandler.postDelayed(10L) { + vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT) + } } + val popVelocity = + if (previousState == GestureState.INACTIVE) { + POP_ON_INACTIVE_TO_ACTIVE_VELOCITY + } else { + POP_ON_ENTRY_TO_ACTIVE_VELOCITY + } mView.popOffEdge(popVelocity) } - GestureState.INACTIVE -> { gestureInactiveTime = SystemClock.uptimeMillis() @@ -937,7 +958,14 @@ class BackPanelController internal constructor( mView.popOffEdge(POP_ON_INACTIVE_VELOCITY) - vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT) + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + vibratorHelper.performHapticFeedback( + mView, + HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE + ) + } else { + vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT) + } updateRestingArrowDimens() } GestureState.FLUNG -> { @@ -945,8 +973,10 @@ class BackPanelController internal constructor( mView.popScale(POP_ON_FLING_VELOCITY) } updateRestingArrowDimens() - mainHandler.postDelayed(onEndSetCommittedStateListener.runnable, - MIN_DURATION_FLING_ANIMATION) + mainHandler.postDelayed( + onEndSetCommittedStateListener.runnable, + MIN_DURATION_FLING_ANIMATION + ) } GestureState.COMMITTED -> { // In most cases, animating between states is handled via `updateRestingArrowDimens` @@ -956,36 +986,43 @@ class BackPanelController internal constructor( // manually play these kinds of animations in parallel. if (previousState == GestureState.FLUNG) { updateRestingArrowDimens() - mainHandler.postDelayed(onEndSetGoneStateListener.runnable, - MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION) + mainHandler.postDelayed( + onEndSetGoneStateListener.runnable, + MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION + ) } else { mView.popScale(POP_ON_COMMITTED_VELOCITY) - mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable, - MIN_DURATION_COMMITTED_ANIMATION) + mainHandler.postDelayed( + onAlphaEndSetGoneStateListener.runnable, + MIN_DURATION_COMMITTED_ANIMATION + ) } } GestureState.CANCELLED -> { val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry) playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay) - val springForceOnCancelled = params.cancelledIndicator - .arrowDimens.alphaSpring?.get(0f)?.value + val springForceOnCancelled = + params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value mView.popArrowAlpha(0f, springForceOnCancelled) - mainHandler.postDelayed(10L) { vibratorHelper.cancel() } + if (!featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) + mainHandler.postDelayed(10L) { vibratorHelper.cancel() } } } } private fun convertVelocityToAnimationFactor( - valueOnFastVelocity: Float, - valueOnSlowVelocity: Float, - fastVelocityBound: Float = 1f, - slowVelocityBound: Float = 0.5f, + valueOnFastVelocity: Float, + valueOnSlowVelocity: Float, + fastVelocityBound: Float = 1f, + slowVelocityBound: Float = 0.5f, ): Float { - val factor = velocityTracker?.run { - computeCurrentVelocity(PX_PER_MS) - MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity)) - } ?: valueOnFastVelocity + val factor = + velocityTracker?.run { + computeCurrentVelocity(PX_PER_MS) + MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity)) + } + ?: valueOnFastVelocity return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor) } @@ -1014,77 +1051,76 @@ class BackPanelController internal constructor( } init { - if (DEBUG) mView.drawDebugInfo = { canvas -> - val debugStrings = listOf( - "$currentState", - "startX=$startX", - "startY=$startY", - "xDelta=${"%.1f".format(totalTouchDeltaActive)}", - "xTranslation=${"%.1f".format(previousXTranslation)}", - "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%", - "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%" - ) - val debugPaint = Paint().apply { - color = Color.WHITE - } - val debugInfoBottom = debugStrings.size * 32f + 4f - canvas.drawRect( + if (DEBUG) + mView.drawDebugInfo = { canvas -> + val preProgress = staticThresholdProgress(previousXTranslation) * 100 + val postProgress = fullScreenProgress(previousXTranslation) * 100 + val debugStrings = + listOf( + "$currentState", + "startX=$startX", + "startY=$startY", + "xDelta=${"%.1f".format(totalTouchDeltaActive)}", + "xTranslation=${"%.1f".format(previousXTranslation)}", + "pre=${"%.0f".format(preProgress)}%", + "post=${"%.0f".format(postProgress)}%" + ) + val debugPaint = Paint().apply { color = Color.WHITE } + val debugInfoBottom = debugStrings.size * 32f + 4f + canvas.drawRect( 4f, 4f, canvas.width.toFloat(), debugStrings.size * 32f + 4f, debugPaint - ) - debugPaint.apply { - color = Color.BLACK - textSize = 32f - } - var offset = 32f - for (debugText in debugStrings) { - canvas.drawText(debugText, 10f, offset, debugPaint) - offset += 32f - } - debugPaint.apply { - color = Color.RED - style = Paint.Style.STROKE - strokeWidth = 4f - } - val canvasWidth = canvas.width.toFloat() - val canvasHeight = canvas.height.toFloat() - canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint) + ) + debugPaint.apply { + color = Color.BLACK + textSize = 32f + } + var offset = 32f + for (debugText in debugStrings) { + canvas.drawText(debugText, 10f, offset, debugPaint) + offset += 32f + } + debugPaint.apply { + color = Color.RED + style = Paint.Style.STROKE + strokeWidth = 4f + } + val canvasWidth = canvas.width.toFloat() + val canvasHeight = canvas.height.toFloat() + canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint) - fun drawVerticalLine(x: Float, color: Int) { - debugPaint.color = color - val x = if (mView.isLeftPanel) x else canvasWidth - x - canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint) - } + fun drawVerticalLine(x: Float, color: Int) { + debugPaint.color = color + val x = if (mView.isLeftPanel) x else canvasWidth - x + canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint) + } - drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE) - drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE) - drawVerticalLine(x = startX, color = Color.GREEN) - drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY) - } + drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE) + drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE) + drawVerticalLine(x = startX, color = Color.GREEN) + drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY) + } } } /** - * In addition to a typical step function which returns one or two - * values based on a threshold, `Step` also gracefully handles quick - * changes in input near the threshold value that would typically - * result in the output rapidly changing. + * In addition to a typical step function which returns one or two values based on a threshold, + * `Step` also gracefully handles quick changes in input near the threshold value that would + * typically result in the output rapidly changing. * - * In the context of Back arrow, the arrow's stroke opacity should - * always appear transparent or opaque. Using a typical Step function, - * this would resulting in a flickering appearance as the output would - * change rapidly. `Step` addresses this by moving the threshold after - * it is crossed so it cannot be easily crossed again with small changes - * in touch events. + * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or + * opaque. Using a typical Step function, this would resulting in a flickering appearance as the + * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so + * it cannot be easily crossed again with small changes in touch events. */ class Step( - private val threshold: Float, - private val factor: Float = 1.1f, - private val postThreshold: T, - private val preThreshold: T + private val threshold: Float, + private val factor: Float = 1.1f, + private val postThreshold: T, + private val preThreshold: T ) { data class Value(val value: T, val isNewState: Boolean) diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index 611c5b987d844..fab1de00dcbcb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -20,6 +20,7 @@ import android.os.Handler import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_MOVE @@ -29,6 +30,8 @@ import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -36,6 +39,8 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify @@ -58,6 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() { @Mock private lateinit var latencyTracker: LatencyTracker @Mock private lateinit var layoutParams: WindowManager.LayoutParams @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback + private val featureFlags = FakeFeatureFlags() @Before fun setup() { @@ -70,7 +76,8 @@ class BackPanelControllerTest : SysuiTestCase() { Handler.createAsync(Looper.myLooper()), vibratorHelper, configurationController, - latencyTracker + latencyTracker, + featureFlags ) mBackPanelController.setLayoutParams(layoutParams) mBackPanelController.setBackCallback(backCallback) @@ -99,6 +106,7 @@ class BackPanelControllerTest : SysuiTestCase() { @Test fun handlesBackCommitted() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) @@ -121,8 +129,35 @@ class BackPanelControllerTest : SysuiTestCase() { verify(backCallback).triggerBack() } + @Test + fun handlesBackCommitted_withOneWayHapticsAPI() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + startTouch() + // Move once to cross the touch slop + continueTouch(START_X + touchSlop.toFloat() + 1) + // Move again to cross the back trigger threshold + continueTouch(START_X + touchSlop + triggerThreshold + 1) + // Wait threshold duration and hold touch past trigger threshold + Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) + continueTouch(START_X + touchSlop + triggerThreshold + 1) + + assertThat(mBackPanelController.currentState) + .isEqualTo(BackPanelController.GestureState.ACTIVE) + verify(backCallback).setTriggerBack(true) + testableLooper.moveTimeForward(100) + testableLooper.processAllMessages() + verify(vibratorHelper) + .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE)) + + finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1) + assertThat(mBackPanelController.currentState) + .isEqualTo(BackPanelController.GestureState.COMMITTED) + verify(backCallback).triggerBack() + } + @Test fun handlesBackCancelled() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) @@ -151,6 +186,38 @@ class BackPanelControllerTest : SysuiTestCase() { verify(backCallback).cancelBack() } + @Test + fun handlesBackCancelled_withOneWayHapticsAPI() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + startTouch() + // Move once to cross the touch slop + continueTouch(START_X + touchSlop.toFloat() + 1) + // Move again to cross the back trigger threshold + continueTouch( + START_X + touchSlop + triggerThreshold - + mBackPanelController.params.deactivationTriggerThreshold + ) + // Wait threshold duration and hold touch before trigger threshold + Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) + continueTouch( + START_X + touchSlop + triggerThreshold - + mBackPanelController.params.deactivationTriggerThreshold + ) + clearInvocations(backCallback) + Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) + // Move in the opposite direction to cross the deactivation threshold and cancel back + continueTouch(START_X) + + assertThat(mBackPanelController.currentState) + .isEqualTo(BackPanelController.GestureState.INACTIVE) + verify(backCallback).setTriggerBack(false) + verify(vibratorHelper) + .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE)) + + finishTouchActionUp(START_X) + verify(backCallback).cancelBack() + } + private fun startTouch() { mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f)) }