Loading packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +90 −16 Original line number Diff line number Diff line Loading @@ -17,10 +17,12 @@ package com.android.systemui.haptics.qs import android.os.VibrationEffect import android.service.quicksettings.Tile 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.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.testScope import com.android.systemui.qs.qsTileFactory Loading Loading @@ -69,6 +71,7 @@ class QSLongPressEffectTest : SysuiTestCase() { QSLongPressEffect( vibratorHelper, kosmos.keyguardStateController, kosmos.falsingManager, ) longPressEffect.callback = callback longPressEffect.qsTile = qsTile Loading Loading @@ -175,17 +178,17 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test fun onAnimationComplete_keyguardDismissible_effectCompletes() = fun onAnimationComplete_keyguardDismissible_effectEndsInLongClicked() = testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the long-press effect completes assertEffectCompleted() // THEN the long-press effect completes with a long-click state assertEffectCompleted(QSLongPressEffect.State.LONG_CLICKED) } @Test fun onAnimationComplete_keyguardNotDismissible_effectEndsWithReset() = fun onAnimationComplete_keyguardNotDismissible_effectEndsInIdleWithReset() = testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the keyguard is not dismissible whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) Loading @@ -193,19 +196,20 @@ class QSLongPressEffectTest : SysuiTestCase() { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the long-press effect completes and the properties are called to reset assertEffectCompleted() // THEN the long-press effect ends in the idle state and the properties are reset assertEffectCompleted(QSLongPressEffect.State.IDLE) verify(callback, times(1)).onResetProperties() } @Test fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversing() = fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() = testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the callback for finished reversing is used. // THEN the callback for finished reversing is used and the effect ends with a click. verify(callback, times(1)).onEffectFinishedReversing() assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.CLICKED) } @Test Loading Loading @@ -262,18 +266,88 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test fun onTileClick_whileWaiting_withoutQSTile_cannotClick() = testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { // GIVEN that no QSTile has been set longPressEffect.qsTile = null fun onTileClick_whileIdle_withQSTile_clicks() = testWhileInState(QSLongPressEffect.State.IDLE) { // GIVEN that a click was detected val couldClick = longPressEffect.onTileClick() // THEN the click is successful assertThat(couldClick).isTrue() } @Test fun onTileClick_whenBouncerIsShowing_ignoresClick() = testWhileInState(QSLongPressEffect.State.IDLE) { // GIVEN that the bouncer is showing whenever(kosmos.keyguardStateController.isPrimaryBouncerShowing).thenReturn(true) // WHEN a click is detected by the tile view val couldClick = longPressEffect.onTileClick() // THEN the click is not successful assertThat(couldClick).isFalse() } @Test fun getStateForClick_withUnavailableTile_returnsIdle() { // GIVEN an unavailable tile qsTile.state?.state = Tile.STATE_UNAVAILABLE // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is IDLE assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } @Test fun getStateForClick_withFalseTapWhenLocked_returnsIdle() { // GIVEN an active tile qsTile.state?.state = Tile.STATE_ACTIVE // GIVEN that the device is locked and a false tap is detected whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) kosmos.falsingManager.setFalseTap(true) // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is IDLE assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } @Test fun getStateForClick_withValidTapAndTile_returnsClicked() { // GIVEN an active tile qsTile.state?.state = Tile.STATE_ACTIVE // GIVEN that the device is locked and a false tap is not detected whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) kosmos.falsingManager.setFalseTap(false) // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is CLICKED assertThat(clickState).isEqualTo(QSLongPressEffect.State.CLICKED) } @Test fun getStateForClick_withNullTile_returnsIdle() { // GIVEN that the tile is null longPressEffect.qsTile = null // GIVEN that the device is locked and a false tap is not detected whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) kosmos.falsingManager.setFalseTap(false) // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is IDLE assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { Loading Loading @@ -339,14 +413,14 @@ class QSLongPressEffectTest : SysuiTestCase() { /** * Asserts that the effect completes by checking that: * 1. The final snap haptics are played * 2. The internal state goes back to [QSLongPressEffect.State.IDLE] * 2. The internal state goes back to specified end state. */ private fun assertEffectCompleted() { private fun assertEffectCompleted(endState: QSLongPressEffect.State) { val snapEffect = LongPressHapticBuilder.createSnapEffect() assertThat(snapEffect).isNotNull() assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue() assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) assertThat(longPressEffect.state).isEqualTo(endState) } /** Loading packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +41 −11 Original line number Diff line number Diff line Loading @@ -17,8 +17,10 @@ package com.android.systemui.haptics.qs import android.os.VibrationEffect import android.service.quicksettings.Tile import androidx.annotation.VisibleForTesting import com.android.systemui.animation.Expandable import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController Loading @@ -40,6 +42,7 @@ class QSLongPressEffect constructor( private val vibratorHelper: VibratorHelper?, private val keyguardStateController: KeyguardStateController, private val falsingManager: FalsingManager, ) { var effectDuration = 0 Loading Loading @@ -130,18 +133,18 @@ constructor( fun handleAnimationComplete() { when (state) { State.RUNNING_FORWARD -> { setState(State.IDLE) vibrate(snapEffect) if (keyguardStateController.isUnlocked) { qsTile?.longClick(expandable) setState(State.LONG_CLICKED) } else { callback?.onResetProperties() qsTile?.longClick(expandable) setState(State.IDLE) } qsTile?.longClick(expandable) } State.RUNNING_BACKWARDS_FROM_UP -> { setState(State.IDLE) callback?.onEffectFinishedReversing() setState(getStateForClick()) qsTile?.click(expandable) } State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE) Loading @@ -160,14 +163,37 @@ constructor( } fun onTileClick(): Boolean { if (state == State.TIMEOUT_WAIT) { setState(State.IDLE) qsTile?.let { it.click(expandable) val isStateClickable = state == State.TIMEOUT_WAIT || state == State.IDLE // Ignore View-generated clicks on invalid states or if the bouncer is showing if (keyguardStateController.isPrimaryBouncerShowing || !isStateClickable) return false setState(getStateForClick()) qsTile?.click(expandable) return true } /** * Get the appropriate state for a click action. * * In some occasions, the click action will not result in a subsequent action that resets the * state upon completion (e.g., a launch transition animation). In these cases, the state needs * to be reset before the click is dispatched. */ @VisibleForTesting fun getStateForClick(): State { val isTileUnavailable = qsTile?.state?.state == Tile.STATE_UNAVAILABLE val isFalseTapWhileLocked = !keyguardStateController.isUnlocked && falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) val handlesLongClick = qsTile?.state?.handlesLongClick == true return if (isTileUnavailable || isFalseTapWhileLocked || !handlesLongClick) { // The click event will not perform an action that resets the state. Therefore, this is // the last opportunity to reset the state back to IDLE. State.IDLE } else { State.CLICKED } return false } /** Loading @@ -194,6 +220,8 @@ constructor( return true } fun resetState() = setState(State.IDLE) enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */ Loading @@ -202,6 +230,8 @@ constructor( RUNNING_BACKWARDS_FROM_UP, /* The effect was interrupted by an ACTION_CANCEL 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 */ } /** Callbacks to notify view and animator actions */ Loading packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +5 −0 Original line number Diff line number Diff line Loading @@ -386,6 +386,7 @@ constructor( // 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) { Loading Loading @@ -771,11 +772,14 @@ constructor( lastIconTint = icon.getColor(state) // Long-press effects longPressEffect?.qsTile?.state?.handlesLongClick = state.handlesLongClick if ( state.handlesLongClick && longPressEffect?.initializeEffect(longPressEffectDuration) == true ) { showRippleEffect = false longPressEffect.qsTile?.state?.state = lastState // Store the tile's state longPressEffect.resetState() initializeLongPressProperties(measuredHeight, measuredWidth) } else { // Long-press effects might have been enabled before but the new state does not Loading Loading @@ -906,6 +910,7 @@ constructor( } override fun onActivityLaunchAnimationEnd() { longPressEffect?.resetState() if (longPressEffect != null && !haveLongPressPropertiesBeenReset) { resetLongPressEffectProperties() } Loading packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt +2 −1 Original line number Diff line number Diff line Loading @@ -16,9 +16,10 @@ package com.android.systemui.haptics.qs import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.policy.keyguardStateController val Kosmos.qsLongPressEffect by Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController) } Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController, falsingManager) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +90 −16 Original line number Diff line number Diff line Loading @@ -17,10 +17,12 @@ package com.android.systemui.haptics.qs import android.os.VibrationEffect import android.service.quicksettings.Tile 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.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.testScope import com.android.systemui.qs.qsTileFactory Loading Loading @@ -69,6 +71,7 @@ class QSLongPressEffectTest : SysuiTestCase() { QSLongPressEffect( vibratorHelper, kosmos.keyguardStateController, kosmos.falsingManager, ) longPressEffect.callback = callback longPressEffect.qsTile = qsTile Loading Loading @@ -175,17 +178,17 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test fun onAnimationComplete_keyguardDismissible_effectCompletes() = fun onAnimationComplete_keyguardDismissible_effectEndsInLongClicked() = testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the long-press effect completes assertEffectCompleted() // THEN the long-press effect completes with a long-click state assertEffectCompleted(QSLongPressEffect.State.LONG_CLICKED) } @Test fun onAnimationComplete_keyguardNotDismissible_effectEndsWithReset() = fun onAnimationComplete_keyguardNotDismissible_effectEndsInIdleWithReset() = testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the keyguard is not dismissible whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) Loading @@ -193,19 +196,20 @@ class QSLongPressEffectTest : SysuiTestCase() { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the long-press effect completes and the properties are called to reset assertEffectCompleted() // THEN the long-press effect ends in the idle state and the properties are reset assertEffectCompleted(QSLongPressEffect.State.IDLE) verify(callback, times(1)).onResetProperties() } @Test fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversing() = fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() = testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() // THEN the callback for finished reversing is used. // THEN the callback for finished reversing is used and the effect ends with a click. verify(callback, times(1)).onEffectFinishedReversing() assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.CLICKED) } @Test Loading Loading @@ -262,18 +266,88 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test fun onTileClick_whileWaiting_withoutQSTile_cannotClick() = testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { // GIVEN that no QSTile has been set longPressEffect.qsTile = null fun onTileClick_whileIdle_withQSTile_clicks() = testWhileInState(QSLongPressEffect.State.IDLE) { // GIVEN that a click was detected val couldClick = longPressEffect.onTileClick() // THEN the click is successful assertThat(couldClick).isTrue() } @Test fun onTileClick_whenBouncerIsShowing_ignoresClick() = testWhileInState(QSLongPressEffect.State.IDLE) { // GIVEN that the bouncer is showing whenever(kosmos.keyguardStateController.isPrimaryBouncerShowing).thenReturn(true) // WHEN a click is detected by the tile view val couldClick = longPressEffect.onTileClick() // THEN the click is not successful assertThat(couldClick).isFalse() } @Test fun getStateForClick_withUnavailableTile_returnsIdle() { // GIVEN an unavailable tile qsTile.state?.state = Tile.STATE_UNAVAILABLE // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is IDLE assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } @Test fun getStateForClick_withFalseTapWhenLocked_returnsIdle() { // GIVEN an active tile qsTile.state?.state = Tile.STATE_ACTIVE // GIVEN that the device is locked and a false tap is detected whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) kosmos.falsingManager.setFalseTap(true) // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is IDLE assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } @Test fun getStateForClick_withValidTapAndTile_returnsClicked() { // GIVEN an active tile qsTile.state?.state = Tile.STATE_ACTIVE // GIVEN that the device is locked and a false tap is not detected whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) kosmos.falsingManager.setFalseTap(false) // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is CLICKED assertThat(clickState).isEqualTo(QSLongPressEffect.State.CLICKED) } @Test fun getStateForClick_withNullTile_returnsIdle() { // GIVEN that the tile is null longPressEffect.qsTile = null // GIVEN that the device is locked and a false tap is not detected whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) kosmos.falsingManager.setFalseTap(false) // WHEN determining the state of a click action val clickState = longPressEffect.getStateForClick() // THEN the state is IDLE assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { Loading Loading @@ -339,14 +413,14 @@ class QSLongPressEffectTest : SysuiTestCase() { /** * Asserts that the effect completes by checking that: * 1. The final snap haptics are played * 2. The internal state goes back to [QSLongPressEffect.State.IDLE] * 2. The internal state goes back to specified end state. */ private fun assertEffectCompleted() { private fun assertEffectCompleted(endState: QSLongPressEffect.State) { val snapEffect = LongPressHapticBuilder.createSnapEffect() assertThat(snapEffect).isNotNull() assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue() assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) assertThat(longPressEffect.state).isEqualTo(endState) } /** Loading
packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +41 −11 Original line number Diff line number Diff line Loading @@ -17,8 +17,10 @@ package com.android.systemui.haptics.qs import android.os.VibrationEffect import android.service.quicksettings.Tile import androidx.annotation.VisibleForTesting import com.android.systemui.animation.Expandable import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController Loading @@ -40,6 +42,7 @@ class QSLongPressEffect constructor( private val vibratorHelper: VibratorHelper?, private val keyguardStateController: KeyguardStateController, private val falsingManager: FalsingManager, ) { var effectDuration = 0 Loading Loading @@ -130,18 +133,18 @@ constructor( fun handleAnimationComplete() { when (state) { State.RUNNING_FORWARD -> { setState(State.IDLE) vibrate(snapEffect) if (keyguardStateController.isUnlocked) { qsTile?.longClick(expandable) setState(State.LONG_CLICKED) } else { callback?.onResetProperties() qsTile?.longClick(expandable) setState(State.IDLE) } qsTile?.longClick(expandable) } State.RUNNING_BACKWARDS_FROM_UP -> { setState(State.IDLE) callback?.onEffectFinishedReversing() setState(getStateForClick()) qsTile?.click(expandable) } State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE) Loading @@ -160,14 +163,37 @@ constructor( } fun onTileClick(): Boolean { if (state == State.TIMEOUT_WAIT) { setState(State.IDLE) qsTile?.let { it.click(expandable) val isStateClickable = state == State.TIMEOUT_WAIT || state == State.IDLE // Ignore View-generated clicks on invalid states or if the bouncer is showing if (keyguardStateController.isPrimaryBouncerShowing || !isStateClickable) return false setState(getStateForClick()) qsTile?.click(expandable) return true } /** * Get the appropriate state for a click action. * * In some occasions, the click action will not result in a subsequent action that resets the * state upon completion (e.g., a launch transition animation). In these cases, the state needs * to be reset before the click is dispatched. */ @VisibleForTesting fun getStateForClick(): State { val isTileUnavailable = qsTile?.state?.state == Tile.STATE_UNAVAILABLE val isFalseTapWhileLocked = !keyguardStateController.isUnlocked && falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) val handlesLongClick = qsTile?.state?.handlesLongClick == true return if (isTileUnavailable || isFalseTapWhileLocked || !handlesLongClick) { // The click event will not perform an action that resets the state. Therefore, this is // the last opportunity to reset the state back to IDLE. State.IDLE } else { State.CLICKED } return false } /** Loading @@ -194,6 +220,8 @@ constructor( return true } fun resetState() = setState(State.IDLE) enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */ Loading @@ -202,6 +230,8 @@ constructor( RUNNING_BACKWARDS_FROM_UP, /* The effect was interrupted by an ACTION_CANCEL 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 */ } /** Callbacks to notify view and animator actions */ Loading
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +5 −0 Original line number Diff line number Diff line Loading @@ -386,6 +386,7 @@ constructor( // 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) { Loading Loading @@ -771,11 +772,14 @@ constructor( lastIconTint = icon.getColor(state) // Long-press effects longPressEffect?.qsTile?.state?.handlesLongClick = state.handlesLongClick if ( state.handlesLongClick && longPressEffect?.initializeEffect(longPressEffectDuration) == true ) { showRippleEffect = false longPressEffect.qsTile?.state?.state = lastState // Store the tile's state longPressEffect.resetState() initializeLongPressProperties(measuredHeight, measuredWidth) } else { // Long-press effects might have been enabled before but the new state does not Loading Loading @@ -906,6 +910,7 @@ constructor( } override fun onActivityLaunchAnimationEnd() { longPressEffect?.resetState() if (longPressEffect != null && !haveLongPressPropertiesBeenReset) { resetLongPressEffectProperties() } Loading
packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt +2 −1 Original line number Diff line number Diff line Loading @@ -16,9 +16,10 @@ package com.android.systemui.haptics.qs import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.policy.keyguardStateController val Kosmos.qsLongPressEffect by Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController) } Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController, falsingManager) }