diff options
| author | 2024-07-11 03:13:13 +0000 | |
|---|---|---|
| committer | 2024-07-11 03:13:13 +0000 | |
| commit | 17f41e3d6b1e51d3b9643134e2e6e1bd57d601b5 (patch) | |
| tree | 6060f25f292a773fc88999df7de8d32451990753 | |
| parent | f048cc66e332ce3c3e8e3bb84199f088b5701c64 (diff) | |
| parent | 41cb43322bad9915458a083f019a0929854cf6bf (diff) | |
Merge "Adding click and long-click terminal states." into main
4 files changed, 138 insertions, 28 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index 74eee9b24cbc..693fcdabea58 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -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 @@ -69,6 +71,7 @@ class QSLongPressEffectTest : SysuiTestCase() { QSLongPressEffect( vibratorHelper, kosmos.keyguardStateController, + kosmos.falsingManager, ) longPressEffect.callback = callback longPressEffect.qsTile = qsTile @@ -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) @@ -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 @@ -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 { @@ -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) } /** diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index 7b5139aa510e..c44eb471d460 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -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 @@ -40,6 +42,7 @@ class QSLongPressEffect constructor( private val vibratorHelper: VibratorHelper?, private val keyguardStateController: KeyguardStateController, + private val falsingManager: FalsingManager, ) { var effectDuration = 0 @@ -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) @@ -160,14 +163,37 @@ constructor( } fun onTileClick(): Boolean { - if (state == State.TIMEOUT_WAIT) { - setState(State.IDLE) - qsTile?.let { - it.click(expandable) - return true - } + 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 } /** @@ -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 */ @@ -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 */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 44c846b5c631..787fd1ab7170 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -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) { @@ -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 @@ -906,6 +910,7 @@ constructor( } override fun onActivityLaunchAnimationEnd() { + longPressEffect?.resetState() if (longPressEffect != null && !haveLongPressPropertiesBeenReset) { resetLongPressEffectProperties() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt index eff99e047f45..28355e1c3efa 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt @@ -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) } |