Loading packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +21 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.testScope Loading Loading @@ -52,6 +53,7 @@ class QSLongPressEffectTest : SysuiTestCase() { private val vibratorHelper = kosmos.vibratorHelper private val qsTile = kosmos.qsTileFactory.createTile("Test Tile") @Mock private lateinit var callback: QSLongPressEffect.Callback @Mock private lateinit var controller: ActivityTransitionAnimator.Controller private val effectDuration = 400 private val lowTickDuration = 12 Loading Loading @@ -218,8 +220,9 @@ class QSLongPressEffectTest : SysuiTestCase() { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the effect ends in the idle state. // THEN the effect ends in the idle state and the reversed callback is used. assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) verify(callback, times(1)).onEffectFinishedReversing() } @Test Loading Loading @@ -348,6 +351,23 @@ class QSLongPressEffectTest : SysuiTestCase() { assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } @Test fun onLongClickTransitionCancelled_whileInLongClickState_reversesEffect() = testWhileInState(QSLongPressEffect.State.LONG_CLICKED) { // GIVEN a transition controller delegate val delegate = longPressEffect.createTransitionControllerDelegate(controller) // WHEN the activity launch animation is cancelled val newOccludedState = false delegate.onTransitionAnimationCancelled(newOccludedState) // THEN the effect reverses and ends in RUNNING_BACKWARDS_FROM_CANCEL assertThat(longPressEffect.state) .isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) verify(callback, times(1)).onReverseAnimator(false) verify(controller).onTransitionAnimationCancelled(newOccludedState) } private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { Loading packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +62 −5 Original line number Diff line number Diff line Loading @@ -16,9 +16,15 @@ package com.android.systemui.haptics.qs import android.content.ComponentName import android.os.VibrationEffect import android.service.quicksettings.Tile import android.view.View import androidx.annotation.VisibleForTesting import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.DelegateTransitionAnimatorController import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile Loading Loading @@ -58,6 +64,7 @@ constructor( /** The [QSTile] and [Expandable] used to perform a long-click and click actions */ var qsTile: QSTile? = null var expandable: Expandable? = null private set /** Haptic effects */ private val durations = Loading Loading @@ -125,9 +132,11 @@ constructor( } fun handleAnimationStart() { if (state == State.TIMEOUT_WAIT) { vibrate(longPressHint) setState(State.RUNNING_FORWARD) } } /** This function is called both when an animator completes or gets cancelled */ fun handleAnimationComplete() { Loading @@ -147,7 +156,10 @@ constructor( setState(getStateForClick()) qsTile?.click(expandable) } State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE) State.RUNNING_BACKWARDS_FROM_CANCEL -> { callback?.onEffectFinishedReversing() setState(State.IDLE) } else -> {} } } Loading Loading @@ -222,13 +234,58 @@ constructor( fun resetState() = setState(State.IDLE) fun createExpandableFromView(view: View) { expandable = object : Expandable { override fun activityTransitionController( launchCujType: Int?, cookie: ActivityTransitionAnimator.TransitionCookie?, component: ComponentName?, returnCujType: Int?, ): ActivityTransitionAnimator.Controller? { val delegatedController = ActivityTransitionAnimator.Controller.fromView( view, launchCujType, cookie, component, returnCujType, ) return delegatedController?.let { createTransitionControllerDelegate(it) } } override fun dialogTransitionController( cuj: DialogCuj?, ): DialogTransitionAnimator.Controller? = DialogTransitionAnimator.Controller.fromView(view, cuj) } } @VisibleForTesting fun createTransitionControllerDelegate( controller: ActivityTransitionAnimator.Controller ): DelegateTransitionAnimatorController { val delegated = object : DelegateTransitionAnimatorController(controller) { override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { if (state == State.LONG_CLICKED) { setState(State.RUNNING_BACKWARDS_FROM_CANCEL) callback?.onReverseAnimator(false) } delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) } } return delegated } enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */ RUNNING_FORWARD, /* The effect is running normally */ /* The effect was interrupted by an ACTION_UP and is now running backwards */ RUNNING_BACKWARDS_FROM_UP, /* The effect was interrupted by an ACTION_CANCEL and is now running backwards */ /* The effect was cancelled by an ACTION_CANCEL or a shade collapse and is now running backwards */ RUNNING_BACKWARDS_FROM_CANCEL, CLICKED, /* The effect has ended with a click */ LONG_CLICKED, /* The effect has ended with a long-click */ Loading @@ -247,7 +304,7 @@ constructor( fun onStartAnimator() /** Reverse the effect animator */ fun onReverseAnimator() fun onReverseAnimator(playHaptics: Boolean = true) /** Cancel the effect animator */ fun onCancelAnimator() Loading packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +7 −13 Original line number Diff line number Diff line Loading @@ -107,8 +107,10 @@ constructor( set(value) { if (field == value) return field = value if (longPressEffect?.state != QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) { updateHeight() } } override var squishinessFraction: Float = 1f set(value) { Loading Loading @@ -381,14 +383,6 @@ constructor( } private fun updateHeight() { // TODO(b/332900989): Find a more robust way of resetting the tile if not reset by the // launch animation. if (!haveLongPressPropertiesBeenReset && longPressEffect != null) { // The launch animation of a long-press effect did not reset the long-press effect so // we must do it here resetLongPressEffectProperties() longPressEffect.resetState() } val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) { heightOverride Loading Loading @@ -417,17 +411,17 @@ constructor( } override fun init(tile: QSTile) { val expandable = Expandable.fromView(this) if (longPressEffect != null) { isHapticFeedbackEnabled = false longPressEffect.qsTile = tile longPressEffect.expandable = expandable longPressEffect.createExpandableFromView(this) initLongPressEffectCallback() init( { _: View -> longPressEffect.onTileClick() }, null, // Haptics and long-clicks will be handled by the [QSLongPressEffect] ) } else { val expandable = Expandable.fromView(this) init( { _: View? -> tile.click(expandable) }, { _: View? -> Loading Loading @@ -475,10 +469,10 @@ constructor( } } override fun onReverseAnimator() { override fun onReverseAnimator(playHaptics: Boolean) { longPressEffectAnimator?.let { val pausedProgress = it.animatedFraction longPressEffect?.playReverseHaptics(pausedProgress) if (playHaptics) longPressEffect?.playReverseHaptics(pausedProgress) it.reverse() } } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +21 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.testScope Loading Loading @@ -52,6 +53,7 @@ class QSLongPressEffectTest : SysuiTestCase() { private val vibratorHelper = kosmos.vibratorHelper private val qsTile = kosmos.qsTileFactory.createTile("Test Tile") @Mock private lateinit var callback: QSLongPressEffect.Callback @Mock private lateinit var controller: ActivityTransitionAnimator.Controller private val effectDuration = 400 private val lowTickDuration = 12 Loading Loading @@ -218,8 +220,9 @@ class QSLongPressEffectTest : SysuiTestCase() { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the effect ends in the idle state. // THEN the effect ends in the idle state and the reversed callback is used. assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) verify(callback, times(1)).onEffectFinishedReversing() } @Test Loading Loading @@ -348,6 +351,23 @@ class QSLongPressEffectTest : SysuiTestCase() { assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } @Test fun onLongClickTransitionCancelled_whileInLongClickState_reversesEffect() = testWhileInState(QSLongPressEffect.State.LONG_CLICKED) { // GIVEN a transition controller delegate val delegate = longPressEffect.createTransitionControllerDelegate(controller) // WHEN the activity launch animation is cancelled val newOccludedState = false delegate.onTransitionAnimationCancelled(newOccludedState) // THEN the effect reverses and ends in RUNNING_BACKWARDS_FROM_CANCEL assertThat(longPressEffect.state) .isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) verify(callback, times(1)).onReverseAnimator(false) verify(controller).onTransitionAnimationCancelled(newOccludedState) } private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { Loading
packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +62 −5 Original line number Diff line number Diff line Loading @@ -16,9 +16,15 @@ package com.android.systemui.haptics.qs import android.content.ComponentName import android.os.VibrationEffect import android.service.quicksettings.Tile import android.view.View import androidx.annotation.VisibleForTesting import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.DelegateTransitionAnimatorController import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile Loading Loading @@ -58,6 +64,7 @@ constructor( /** The [QSTile] and [Expandable] used to perform a long-click and click actions */ var qsTile: QSTile? = null var expandable: Expandable? = null private set /** Haptic effects */ private val durations = Loading Loading @@ -125,9 +132,11 @@ constructor( } fun handleAnimationStart() { if (state == State.TIMEOUT_WAIT) { vibrate(longPressHint) setState(State.RUNNING_FORWARD) } } /** This function is called both when an animator completes or gets cancelled */ fun handleAnimationComplete() { Loading @@ -147,7 +156,10 @@ constructor( setState(getStateForClick()) qsTile?.click(expandable) } State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE) State.RUNNING_BACKWARDS_FROM_CANCEL -> { callback?.onEffectFinishedReversing() setState(State.IDLE) } else -> {} } } Loading Loading @@ -222,13 +234,58 @@ constructor( fun resetState() = setState(State.IDLE) fun createExpandableFromView(view: View) { expandable = object : Expandable { override fun activityTransitionController( launchCujType: Int?, cookie: ActivityTransitionAnimator.TransitionCookie?, component: ComponentName?, returnCujType: Int?, ): ActivityTransitionAnimator.Controller? { val delegatedController = ActivityTransitionAnimator.Controller.fromView( view, launchCujType, cookie, component, returnCujType, ) return delegatedController?.let { createTransitionControllerDelegate(it) } } override fun dialogTransitionController( cuj: DialogCuj?, ): DialogTransitionAnimator.Controller? = DialogTransitionAnimator.Controller.fromView(view, cuj) } } @VisibleForTesting fun createTransitionControllerDelegate( controller: ActivityTransitionAnimator.Controller ): DelegateTransitionAnimatorController { val delegated = object : DelegateTransitionAnimatorController(controller) { override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { if (state == State.LONG_CLICKED) { setState(State.RUNNING_BACKWARDS_FROM_CANCEL) callback?.onReverseAnimator(false) } delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) } } return delegated } enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */ RUNNING_FORWARD, /* The effect is running normally */ /* The effect was interrupted by an ACTION_UP and is now running backwards */ RUNNING_BACKWARDS_FROM_UP, /* The effect was interrupted by an ACTION_CANCEL and is now running backwards */ /* The effect was cancelled by an ACTION_CANCEL or a shade collapse and is now running backwards */ RUNNING_BACKWARDS_FROM_CANCEL, CLICKED, /* The effect has ended with a click */ LONG_CLICKED, /* The effect has ended with a long-click */ Loading @@ -247,7 +304,7 @@ constructor( fun onStartAnimator() /** Reverse the effect animator */ fun onReverseAnimator() fun onReverseAnimator(playHaptics: Boolean = true) /** Cancel the effect animator */ fun onCancelAnimator() Loading
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +7 −13 Original line number Diff line number Diff line Loading @@ -107,8 +107,10 @@ constructor( set(value) { if (field == value) return field = value if (longPressEffect?.state != QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) { updateHeight() } } override var squishinessFraction: Float = 1f set(value) { Loading Loading @@ -381,14 +383,6 @@ constructor( } private fun updateHeight() { // TODO(b/332900989): Find a more robust way of resetting the tile if not reset by the // launch animation. if (!haveLongPressPropertiesBeenReset && longPressEffect != null) { // The launch animation of a long-press effect did not reset the long-press effect so // we must do it here resetLongPressEffectProperties() longPressEffect.resetState() } val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) { heightOverride Loading Loading @@ -417,17 +411,17 @@ constructor( } override fun init(tile: QSTile) { val expandable = Expandable.fromView(this) if (longPressEffect != null) { isHapticFeedbackEnabled = false longPressEffect.qsTile = tile longPressEffect.expandable = expandable longPressEffect.createExpandableFromView(this) initLongPressEffectCallback() init( { _: View -> longPressEffect.onTileClick() }, null, // Haptics and long-clicks will be handled by the [QSLongPressEffect] ) } else { val expandable = Expandable.fromView(this) init( { _: View? -> tile.click(expandable) }, { _: View? -> Loading Loading @@ -475,10 +469,10 @@ constructor( } } override fun onReverseAnimator() { override fun onReverseAnimator(playHaptics: Boolean) { longPressEffectAnimator?.let { val pausedProgress = it.animatedFraction longPressEffect?.playReverseHaptics(pausedProgress) if (playHaptics) longPressEffect?.playReverseHaptics(pausedProgress) it.reverse() } } Loading