Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 17f41e3d authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge "Adding click and long-click terminal states." into main

parents f048cc66 41cb4332
Loading
Loading
Loading
Loading
+90 −16
Original line number Diff line number Diff line
@@ -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)
    }

    /**
+41 −11
Original line number Diff line number Diff line
@@ -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)
        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 */
+5 −0
Original line number Diff line number Diff line
@@ -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()
        }
+2 −1
Original line number Diff line number Diff line
@@ -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) }