Merge "Optimizing the QSLongPressEffect" into main
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 3889703..e332656 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
@@ -18,9 +18,6 @@
 
 import android.os.VibrationEffect
 import android.testing.TestableLooper.RunWithLooper
-import android.view.MotionEvent
-import android.view.View
-import androidx.test.core.view.MotionEventBuilder
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -29,18 +26,15 @@
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
-import com.android.systemui.kosmos.backgroundCoroutineContext
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 
@@ -50,7 +44,6 @@
 class QSLongPressEffectTest : SysuiTestCase() {
 
     @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
-    @Mock private lateinit var testView: View
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
     private val kosmos = testKosmos()
     private val vibratorHelper = kosmos.vibratorHelper
@@ -73,7 +66,6 @@
             QSLongPressEffect(
                 vibratorHelper,
                 kosmos.keyguardInteractor,
-                CoroutineScope(kosmos.backgroundCoroutineContext),
             )
         longPressEffect.initializeEffect(effectDuration)
     }
@@ -133,8 +125,7 @@
     @Test
     fun onActionDown_whileIdle_startsWait() = testWithScope {
         // GIVEN an action down event occurs
-        val downEvent = buildMotionEvent(MotionEvent.ACTION_DOWN)
-        longPressEffect.onTouch(testView, downEvent)
+        longPressEffect.handleActionDown()
 
         // THEN the effect moves to the TIMEOUT_WAIT state
         val state by collectLastValue(longPressEffect.state)
@@ -144,8 +135,7 @@
     @Test
     fun onActionCancel_whileWaiting_goesIdle() = testWhileWaiting {
         // GIVEN an action cancel occurs
-        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
-        longPressEffect.onTouch(testView, cancelEvent)
+        longPressEffect.handleActionCancel()
 
         // THEN the effect goes back to idle and does not start
         val state by collectLastValue(longPressEffect.state)
@@ -159,8 +149,7 @@
         val action by collectLastValue(longPressEffect.actionType)
 
         // GIVEN an action up occurs
-        val upEvent = buildMotionEvent(MotionEvent.ACTION_UP)
-        longPressEffect.onTouch(testView, upEvent)
+        longPressEffect.handleActionUp()
 
         // THEN the action to invoke is the click action and the effect does not start
         assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CLICK)
@@ -182,8 +171,7 @@
         animatorTestRule.advanceTimeBy(effectDuration / 2L)
 
         // WHEN an action up occurs
-        val upEvent = buildMotionEvent(MotionEvent.ACTION_UP)
-        longPressEffect.onTouch(testView, upEvent)
+        longPressEffect.handleActionUp()
 
         // THEN the effect gets reversed at 50% progress
         assertEffectReverses(0.5f)
@@ -195,8 +183,7 @@
         animatorTestRule.advanceTimeBy(effectDuration / 2L)
 
         // WHEN an action cancel occurs
-        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
-        longPressEffect.onTouch(testView, cancelEvent)
+        longPressEffect.handleActionCancel()
 
         // THEN the effect gets reversed at 50% progress
         assertEffectReverses(0.5f)
@@ -230,12 +217,10 @@
         animatorTestRule.advanceTimeBy(effectDuration / 2L)
 
         // GIVEN an action cancel occurs and the effect gets reversed
-        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
-        longPressEffect.onTouch(testView, cancelEvent)
+        longPressEffect.handleActionCancel()
 
         // GIVEN an action down occurs
-        val downEvent = buildMotionEvent(MotionEvent.ACTION_DOWN)
-        longPressEffect.onTouch(testView, downEvent)
+        longPressEffect.handleActionDown()
 
         // THEN the effect resets
         assertEffectResets()
@@ -247,8 +232,7 @@
         animatorTestRule.advanceTimeBy(effectDuration / 2L)
 
         // GIVEN an action cancel occurs and the effect gets reversed
-        val cancelEvent = buildMotionEvent(MotionEvent.ACTION_CANCEL)
-        longPressEffect.onTouch(testView, cancelEvent)
+        longPressEffect.handleActionCancel()
 
         // GIVEN that the animation completes after a sufficient amount of time
         animatorTestRule.advanceTimeBy(effectDuration.toLong())
@@ -258,9 +242,6 @@
         assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
     }
 
-    private fun buildMotionEvent(action: Int): MotionEvent =
-        MotionEventBuilder.newBuilder().setAction(action).build()
-
     private fun testWithScope(test: suspend TestScope.() -> Unit) =
         with(kosmos) { testScope.runTest { test() } }
 
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 4327d18..bccc3c5 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -17,9 +17,7 @@
 package com.android.systemui.haptics.qs
 
 import android.animation.ValueAnimator
-import android.annotation.SuppressLint
 import android.os.VibrationEffect
-import android.view.MotionEvent
 import android.view.View
 import android.view.ViewConfiguration
 import android.view.animation.AccelerateDecelerateInterpolator
@@ -27,18 +25,14 @@
 import androidx.core.animation.doOnCancel
 import androidx.core.animation.doOnEnd
 import androidx.core.animation.doOnStart
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.statusbar.VibratorHelper
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
 
 /**
  * A class that handles the long press visuo-haptic effect for a QS tile.
@@ -55,34 +49,32 @@
 @Inject
 constructor(
     private val vibratorHelper: VibratorHelper?,
-    val keyguardInteractor: KeyguardInteractor,
-    @Background bgScope: CoroutineScope,
-) : View.OnTouchListener {
+    keyguardInteractor: KeyguardInteractor,
+) {
 
     private var effectDuration = 0
 
     /** Current state */
     private var _state = MutableStateFlow(State.IDLE)
-    val state = _state.stateIn(bgScope, SharingStarted.Lazily, State.IDLE)
+    val state = _state.asStateFlow()
 
     /** Flows for view control and action */
     private val _effectProgress = MutableStateFlow<Float?>(null)
-    val effectProgress = _effectProgress.stateIn(bgScope, SharingStarted.Lazily, null)
+    val effectProgress = _effectProgress.asStateFlow()
 
     // Actions to perform
     private val _postedActionType = MutableStateFlow<ActionType?>(null)
-    val actionType: StateFlow<ActionType?> =
+    val actionType: Flow<ActionType?> =
         combine(
-                _postedActionType,
-                keyguardInteractor.isKeyguardDismissible,
-            ) { action, isDismissible ->
-                if (!isDismissible && action == ActionType.LONG_PRESS) {
-                    ActionType.RESET_AND_LONG_PRESS
-                } else {
-                    action
-                }
+            _postedActionType,
+            keyguardInteractor.isKeyguardDismissible,
+        ) { action, isDismissible ->
+            if (!isDismissible && action == ActionType.LONG_PRESS) {
+                ActionType.RESET_AND_LONG_PRESS
+            } else {
+                action
             }
-            .stateIn(bgScope, SharingStarted.Lazily, null)
+        }
 
     // Should a tap timeout countdown begin
     val shouldWaitForTapTimeout: Flow<Boolean> = state.map { it == State.TIMEOUT_WAIT }
@@ -129,23 +121,7 @@
         }
     }
 
-    /**
-     * Handle relevant touch events for the operation of a Tile.
-     *
-     * A click action is performed following the relevant logic that originates from the
-     * [MotionEvent.ACTION_UP] event depending on the current state.
-     */
-    @SuppressLint("ClickableViewAccessibility")
-    override fun onTouch(view: View?, event: MotionEvent?): Boolean {
-        when (event?.actionMasked) {
-            MotionEvent.ACTION_DOWN -> handleActionDown()
-            MotionEvent.ACTION_UP -> handleActionUp()
-            MotionEvent.ACTION_CANCEL -> handleActionCancel()
-        }
-        return true
-    }
-
-    private fun handleActionDown() {
+    fun handleActionDown() {
         when (_state.value) {
             State.IDLE -> {
                 setState(State.TIMEOUT_WAIT)
@@ -155,7 +131,7 @@
         }
     }
 
-    private fun handleActionUp() {
+    fun handleActionUp() {
         when (_state.value) {
             State.TIMEOUT_WAIT -> {
                 _postedActionType.value = ActionType.CLICK
@@ -169,7 +145,7 @@
         }
     }
 
-    private fun handleActionCancel() {
+    fun handleActionCancel() {
         when (_state.value) {
             State.TIMEOUT_WAIT -> {
                 setState(State.IDLE)
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
index ddb9f35..2ef901d 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.haptics.qs
 
+import android.annotation.SuppressLint
+import android.view.MotionEvent
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launch
@@ -25,10 +27,9 @@
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.launch
 
-// TODO(b/332903800)
 object QSLongPressEffectViewBinder {
+
     fun bind(
         tile: QSTileViewImpl,
         qsLongPressEffect: QSLongPressEffect?,
@@ -36,11 +37,13 @@
     ): DisposableHandle? {
         if (qsLongPressEffect == null) return null
 
+        // Set the touch listener as the long-press effect
+        setTouchListener(tile, qsLongPressEffect)
+
         return tile.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
-                val tag = "${tileSpec ?: "unknownTileSpec"}#LongPressEffect"
                 // Progress of the effect
-                launch("$tag#progress") {
+                launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#progress" }) {
                     qsLongPressEffect.effectProgress.collect { progress ->
                         progress?.let {
                             if (it == 0f) {
@@ -53,7 +56,7 @@
                 }
 
                 // Action to perform
-                launch("$tag#action") {
+                launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#action" }) {
                     qsLongPressEffect.actionType.collect { action ->
                         action?.let {
                             when (it) {
@@ -70,7 +73,7 @@
                 }
 
                 // Tap timeout wait
-                launch("$tag#timeout") {
+                launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#timeout" }) {
                     qsLongPressEffect.shouldWaitForTapTimeout
                         .filter { it }
                         .collect {
@@ -85,4 +88,16 @@
             }
         }
     }
+
+    @SuppressLint("ClickableViewAccessibility")
+    private fun setTouchListener(tile: QSTileViewImpl, longPressEffect: QSLongPressEffect?) {
+        tile.setOnTouchListener { _, event ->
+            when (event.actionMasked) {
+                MotionEvent.ACTION_DOWN -> longPressEffect?.handleActionDown()
+                MotionEvent.ACTION_UP -> longPressEffect?.handleActionUp()
+                MotionEvent.ACTION_CANCEL -> longPressEffect?.handleActionCancel()
+            }
+            true
+        }
+    }
 }
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 ca71870..40cf4a4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -185,8 +185,9 @@
     private val colorEvaluator = ArgbEvaluator.getInstance()
     val isLongPressEffectInitialized: Boolean
         get() = longPressEffect?.hasInitialized == true
-    @VisibleForTesting
-    var longPressEffectHandle: DisposableHandle? = null
+    private var longPressEffectHandle: DisposableHandle? = null
+    val isLongPressEffectBound: Boolean
+        get() = longPressEffectHandle != null
 
     init {
         val typedValue = TypedValue()
@@ -621,11 +622,14 @@
         // Long-press effects
         if (state.handlesLongClick &&
             longPressEffect?.initializeEffect(longPressEffectDuration) == true) {
-            // set the valid long-press effect as the touch listener
-            if (longPressEffectHandle == null) {
+            // bind the long-press effect and set it as the touch listener
+            if (!isLongPressEffectBound) {
                 longPressEffectHandle =
-                    QSLongPressEffectViewBinder.bind(this, longPressEffect, state.spec)
-                setOnTouchListener(longPressEffect)
+                    QSLongPressEffectViewBinder.bind(
+                        this,
+                        longPressEffect,
+                        state.spec,
+                    )
             }
             showRippleEffect = false
             initializeLongPressProperties()
@@ -634,8 +638,7 @@
             // handle a long-press. In this case, we go back to the behaviour of a regular tile
             // and clean-up the resources
             setOnTouchListener(null)
-            longPressEffectHandle?.dispose()
-            longPressEffectHandle = null
+            unbindLongPressEffect()
             showRippleEffect = isClickable
             initialLongPressProperties = null
             finalLongPressProperties = null
@@ -827,6 +830,11 @@
         changeCornerRadius(newRadius)
     }
 
+    private fun unbindLongPressEffect() {
+        longPressEffectHandle?.dispose()
+        longPressEffectHandle = null
+    }
+
     private fun interpolateFloat(fraction: Float, start: Float, end: Float): Float =
         start + fraction * (end - start)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index 512ca53..ecbd0f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -385,7 +385,7 @@
     }
 
     @Test
-    fun onStateChange_longPressEffectActive_withInvalidDuration_doesNotCreateEffect() {
+    fun onStateChange_longPressEffectActive_withInvalidDuration_doesNotInitializeEffect() {
         val state = QSTile.State() // A state that handles longPress
 
         // GIVEN an invalid long-press effect duration
@@ -399,7 +399,7 @@
     }
 
     @Test
-    fun onStateChange_longPressEffectActive_withValidDuration_createsEffect() {
+    fun onStateChange_longPressEffectActive_withValidDuration_initializesEffect() {
         // GIVEN a test state that handles long-press and a valid long-press effect duration
         val state = QSTile.State()
 
@@ -420,7 +420,7 @@
         tileView.changeState(state)
 
         // THEN the view binder no longer binds the view to the long-press effect
-        assertThat(tileView.longPressEffectHandle).isNull()
+        assertThat(tileView.isLongPressEffectBound).isFalse()
     }
 
     @Test
@@ -435,7 +435,7 @@
         tileView.changeState(state)
 
         // THEN the view is bounded to the long-press effect
-        assertThat(tileView.longPressEffectHandle).isNotNull()
+        assertThat(tileView.isLongPressEffectBound).isTrue()
     }
 
     @Test
@@ -451,7 +451,7 @@
         tileView.changeState(state)
 
         // THEN the view binder does not bind the view and no effect is initialized
-        assertThat(tileView.longPressEffectHandle).isNull()
+        assertThat(tileView.isLongPressEffectBound).isFalse()
         assertThat(tileView.isLongPressEffectInitialized).isFalse()
     }
 
@@ -470,7 +470,7 @@
         tileView.changeState(state)
 
         // THEN the view binder does not bind the view and no effect is initialized
-        assertThat(tileView.longPressEffectHandle).isNull()
+        assertThat(tileView.isLongPressEffectBound).isFalse()
         assertThat(tileView.isLongPressEffectInitialized).isFalse()
     }
 
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 636d509..24603ef 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
@@ -19,7 +19,6 @@
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
 
 val Kosmos.qsLongPressEffect by
-    Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardInteractor, testScope) }
+    Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardInteractor) }