summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Juan Sebastian Martinez <juansmartinez@google.com> 2024-04-11 15:15:57 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-04-11 15:15:57 +0000
commit3b92b80f0298897a137897b4423f55a13465b52a (patch)
tree790fa5e397be4b7cab6b6a888a05149738cf698e
parente3ff4ab96facc4a8d0a308aa83d8b194002d9fac (diff)
parent83c620d9bf330edc4508fcb27e0276a73ef81bae (diff)
Merge "Refactoring the quick settings long-press effect" into main
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt178
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt201
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt80
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt79
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java48
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt66
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt25
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt2
13 files changed, 461 insertions, 273 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 8f03717b42f2..3889703e74c4 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
@@ -26,39 +26,34 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.AnimatorTestRule
import com.android.systemui.coroutines.collectLastValue
+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.statusbar.VibratorHelper
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.advanceTimeBy
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.ArgumentMatchers.any
import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@SmallTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWithLooper(setAsMainLooper = true)
class QSLongPressEffectTest : SysuiTestCase() {
@Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
- @Mock private lateinit var vibratorHelper: VibratorHelper
@Mock private lateinit var testView: View
@get:Rule val animatorTestRule = AnimatorTestRule(this)
private val kosmos = testKosmos()
+ private val vibratorHelper = kosmos.vibratorHelper
private val effectDuration = 400
private val lowTickDuration = 12
@@ -68,19 +63,71 @@ class QSLongPressEffectTest : SysuiTestCase() {
@Before
fun setup() {
- whenever(
- vibratorHelper.getPrimitiveDurations(
- VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
- VibrationEffect.Composition.PRIMITIVE_SPIN,
- )
- )
- .thenReturn(intArrayOf(lowTickDuration, spinDuration))
+ vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] =
+ lowTickDuration
+ vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration
+
+ kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
longPressEffect =
QSLongPressEffect(
vibratorHelper,
- effectDuration,
+ kosmos.keyguardInteractor,
+ CoroutineScope(kosmos.backgroundCoroutineContext),
)
+ longPressEffect.initializeEffect(effectDuration)
+ }
+
+ @Test
+ fun onReset_whileIdle_resetsEffect() = testWithScope {
+ // GIVEN a call to reset
+ longPressEffect.resetEffect()
+
+ // THEN the effect remains idle and has not been initialized
+ val state by collectLastValue(longPressEffect.state)
+ assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
+ assertThat(longPressEffect.hasInitialized).isFalse()
+ }
+
+ @Test
+ fun onReset_whileRunning_resetsEffect() = testWhileRunning {
+ // GIVEN a call to reset
+ longPressEffect.resetEffect()
+
+ // THEN the effect remains idle and has not been initialized
+ val state by collectLastValue(longPressEffect.state)
+ assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
+ assertThat(longPressEffect.hasInitialized).isFalse()
+ }
+
+ @Test
+ fun onInitialize_withNegativeDuration_doesNotInitialize() = testWithScope {
+ // GIVEN an effect that has reset
+ longPressEffect.resetEffect()
+
+ // WHEN attempting to initialize with a negative duration
+ val couldInitialize = longPressEffect.initializeEffect(-1)
+
+ // THEN the effect can't initialized and remains reset
+ val state by collectLastValue(longPressEffect.state)
+ assertThat(couldInitialize).isFalse()
+ assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
+ assertThat(longPressEffect.hasInitialized).isFalse()
+ }
+
+ @Test
+ fun onInitialize_withPositiveDuration_initializes() = testWithScope {
+ // GIVEN an effect that has reset
+ longPressEffect.resetEffect()
+
+ // WHEN attempting to initialize with a positive duration
+ val couldInitialize = longPressEffect.initializeEffect(effectDuration)
+
+ // THEN the effect is initialized
+ val state by collectLastValue(longPressEffect.state)
+ assertThat(couldInitialize).isTrue()
+ assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
+ assertThat(longPressEffect.hasInitialized).isTrue()
}
@Test
@@ -90,7 +137,8 @@ class QSLongPressEffectTest : SysuiTestCase() {
longPressEffect.onTouch(testView, downEvent)
// THEN the effect moves to the TIMEOUT_WAIT state
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
+ val state by collectLastValue(longPressEffect.state)
+ assertThat(state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
}
@Test
@@ -100,7 +148,8 @@ class QSLongPressEffectTest : SysuiTestCase() {
longPressEffect.onTouch(testView, cancelEvent)
// THEN the effect goes back to idle and does not start
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+ val state by collectLastValue(longPressEffect.state)
+ assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
assertEffectDidNotStart()
}
@@ -121,7 +170,7 @@ class QSLongPressEffectTest : SysuiTestCase() {
@Test
fun onWaitComplete_whileWaiting_beginsEffect() = testWhileWaiting {
// GIVEN the pressed timeout is complete
- advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L)
+ longPressEffect.handleTimeoutComplete()
// THEN the effect starts
assertEffectStarted()
@@ -154,15 +203,28 @@ class QSLongPressEffectTest : SysuiTestCase() {
}
@Test
- fun onAnimationComplete_effectEnds() = testWhileRunning {
+ fun onAnimationComplete_keyguardDismissible_effectEndsWithLongPress() = testWhileRunning {
// GIVEN that the animation completes
animatorTestRule.advanceTimeBy(effectDuration + 10L)
- // THEN the long-press effect completes
- assertEffectCompleted()
+ // THEN the long-press effect completes with a LONG_PRESS
+ assertEffectCompleted(QSLongPressEffect.ActionType.LONG_PRESS)
}
@Test
+ fun onAnimationComplete_keyguardNotDismissible_effectEndsWithResetAndLongPress() =
+ testWhileRunning {
+ // GIVEN that the keyguard is not dismissible
+ kosmos.fakeKeyguardRepository.setKeyguardDismissible(false)
+
+ // GIVEN that the animation completes
+ animatorTestRule.advanceTimeBy(effectDuration + 10L)
+
+ // THEN the long-press effect completes with RESET_AND_LONG_PRESS
+ assertEffectCompleted(QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS)
+ }
+
+ @Test
fun onActionDown_whileRunningBackwards_resets() = testWhileRunning {
// GIVEN that the effect is at the middle of its completion (progress of 50%)
animatorTestRule.advanceTimeBy(effectDuration / 2L)
@@ -192,33 +254,21 @@ class QSLongPressEffectTest : SysuiTestCase() {
animatorTestRule.advanceTimeBy(effectDuration.toLong())
// THEN the state goes to [QSLongPressEffect.State.IDLE]
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+ val state by collectLastValue(longPressEffect.state)
+ 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 {
- // GIVEN an effect with a testing scope
- longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
-
- // THEN run the test
- test()
- }
- }
+ with(kosmos) { testScope.runTest { test() } }
private fun testWhileWaiting(test: suspend TestScope.() -> Unit) =
with(kosmos) {
testScope.runTest {
- // GIVEN an effect with a testing scope
- longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
-
// GIVEN the TIMEOUT_WAIT state is entered
- val downEvent =
- MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build()
- longPressEffect.onTouch(testView, downEvent)
+ longPressEffect.setState(QSLongPressEffect.State.TIMEOUT_WAIT)
// THEN run the test
test()
@@ -228,16 +278,9 @@ class QSLongPressEffectTest : SysuiTestCase() {
private fun testWhileRunning(test: suspend TestScope.() -> Unit) =
with(kosmos) {
testScope.runTest {
- // GIVEN an effect with a testing scope
- longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
-
- // GIVEN the down event that enters the TIMEOUT_WAIT state
- val downEvent =
- MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build()
- longPressEffect.onTouch(testView, downEvent)
-
- // GIVEN that the timeout completes and the effect starts
- advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L)
+ // GIVEN that the effect starts after the tap timeout is complete
+ longPressEffect.setState(QSLongPressEffect.State.TIMEOUT_WAIT)
+ longPressEffect.handleTimeoutComplete()
// THEN run the test
test()
@@ -252,6 +295,7 @@ class QSLongPressEffectTest : SysuiTestCase() {
*/
private fun TestScope.assertEffectStarted() {
val effectProgress by collectLastValue(longPressEffect.effectProgress)
+ val state by collectLastValue(longPressEffect.state)
val longPressHint =
LongPressHapticBuilder.createLongPressHint(
lowTickDuration,
@@ -259,10 +303,10 @@ class QSLongPressEffectTest : SysuiTestCase() {
effectDuration,
)
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
+ assertThat(state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
assertThat(effectProgress).isEqualTo(0f)
assertThat(longPressHint).isNotNull()
- verify(vibratorHelper).vibrate(longPressHint!!)
+ assertThat(vibratorHelper.hasVibratedWithEffects(longPressHint!!)).isTrue()
}
/**
@@ -274,11 +318,12 @@ class QSLongPressEffectTest : SysuiTestCase() {
*/
private fun TestScope.assertEffectDidNotStart() {
val effectProgress by collectLastValue(longPressEffect.effectProgress)
+ val state by collectLastValue(longPressEffect.state)
- assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
- assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
+ assertThat(state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
+ assertThat(state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
assertThat(effectProgress).isNull()
- verify(vibratorHelper, never()).vibrate(any(/* type= */ VibrationEffect::class.java))
+ assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
}
/**
@@ -286,18 +331,19 @@ class QSLongPressEffectTest : SysuiTestCase() {
* 1. The progress is null
* 2. The final snap haptics are played
* 3. The internal state goes back to [QSLongPressEffect.State.IDLE]
- * 4. The action to perform on the tile is the long-press action
+ * 4. The action to perform on the tile is the action given as a parameter
*/
- private fun TestScope.assertEffectCompleted() {
+ private fun TestScope.assertEffectCompleted(expectedAction: QSLongPressEffect.ActionType) {
val action by collectLastValue(longPressEffect.actionType)
val effectProgress by collectLastValue(longPressEffect.effectProgress)
val snapEffect = LongPressHapticBuilder.createSnapEffect()
+ val state by collectLastValue(longPressEffect.state)
assertThat(effectProgress).isNull()
assertThat(snapEffect).isNotNull()
- verify(vibratorHelper).vibrate(snapEffect!!)
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
- assertThat(action).isEqualTo(QSLongPressEffect.ActionType.LONG_PRESS)
+ assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue()
+ assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
+ assertThat(action).isEqualTo(expectedAction)
}
/**
@@ -305,17 +351,18 @@ class QSLongPressEffectTest : SysuiTestCase() {
* 1. The internal state is [QSLongPressEffect.State.RUNNING_BACKWARDS]
* 2. The reverse haptics plays at the point where the animation was paused
*/
- private fun assertEffectReverses(pausedProgress: Float) {
+ private fun TestScope.assertEffectReverses(pausedProgress: Float) {
val reverseHaptics =
LongPressHapticBuilder.createReversedEffect(
pausedProgress,
lowTickDuration,
effectDuration,
)
+ val state by collectLastValue(longPressEffect.state)
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
+ assertThat(state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
assertThat(reverseHaptics).isNotNull()
- verify(vibratorHelper).vibrate(reverseHaptics!!)
+ assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue()
}
/**
@@ -325,8 +372,9 @@ class QSLongPressEffectTest : SysuiTestCase() {
*/
private fun TestScope.assertEffectResets() {
val effectProgress by collectLastValue(longPressEffect.effectProgress)
- assertThat(effectProgress).isEqualTo(0f)
+ val state by collectLastValue(longPressEffect.state)
- assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
+ assertThat(effectProgress).isNull()
+ assertThat(state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
}
}
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 f1620d96b159..4327d18da97e 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -27,40 +27,65 @@ import androidx.annotation.VisibleForTesting
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 kotlinx.coroutines.CancellationException
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+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.
*
* The class is also a [View.OnTouchListener] to handle the touch events, clicks and long-press
- * gestures of the tile. The class also provides a [State] that can be used to determine the current
+ * gestures of the tile. The class also provides a [State] tha can be used to determine the current
* state of the long press effect.
*
* @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects.
* @property[effectDuration] The duration of the effect in ms.
*/
-class QSLongPressEffect(
+// TODO(b/332902869): In addition from being injectable, we can consider making it a singleton
+class QSLongPressEffect
+@Inject
+constructor(
private val vibratorHelper: VibratorHelper?,
- private val effectDuration: Int,
+ val keyguardInteractor: KeyguardInteractor,
+ @Background bgScope: CoroutineScope,
) : View.OnTouchListener {
+ private var effectDuration = 0
+
/** Current state */
- var state = State.IDLE
- @VisibleForTesting set
+ private var _state = MutableStateFlow(State.IDLE)
+ val state = _state.stateIn(bgScope, SharingStarted.Lazily, State.IDLE)
/** Flows for view control and action */
private val _effectProgress = MutableStateFlow<Float?>(null)
- val effectProgress = _effectProgress.asStateFlow()
+ val effectProgress = _effectProgress.stateIn(bgScope, SharingStarted.Lazily, null)
+
+ // Actions to perform
+ private val _postedActionType = MutableStateFlow<ActionType?>(null)
+ val actionType: StateFlow<ActionType?> =
+ combine(
+ _postedActionType,
+ keyguardInteractor.isKeyguardDismissible,
+ ) { action, isDismissible ->
+ if (!isDismissible && action == ActionType.LONG_PRESS) {
+ ActionType.RESET_AND_LONG_PRESS
+ } else {
+ action
+ }
+ }
+ .stateIn(bgScope, SharingStarted.Lazily, null)
- private val _actionType = MutableStateFlow<ActionType?>(null)
- val actionType = _actionType.asStateFlow()
+ // Should a tap timeout countdown begin
+ val shouldWaitForTapTimeout: Flow<Boolean> = state.map { it == State.TIMEOUT_WAIT }
/** Haptic effects */
private val durations =
@@ -69,41 +94,33 @@ class QSLongPressEffect(
VibrationEffect.Composition.PRIMITIVE_SPIN
)
- private val longPressHint =
- LongPressHapticBuilder.createLongPressHint(
- durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
- durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
- effectDuration
- )
+ private var longPressHint: VibrationEffect? = null
private val snapEffect = LongPressHapticBuilder.createSnapEffect()
- /* A coroutine scope and a timer job that waits for the pressedTimeout */
- var scope: CoroutineScope? = null
- private var waitJob: Job? = null
+ private var effectAnimator: ValueAnimator? = null
- private val effectAnimator =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = effectDuration.toLong()
- interpolator = AccelerateDecelerateInterpolator()
+ val hasInitialized: Boolean
+ get() = longPressHint != null && effectAnimator != null
- doOnStart { handleAnimationStart() }
- addUpdateListener { _effectProgress.value = animatedValue as Float }
- doOnEnd { handleAnimationComplete() }
- doOnCancel { handleAnimationCancel() }
- }
+ @VisibleForTesting
+ fun setState(state: State) {
+ _state.value = state
+ }
private fun reverse() {
- val pausedProgress = effectAnimator.animatedFraction
- val effect =
- LongPressHapticBuilder.createReversedEffect(
- pausedProgress,
- durations?.get(0) ?: 0,
- effectDuration,
- )
- vibratorHelper?.cancel()
- vibrate(effect)
- effectAnimator.reverse()
+ effectAnimator?.let {
+ val pausedProgress = it.animatedFraction
+ val effect =
+ LongPressHapticBuilder.createReversedEffect(
+ pausedProgress,
+ durations?.get(0) ?: 0,
+ effectDuration,
+ )
+ vibratorHelper?.cancel()
+ vibrate(effect)
+ it.reverse()
+ }
}
private fun vibrate(effect: VibrationEffect?) {
@@ -129,52 +146,37 @@ class QSLongPressEffect(
}
private fun handleActionDown() {
- when (state) {
+ when (_state.value) {
State.IDLE -> {
- startPressedTimeoutWait()
- state = State.TIMEOUT_WAIT
+ setState(State.TIMEOUT_WAIT)
}
- State.RUNNING_BACKWARDS -> effectAnimator.cancel()
+ State.RUNNING_BACKWARDS -> effectAnimator?.cancel()
else -> {}
}
}
- private fun startPressedTimeoutWait() {
- waitJob =
- scope?.launch {
- try {
- delay(PRESSED_TIMEOUT)
- handleTimeoutComplete()
- } catch (_: CancellationException) {
- state = State.IDLE
- }
- }
- }
-
private fun handleActionUp() {
- when (state) {
+ when (_state.value) {
State.TIMEOUT_WAIT -> {
- waitJob?.cancel()
- _actionType.value = ActionType.CLICK
- state = State.IDLE
+ _postedActionType.value = ActionType.CLICK
+ setState(State.IDLE)
}
State.RUNNING_FORWARD -> {
reverse()
- state = State.RUNNING_BACKWARDS
+ setState(State.RUNNING_BACKWARDS)
}
else -> {}
}
}
private fun handleActionCancel() {
- when (state) {
+ when (_state.value) {
State.TIMEOUT_WAIT -> {
- waitJob?.cancel()
- state = State.IDLE
+ setState(State.IDLE)
}
State.RUNNING_FORWARD -> {
reverse()
- state = State.RUNNING_BACKWARDS
+ setState(State.RUNNING_BACKWARDS)
}
else -> {}
}
@@ -182,54 +184,78 @@ class QSLongPressEffect(
private fun handleAnimationStart() {
vibrate(longPressHint)
- state = State.RUNNING_FORWARD
+ setState(State.RUNNING_FORWARD)
}
/** This function is called both when an animator completes or gets cancelled */
private fun handleAnimationComplete() {
- if (state == State.RUNNING_FORWARD) {
+ if (_state.value == State.RUNNING_FORWARD) {
vibrate(snapEffect)
- _actionType.value = ActionType.LONG_PRESS
+ _postedActionType.value = ActionType.LONG_PRESS
_effectProgress.value = null
}
- if (state != State.TIMEOUT_WAIT) {
+ if (_state.value != State.TIMEOUT_WAIT) {
// This will happen if the animator did not finish by being cancelled
- state = State.IDLE
+ setState(State.IDLE)
}
}
private fun handleAnimationCancel() {
- _effectProgress.value = 0f
- startPressedTimeoutWait()
- state = State.TIMEOUT_WAIT
+ _effectProgress.value = null
+ setState(State.TIMEOUT_WAIT)
}
- private fun handleTimeoutComplete() {
- if (state == State.TIMEOUT_WAIT && !effectAnimator.isRunning) {
- effectAnimator.start()
+ fun handleTimeoutComplete() {
+ if (_state.value == State.TIMEOUT_WAIT && effectAnimator?.isRunning == false) {
+ effectAnimator?.start()
}
}
fun clearActionType() {
- _actionType.value = null
+ _postedActionType.value = null
+ }
+
+ /** Reset the effect by going back to a default [IDLE] state */
+ fun resetEffect() {
+ if (effectAnimator?.isRunning == true) {
+ effectAnimator?.cancel()
+ }
+ longPressHint = null
+ effectAnimator = null
+ _effectProgress.value = null
+ _postedActionType.value = null
+ setState(State.IDLE)
}
/**
* Reset the effect with a new effect duration.
*
- * The effect will go back to an [IDLE] state where it can begin its logic with a new duration.
- *
* @param[duration] New duration for the long-press effect
+ * @return true if the effect initialized correctly
*/
- fun resetWithDuration(duration: Int) {
+ fun initializeEffect(duration: Int): Boolean {
// The effect can't reset if it is running
- if (effectAnimator.isRunning) return
+ if (duration <= 0) return false
+
+ resetEffect()
+ effectDuration = duration
+ effectAnimator =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ this.duration = effectDuration.toLong()
+ interpolator = AccelerateDecelerateInterpolator()
- effectAnimator.duration = duration.toLong()
- _effectProgress.value = 0f
- _actionType.value = null
- waitJob?.cancel()
- state = State.IDLE
+ doOnStart { handleAnimationStart() }
+ addUpdateListener { _effectProgress.value = animatedValue as Float }
+ doOnEnd { handleAnimationComplete() }
+ doOnCancel { handleAnimationCancel() }
+ }
+ longPressHint =
+ LongPressHapticBuilder.createLongPressHint(
+ durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
+ durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
+ effectDuration
+ )
+ return true
}
enum class State {
@@ -243,6 +269,7 @@ class QSLongPressEffect(
enum class ActionType {
CLICK,
LONG_PRESS,
+ RESET_AND_LONG_PRESS,
}
companion object {
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 f4998a7b8789..ddb9f35c74d9 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
@@ -21,56 +21,68 @@ import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launch
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.qs.tileimpl.QSTileViewImpl
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
-class QSLongPressEffectViewBinder {
-
- private var handle: DisposableHandle? = null
- val isBound: Boolean
- get() = handle != null
-
+// TODO(b/332903800)
+object QSLongPressEffectViewBinder {
fun bind(
tile: QSTileViewImpl,
+ qsLongPressEffect: QSLongPressEffect?,
tileSpec: String?,
- effect: QSLongPressEffect?,
- ) {
- if (effect == null) return
-
- handle =
- tile.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- effect.scope = this
- val tag = "${tileSpec ?: "unknownTileSpec"}#LongPressEffect"
+ ): DisposableHandle? {
+ if (qsLongPressEffect == null) return null
- launch("$tag#progress") {
- effect.effectProgress.collect { progress ->
- progress?.let {
- if (it == 0f) {
- tile.bringToFront()
- }
+ return tile.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ val tag = "${tileSpec ?: "unknownTileSpec"}#LongPressEffect"
+ // Progress of the effect
+ launch("$tag#progress") {
+ qsLongPressEffect.effectProgress.collect { progress ->
+ progress?.let {
+ if (it == 0f) {
+ tile.bringToFront()
+ } else {
tile.updateLongPressEffectProperties(it)
}
}
}
+ }
- launch("$tag#action") {
- effect.actionType.collect { action ->
- action?.let {
- when (it) {
- QSLongPressEffect.ActionType.CLICK -> tile.performClick()
- QSLongPressEffect.ActionType.LONG_PRESS ->
- tile.performLongClick()
+ // Action to perform
+ launch("$tag#action") {
+ qsLongPressEffect.actionType.collect { action ->
+ action?.let {
+ when (it) {
+ QSLongPressEffect.ActionType.CLICK -> tile.performClick()
+ QSLongPressEffect.ActionType.LONG_PRESS -> tile.performLongClick()
+ QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> {
+ tile.resetLongPressEffectProperties()
+ tile.performLongClick()
}
- effect.clearActionType()
}
+ qsLongPressEffect.clearActionType()
}
}
}
- }
- }
- fun dispose() {
- handle?.dispose()
- handle = null
+ // Tap timeout wait
+ launch("$tag#timeout") {
+ qsLongPressEffect.shouldWaitForTapTimeout
+ .filter { it }
+ .collect {
+ try {
+ delay(QSLongPressEffect.PRESSED_TIMEOUT)
+ qsLongPressEffect.handleTimeoutComplete()
+ } catch (_: CancellationException) {
+ qsLongPressEffect.resetEffect()
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 55dc4859cf90..b8c3c1a2af5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -29,6 +29,7 @@ import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.qs.QSLongPressEffect;
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
import com.android.systemui.media.controls.ui.view.MediaHost;
import com.android.systemui.media.controls.ui.view.MediaHostState;
@@ -41,13 +42,13 @@ import com.android.systemui.settings.brightness.BrightnessController;
import com.android.systemui.settings.brightness.BrightnessMirrorHandler;
import com.android.systemui.settings.brightness.BrightnessSliderController;
import com.android.systemui.settings.brightness.MirrorController;
-import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.tuner.TunerService;
import javax.inject.Inject;
import javax.inject.Named;
+import javax.inject.Provider;
/**
* Controller for {@link QSPanel}.
@@ -94,10 +95,10 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
SplitShadeStateController splitShadeStateController,
SceneContainerFlags sceneContainerFlags,
- VibratorHelper vibratorHelper) {
+ Provider<QSLongPressEffect> longPRessEffectProvider) {
super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost,
metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController,
- vibratorHelper);
+ longPRessEffectProvider);
mTunerService = tunerService;
mQsCustomizerController = qsCustomizerController;
mQsTileRevealControllerFactory = qsTileRevealControllerFactory;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index d8e81875bbbf..583cfb9ab47e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -17,6 +17,7 @@
package com.android.systemui.qs;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import static com.android.systemui.Flags.quickSettingsVisualHapticsLongpress;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -32,6 +33,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.Dumpable;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.qs.QSLongPressEffect;
import com.android.systemui.media.controls.ui.view.MediaHost;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
@@ -39,7 +41,6 @@ import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileViewImpl;
-import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.animation.DisappearParameters;
@@ -55,6 +56,8 @@ import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
+import javax.inject.Provider;
+
/**
* Controller for QSPanel views.
*
@@ -88,7 +91,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr
private SplitShadeStateController mSplitShadeStateController;
- private final VibratorHelper mVibratorHelper;
+ private final Provider<QSLongPressEffect> mLongPressEffectProvider;
@VisibleForTesting
protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
@@ -148,7 +151,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr
QSLogger qsLogger,
DumpManager dumpManager,
SplitShadeStateController splitShadeStateController,
- VibratorHelper vibratorHelper
+ Provider<QSLongPressEffect> longPressEffectProvider
) {
super(view);
mHost = host;
@@ -162,7 +165,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr
mSplitShadeStateController = splitShadeStateController;
mShouldUseSplitNotificationShade =
mSplitShadeStateController.shouldUseSplitNotificationShade(getResources());
- mVibratorHelper = vibratorHelper;
+ mLongPressEffectProvider = longPressEffectProvider;
}
@Override
@@ -305,8 +308,14 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr
}
private void addTile(final QSTile tile, boolean collapsedView) {
+ QSLongPressEffect longPressEffect;
+ if (quickSettingsVisualHapticsLongpress()) {
+ longPressEffect = mLongPressEffectProvider.get();
+ } else {
+ longPressEffect = null;
+ }
final QSTileViewImpl tileView = new QSTileViewImpl(
- getContext(), collapsedView, mVibratorHelper);
+ getContext(), collapsedView, longPressEffect);
final TileRecord r = new TileRecord(tile, tileView);
// TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of
// b/250618218.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 05bb08813cc5..6cda740dd1a8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -25,6 +25,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.qs.QSLongPressEffect;
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
import com.android.systemui.media.controls.ui.view.MediaHost;
import com.android.systemui.plugins.qs.QSTile;
@@ -32,7 +33,6 @@ import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.res.R;
-import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.util.leak.RotationUtils;
@@ -58,10 +58,11 @@ public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel>
Provider<Boolean> usingCollapsedLandscapeMediaProvider,
MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
DumpManager dumpManager, SplitShadeStateController splitShadeStateController,
- VibratorHelper vibratorHelper
+ Provider<QSLongPressEffect> longPressEffectProvider
) {
super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
- uiEventLogger, qsLogger, dumpManager, splitShadeStateController, vibratorHelper);
+ uiEventLogger, qsLogger, dumpManager, splitShadeStateController,
+ longPressEffectProvider);
mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider;
}
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 30044856a7d4..ca71870845e0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -62,15 +62,15 @@ import com.android.systemui.plugins.qs.QSTileView
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
import com.android.systemui.res.R
-import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.children
+import kotlinx.coroutines.DisposableHandle
import java.util.Objects
private const val TAG = "QSTileViewImpl"
open class QSTileViewImpl @JvmOverloads constructor(
context: Context,
private val collapsed: Boolean = false,
- private val vibratorHelper: VibratorHelper? = null,
+ private val longPressEffect: QSLongPressEffect? = null,
) : QSTileView(context), HeightOverrideable, LaunchableView {
companion object {
@@ -180,15 +180,13 @@ open class QSTileViewImpl @JvmOverloads constructor(
private val locInScreen = IntArray(2)
/** Visuo-haptic long-press effects */
- private var longPressEffect: QSLongPressEffect? = null
- private val longPressEffectViewBinder = QSLongPressEffectViewBinder()
private var initialLongPressProperties: QSLongPressProperties? = null
private var finalLongPressProperties: QSLongPressProperties? = null
private val colorEvaluator = ArgbEvaluator.getInstance()
- val hasLongPressEffect: Boolean
- get() = longPressEffect != null
- @VisibleForTesting val isLongPressEffectBound: Boolean
- get() = longPressEffectViewBinder.isBound
+ val isLongPressEffectInitialized: Boolean
+ get() = longPressEffect?.hasInitialized == true
+ @VisibleForTesting
+ var longPressEffectHandle: DisposableHandle? = null
init {
val typedValue = TypedValue()
@@ -325,6 +323,13 @@ open class QSTileViewImpl @JvmOverloads constructor(
}
private fun updateHeight() {
+ // TODO(b/332900989): Find a more robust way of resetting the tile if not reset by the
+ // launch animation.
+ if (scaleX != 1f || scaleY != 1f) {
+ // The launch animation of a long-press effect did not reset the long-press effect so
+ // we must do it here
+ resetLongPressEffectProperties()
+ }
val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
heightOverride
} else {
@@ -614,25 +619,26 @@ open class QSTileViewImpl @JvmOverloads constructor(
lastIconTint = icon.getColor(state)
// Long-press effects
- if (quickSettingsVisualHapticsLongpress()){
- if (state.handlesLongClick && maybeCreateAndInitializeLongPressEffect()) {
- // set the valid long-press effect as the touch listener
- showRippleEffect = false
+ if (state.handlesLongClick &&
+ longPressEffect?.initializeEffect(longPressEffectDuration) == true) {
+ // set the valid long-press effect as the touch listener
+ if (longPressEffectHandle == null) {
+ longPressEffectHandle =
+ QSLongPressEffectViewBinder.bind(this, longPressEffect, state.spec)
setOnTouchListener(longPressEffect)
- if (!longPressEffectViewBinder.isBound) {
- longPressEffectViewBinder.bind(this, state.spec, longPressEffect)
- }
- } else {
- // Long-press effects might have been enabled before but the new state does not
- // handle a long-press. In this case, we go back to the behaviour of a regular tile
- // and clean-up the resources
- longPressEffectViewBinder.dispose()
- showRippleEffect = isClickable
- setOnTouchListener(null)
- longPressEffect = null
- initialLongPressProperties = null
- finalLongPressProperties = null
}
+ showRippleEffect = false
+ initializeLongPressProperties()
+ } else {
+ // Long-press effects might have been enabled before but the new state does not
+ // 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
+ showRippleEffect = isClickable
+ initialLongPressProperties = null
+ finalLongPressProperties = null
}
}
@@ -824,7 +830,7 @@ open class QSTileViewImpl @JvmOverloads constructor(
private fun interpolateFloat(fraction: Float, start: Float, end: Float): Float =
start + fraction * (end - start)
- private fun resetLongPressEffectProperties() {
+ fun resetLongPressEffectProperties() {
scaleY = 1f
scaleX = 1f
for (child in children) {
@@ -842,27 +848,6 @@ open class QSTileViewImpl @JvmOverloads constructor(
icon.setTint(icon.mIcon as ImageView, lastIconTint)
}
- private fun maybeCreateAndInitializeLongPressEffect(): Boolean {
- // Don't setup the effect if the long-press duration is invalid
- val effectDuration = longPressEffectDuration
- if (effectDuration <= 0) {
- longPressEffect = null
- return false
- }
-
- initializeLongPressProperties()
- if (longPressEffect == null) {
- longPressEffect =
- QSLongPressEffect(
- vibratorHelper,
- effectDuration,
- )
- } else {
- longPressEffect?.resetWithDuration(effectDuration)
- }
- return true
- }
-
private fun initializeLongPressProperties() {
initialLongPressProperties =
QSLongPressProperties(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 0101741a9242..542bfaaa8484 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.qs;
+import static com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
@@ -33,6 +35,8 @@ import static org.mockito.Mockito.when;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.view.ContextThemeWrapper;
@@ -45,13 +49,14 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.logging.testing.UiEventLoggerFake;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.qs.QSLongPressEffect;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.media.controls.ui.view.MediaHost;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.res.R;
-import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
import com.android.systemui.util.animation.DisappearParameters;
@@ -66,11 +71,14 @@ import java.io.StringWriter;
import java.util.Collections;
import java.util.List;
+import javax.inject.Provider;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class QSPanelControllerBaseTest extends SysuiTestCase {
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
@Mock
private QSPanel mQSPanel;
@Mock
@@ -101,8 +109,8 @@ public class QSPanelControllerBaseTest extends SysuiTestCase {
Configuration mConfiguration;
@Mock
Runnable mHorizontalLayoutListener;
- @Mock
- VibratorHelper mVibratorHelper;
+ private TestableLongPressEffectProvider mLongPressEffectProvider =
+ new TestableLongPressEffectProvider();
private QSPanelControllerBase<QSPanel> mController;
@@ -114,7 +122,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase {
DumpManager dumpManager) {
super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger,
qsLogger, dumpManager, new ResourcesSplitShadeStateController(),
- mVibratorHelper);
+ mLongPressEffectProvider);
}
@Override
@@ -123,6 +131,17 @@ public class QSPanelControllerBaseTest extends SysuiTestCase {
}
}
+ private class TestableLongPressEffectProvider implements Provider<QSLongPressEffect> {
+
+ private int mEffectsProvided = 0;
+
+ @Override
+ public QSLongPressEffect get() {
+ mEffectsProvided++;
+ return mKosmos.getQsLongPressEffect();
+ }
+ }
+
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -421,6 +440,27 @@ public class QSPanelControllerBaseTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
+ public void setTiles_longPressEffectEnabled_nonNullLongPressEffectsAreProvided() {
+ mLongPressEffectProvider.mEffectsProvided = 0;
+ when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
+ mController.setTiles();
+
+ // There is one non-null effect provided for each tile in the host
+ assertThat(mLongPressEffectProvider.mEffectsProvided).isEqualTo(2);
+ }
+
+ @Test
+ @DisableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
+ public void setTiles_longPressEffectDisabled_noLongPressEffectsAreProvided() {
+ mLongPressEffectProvider.mEffectsProvided = 0;
+ when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
+ mController.setTiles();
+
+ assertThat(mLongPressEffectProvider.mEffectsProvided).isEqualTo(0);
+ }
+
+ @Test
public void setTiles_differentTiles_extraTileRemoved() {
when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
mController.setTiles();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 916e8ddb6e8a..a60494f87fb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -7,19 +7,19 @@ import android.testing.TestableResources
import android.view.ContextThemeWrapper
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.UiEventLogger
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
+import com.android.systemui.haptics.qs.QSLongPressEffect
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.customize.QSCustomizerController
import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
import com.android.systemui.settings.brightness.BrightnessController
import com.android.systemui.settings.brightness.BrightnessSliderController
-import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
import com.android.systemui.tuner.TunerService
@@ -36,6 +36,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import javax.inject.Provider
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -62,7 +63,7 @@ class QSPanelControllerTest : SysuiTestCase() {
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var configuration: Configuration
@Mock private lateinit var pagedTileLayout: PagedTileLayout
- @Mock private lateinit var vibratorHelper: VibratorHelper
+ @Mock private lateinit var longPressEffectProvider: Provider<QSLongPressEffect>
private val sceneContainerFlags = FakeSceneContainerFlags()
@@ -103,7 +104,7 @@ class QSPanelControllerTest : SysuiTestCase() {
statusBarKeyguardViewManager,
ResourcesSplitShadeStateController(),
sceneContainerFlags,
- vibratorHelper,
+ longPressEffectProvider,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 71a9a8b3318f..1eb0a51bcaf6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -22,15 +22,15 @@ import android.view.ContextThemeWrapper
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
+import com.android.systemui.haptics.qs.QSLongPressEffect
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.customize.QSCustomizerController
import com.android.systemui.qs.logging.QSLogger
-import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
import com.android.systemui.util.leak.RotationUtils
import org.junit.After
@@ -45,6 +45,7 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import javax.inject.Provider
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -60,7 +61,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() {
@Mock private lateinit var tile: QSTile
@Mock private lateinit var tileLayout: TileLayout
@Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
- @Mock private lateinit var vibratorHelper: VibratorHelper
+ @Mock private lateinit var longPressEffectProvider: Provider<QSLongPressEffect>
private val uiEventLogger = UiEventLoggerFake()
private val dumpManager = DumpManager()
@@ -92,7 +93,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() {
uiEventLogger,
qsLogger,
dumpManager,
- vibratorHelper,
+ longPressEffectProvider,
)
controller.init()
@@ -161,7 +162,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() {
uiEventLogger: UiEventLoggerFake,
qsLogger: QSLogger,
dumpManager: DumpManager,
- vibratorHelper: VibratorHelper,
+ longPressEffectProvider: Provider<QSLongPressEffect>,
) :
QuickQSPanelController(
view,
@@ -175,7 +176,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() {
qsLogger,
dumpManager,
ResourcesSplitShadeStateController(),
- vibratorHelper,
+ longPressEffectProvider,
) {
private var rotation = RotationUtils.ROTATION_NONE
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 2b1ac915f430..512ca5315530 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
@@ -18,7 +18,6 @@ package com.android.systemui.qs.tileimpl
import android.content.Context
import android.graphics.drawable.Drawable
-import android.platform.test.annotations.EnableFlags
import android.service.quicksettings.Tile
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
@@ -28,10 +27,12 @@ import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.TextView
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.haptics.qs.QSLongPressEffect
+import com.android.systemui.haptics.qs.qsLongPressEffect
import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -50,13 +51,14 @@ class QSTileViewImplTest : SysuiTestCase() {
private lateinit var tileView: FakeTileView
private lateinit var customDrawableView: View
private lateinit var chevronView: View
+ private val kosmos = testKosmos()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
context.ensureTestableResources()
- tileView = FakeTileView(context, false)
+ tileView = FakeTileView(context, false, kosmos.qsLongPressEffect)
customDrawableView = tileView.requireViewById(R.id.customDrawable)
chevronView = tileView.requireViewById(R.id.chevron)
}
@@ -383,7 +385,6 @@ class QSTileViewImplTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
fun onStateChange_longPressEffectActive_withInvalidDuration_doesNotCreateEffect() {
val state = QSTile.State() // A state that handles longPress
@@ -393,12 +394,11 @@ class QSTileViewImplTest : SysuiTestCase() {
// WHEN the state changes
tileView.changeState(state)
- // THEN the long-press effect is not created
- assertThat(tileView.hasLongPressEffect).isFalse()
+ // THEN the long-press effect is not initialized
+ assertThat(tileView.isLongPressEffectInitialized).isFalse()
}
@Test
- @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
fun onStateChange_longPressEffectActive_withValidDuration_createsEffect() {
// GIVEN a test state that handles long-press and a valid long-press effect duration
val state = QSTile.State()
@@ -406,12 +406,11 @@ class QSTileViewImplTest : SysuiTestCase() {
// WHEN the state changes
tileView.changeState(state)
- // THEN the long-press effect created
- assertThat(tileView.hasLongPressEffect).isTrue()
+ // THEN the long-press effect is initialized
+ assertThat(tileView.isLongPressEffectInitialized).isTrue()
}
@Test
- @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
fun onStateChange_fromLongPress_to_noLongPress_unBoundsTile() {
// GIVEN a state that no longer handles long-press
val state = QSTile.State()
@@ -421,11 +420,10 @@ class QSTileViewImplTest : SysuiTestCase() {
tileView.changeState(state)
// THEN the view binder no longer binds the view to the long-press effect
- assertThat(tileView.isLongPressEffectBound).isFalse()
+ assertThat(tileView.longPressEffectHandle).isNull()
}
@Test
- @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
fun onStateChange_fromNoLongPress_to_longPress_bindsTile() {
// GIVEN that the tile has changed to a state that does not handle long-press
val state = QSTile.State()
@@ -437,15 +435,53 @@ class QSTileViewImplTest : SysuiTestCase() {
tileView.changeState(state)
// THEN the view is bounded to the long-press effect
- assertThat(tileView.isLongPressEffectBound).isTrue()
+ assertThat(tileView.longPressEffectHandle).isNotNull()
+ }
+
+ @Test
+ fun onStateChange_withoutLongPressEffect_fromLongPress_to_noLongPress_neverBindsEffect() {
+ // GIVEN a tile where the long-press effect is null
+ tileView = FakeTileView(context, false, null)
+
+ // GIVEN a state that no longer handles long-press
+ val state = QSTile.State()
+ state.handlesLongClick = false
+
+ // WHEN the state changes
+ tileView.changeState(state)
+
+ // THEN the view binder does not bind the view and no effect is initialized
+ assertThat(tileView.longPressEffectHandle).isNull()
+ assertThat(tileView.isLongPressEffectInitialized).isFalse()
+ }
+
+ @Test
+ fun onStateChange_withoutLongPressEffect_fromNoLongPress_to_longPress_neverBindsEffect() {
+ // GIVEN a tile where the long-press effect is null
+ tileView = FakeTileView(context, false, null)
+
+ // GIVEN that the tile has changed to a state that does not handle long-press
+ val state = QSTile.State()
+ state.handlesLongClick = false
+ tileView.changeState(state)
+
+ // WHEN the state changes back to handling long-press
+ state.handlesLongClick = true
+ tileView.changeState(state)
+
+ // THEN the view binder does not bind the view and no effect is initialized
+ assertThat(tileView.longPressEffectHandle).isNull()
+ assertThat(tileView.isLongPressEffectInitialized).isFalse()
}
class FakeTileView(
context: Context,
- collapsed: Boolean
+ collapsed: Boolean,
+ longPressEffect: QSLongPressEffect?,
) : QSTileViewImpl(
ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings),
- collapsed
+ collapsed,
+ longPressEffect,
) {
var constantLongPressEffectDuration = 500
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
new file mode 100644
index 000000000000..636d509663a2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.qs
+
+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) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index a46d35842cf3..fdc3e0a22627 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -33,6 +33,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor
+import com.android.systemui.haptics.qs.qsLongPressEffect
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -110,6 +111,7 @@ class KosmosJavaAdapter(
kosmos.sharedNotificationContainerInteractor
}
val brightnessMirrorShowingInteractor by lazy { kosmos.brightnessMirrorShowingInteractor }
+ val qsLongPressEffect by lazy { kosmos.qsLongPressEffect }
init {
kosmos.applicationContext = testCase.context