summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Juan Sebastian Martinez <juansmartinez@google.com> 2024-10-24 17:01:57 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-10-24 17:01:57 +0000
commit2b52c719d6eeb94162fa4cf0c89d7e9b4eefe296 (patch)
tree25c06abdb9a7332f7355e3f98dea5bdcc74c4fae
parent0c6f881e25f47e93d9617341ba6cbd855c3e111f (diff)
parenta5a63692fc6530f106353a7e0801289b442cebd7 (diff)
Merge "Adding a TileHapticsViewModel for haptic playback on QS tiles." into main
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt166
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/StateAwareExpandable.kt93
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt190
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/TileHapticsViewModelFactoryKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModelKosmos.kt25
13 files changed, 568 insertions, 10 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt
new file mode 100644
index 000000000000..5efb6171cdde
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.msdl.qs
+
+import android.service.quicksettings.Tile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
+import com.android.systemui.haptics.msdl.tileHapticsViewModelFactory
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.panels.ui.viewmodel.fakeQsTile
+import com.android.systemui.qs.panels.ui.viewmodel.tileViewModel
+import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class TileHapticsViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val qsTile = kosmos.fakeQsTile
+ private val msdlPlayer = kosmos.fakeMSDLPlayer
+ private val tileViewModel = kosmos.tileViewModel
+
+ private val underTest = kosmos.tileHapticsViewModelFactory.create(tileViewModel)
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ fun whenTileTogglesOnFromClick_playsSwitchOnHaptics() =
+ testScope.runTest {
+ // WHEN the tile toggles on after being clicked
+ underTest.setTileInteractionState(TileHapticsViewModel.TileInteractionState.CLICKED)
+ toggleOn()
+
+ // THEN the switch on token plays
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWITCH_ON)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @Test
+ fun whenTileTogglesOffFromClick_playsSwitchOffHaptics() =
+ testScope.runTest {
+ // WHEN the tile toggles off after being clicked
+ underTest.setTileInteractionState(TileHapticsViewModel.TileInteractionState.CLICKED)
+ toggleOff()
+
+ // THEN the switch off token plays
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWITCH_OFF)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @Test
+ fun whenTileTogglesOnWhileIdle_doesNotPlaySwitchOnHaptics() =
+ testScope.runTest {
+ // WHEN the tile toggles on without being clicked
+ toggleOn()
+
+ // THEN no token plays
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @Test
+ fun whenTileTogglesOffWhileIdle_doesNotPlaySwitchOffHaptics() =
+ testScope.runTest {
+ // WHEN the tile toggles off without being clicked
+ toggleOff()
+
+ // THEN no token plays
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @Test
+ fun whenLaunchingFromLongClick_playsLongPressHaptics() =
+ testScope.runTest {
+ // WHEN the tile is long-clicked and its action state changes accordingly
+ underTest.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+ )
+ // WHEN the activity transition (from the long-click) starts
+ underTest.onActivityLaunchTransitionStart()
+ runCurrent()
+
+ // THEN the long-press token plays
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.LONG_PRESS)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @Test
+ fun onLongClick_whenTileDoesNotHandleLongClick_playsFailureHaptics() =
+ testScope.runTest {
+ // WHEN the tile is long-clicked but the tile does not handle a long-click
+ val state = QSTile.State().apply { handlesLongClick = false }
+ qsTile.changeState(state)
+ underTest.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+ )
+ runCurrent()
+
+ // THEN the failure token plays
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @Test
+ fun whenLaunchingFromClick_doesNotPlayHaptics() =
+ testScope.runTest {
+ // WHEN the tile is clicked and its action state changes accordingly
+ underTest.setTileInteractionState(TileHapticsViewModel.TileInteractionState.CLICKED)
+ // WHEN an activity transition starts (from clicking)
+ underTest.onActivityLaunchTransitionStart()
+ runCurrent()
+
+ // THEN no haptics play
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ private fun TestScope.toggleOn() {
+ qsTile.changeState(QSTile.State().apply { state = Tile.STATE_INACTIVE })
+ runCurrent()
+
+ qsTile.changeState(QSTile.State().apply { state = Tile.STATE_ACTIVE })
+ runCurrent()
+ }
+
+ private fun TestScope.toggleOff() {
+ qsTile.changeState(QSTile.State().apply { state = Tile.STATE_ACTIVE })
+ runCurrent()
+
+ qsTile.changeState(QSTile.State().apply { state = Tile.STATE_INACTIVE })
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/StateAwareExpandable.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/StateAwareExpandable.kt
new file mode 100644
index 000000000000..215ceacaef14
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/StateAwareExpandable.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.msdl.qs
+
+import android.content.ComponentName
+import android.view.ViewGroup
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+
+private fun ActivityTransitionAnimator.Controller.withStateAwareness(
+ onActivityLaunchTransitionStart: () -> Unit,
+ onActivityLaunchTransitionEnd: () -> Unit,
+): ActivityTransitionAnimator.Controller {
+ val delegate = this
+ return object : ActivityTransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ onActivityLaunchTransitionStart()
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
+ }
+
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ onActivityLaunchTransitionEnd()
+ delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+ }
+
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ onActivityLaunchTransitionEnd()
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
+ }
+ }
+}
+
+private fun DialogTransitionAnimator.Controller.withStateAwareness(
+ onDialogDrawingStart: () -> Unit,
+ onDialogDrawingEnd: () -> Unit,
+): DialogTransitionAnimator.Controller {
+ val delegate = this
+ return object : DialogTransitionAnimator.Controller by delegate {
+
+ override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+ onDialogDrawingStart()
+ delegate.startDrawingInOverlayOf(viewGroup)
+ }
+
+ override fun stopDrawingInOverlay() {
+ onDialogDrawingEnd()
+ delegate.stopDrawingInOverlay()
+ }
+ }
+}
+
+fun Expandable.withStateAwareness(
+ onDialogDrawingStart: () -> Unit,
+ onDialogDrawingEnd: () -> Unit,
+ onActivityLaunchTransitionStart: () -> Unit,
+ onActivityLaunchTransitionEnd: () -> Unit,
+): Expandable {
+ val delegate = this
+ return object : Expandable {
+ override fun activityTransitionController(
+ launchCujType: Int?,
+ cookie: ActivityTransitionAnimator.TransitionCookie?,
+ component: ComponentName?,
+ returnCujType: Int?,
+ ): ActivityTransitionAnimator.Controller? =
+ delegate
+ .activityTransitionController(launchCujType, cookie, component, returnCujType)
+ ?.withStateAwareness(onActivityLaunchTransitionStart, onActivityLaunchTransitionEnd)
+
+ override fun dialogTransitionController(
+ cuj: DialogCuj?
+ ): DialogTransitionAnimator.Controller? =
+ delegate
+ .dialogTransitionController(cuj)
+ ?.withStateAwareness(onDialogDrawingStart, onDialogDrawingEnd)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
new file mode 100644
index 000000000000..79059506b727
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.msdl.qs
+
+import android.service.quicksettings.Tile
+import com.android.systemui.Flags
+import com.android.systemui.animation.Expandable
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.util.kotlin.pairwise
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.transform
+
+/** A view-model to trigger haptics feedback on Quick Settings tiles */
+@OptIn(ExperimentalCoroutinesApi::class)
+class TileHapticsViewModel
+@AssistedInject
+constructor(
+ private val msdlPlayer: MSDLPlayer,
+ @Assisted private val tileViewModel: TileViewModel,
+) : ExclusiveActivatable() {
+
+ private val tileInteractionState = MutableStateFlow(TileInteractionState.IDLE)
+ private val tileAnimationState = MutableStateFlow(TileAnimationState.IDLE)
+ private val canPlayToggleHaptics: Boolean
+ get() =
+ tileAnimationState.value == TileAnimationState.IDLE &&
+ tileInteractionState.value == TileInteractionState.CLICKED
+
+ val isIdle: Boolean
+ get() =
+ tileAnimationState.value == TileAnimationState.IDLE &&
+ tileInteractionState.value == TileInteractionState.IDLE
+
+ private val toggleHapticsState: Flow<TileHapticsState> =
+ tileViewModel.state
+ .mapLatest { it.state }
+ .pairwise()
+ .transform { (previous, current) ->
+ val toggleState =
+ when {
+ !canPlayToggleHaptics -> TileHapticsState.NO_HAPTICS
+ previous == Tile.STATE_INACTIVE && current == Tile.STATE_ACTIVE ->
+ TileHapticsState.TOGGLE_ON
+ previous == Tile.STATE_ACTIVE && current == Tile.STATE_INACTIVE ->
+ TileHapticsState.TOGGLE_OFF
+ else -> TileHapticsState.NO_HAPTICS
+ }
+ emit(toggleState)
+ }
+ .distinctUntilChanged()
+
+ private val interactionHapticsState: Flow<TileHapticsState> =
+ combine(tileInteractionState, tileAnimationState) { interactionState, animationState ->
+ when {
+ interactionState == TileInteractionState.LONG_CLICKED &&
+ animationState == TileAnimationState.ACTIVITY_LAUNCH ->
+ TileHapticsState.LONG_PRESS
+ interactionState == TileInteractionState.LONG_CLICKED &&
+ !tileViewModel.currentState.handlesLongClick ->
+ TileHapticsState.FAILED_LONGPRESS
+ else -> TileHapticsState.NO_HAPTICS
+ }
+ }
+ .distinctUntilChanged()
+
+ private val hapticsState: Flow<TileHapticsState> =
+ merge(toggleHapticsState, interactionHapticsState)
+
+ override suspend fun onActivated(): Nothing {
+ try {
+ hapticsState.collect { hapticsState ->
+ val tokenToPlay: MSDLToken? =
+ when (hapticsState) {
+ TileHapticsState.TOGGLE_ON -> MSDLToken.SWITCH_ON
+ TileHapticsState.TOGGLE_OFF -> MSDLToken.SWITCH_OFF
+ TileHapticsState.LONG_PRESS -> MSDLToken.LONG_PRESS
+ TileHapticsState.FAILED_LONGPRESS -> MSDLToken.FAILURE
+ TileHapticsState.NO_HAPTICS -> null
+ }
+ tokenToPlay?.let {
+ msdlPlayer.playToken(it)
+ resetStates()
+ }
+ }
+ awaitCancellation()
+ } finally {
+ resetStates()
+ }
+ }
+
+ private fun resetStates() {
+ tileInteractionState.value = TileInteractionState.IDLE
+ tileAnimationState.value = TileAnimationState.IDLE
+ }
+
+ fun onDialogDrawingStart() {
+ tileAnimationState.value = TileAnimationState.DIALOG_LAUNCH
+ }
+
+ fun onDialogDrawingEnd() {
+ tileAnimationState.value = TileAnimationState.IDLE
+ }
+
+ fun onActivityLaunchTransitionStart() {
+ tileAnimationState.value = TileAnimationState.ACTIVITY_LAUNCH
+ }
+
+ fun onActivityLaunchTransitionEnd() {
+ tileAnimationState.value = TileAnimationState.IDLE
+ }
+
+ fun setTileInteractionState(actionState: TileInteractionState) {
+ tileInteractionState.value = actionState
+ }
+
+ fun createStateAwareExpandable(baseExpandable: Expandable): Expandable =
+ baseExpandable.withStateAwareness(
+ onDialogDrawingStart = ::onDialogDrawingStart,
+ onDialogDrawingEnd = ::onDialogDrawingEnd,
+ onActivityLaunchTransitionStart = ::onActivityLaunchTransitionStart,
+ onActivityLaunchTransitionEnd = ::onActivityLaunchTransitionEnd,
+ )
+
+ /** Models the state of toggle haptics to play */
+ enum class TileHapticsState {
+ TOGGLE_ON,
+ TOGGLE_OFF,
+ LONG_PRESS,
+ FAILED_LONGPRESS,
+ NO_HAPTICS,
+ }
+
+ /** Models the interaction that took place on the tile */
+ enum class TileInteractionState {
+ IDLE,
+ CLICKED,
+ LONG_CLICKED,
+ }
+
+ /** Models the animation state of dialogs and activity launches from a tile */
+ enum class TileAnimationState {
+ IDLE,
+ DIALOG_LAUNCH,
+ ACTIVITY_LAUNCH,
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(tileViewModel: TileViewModel): TileHapticsViewModel
+ }
+}
+
+class TileHapticsViewModelFactoryProvider
+@Inject
+constructor(private val tileHapticsViewModelFactory: TileHapticsViewModel.Factory) {
+ fun getHapticsViewModelFactory(): TileHapticsViewModel.Factory? =
+ if (Flags.msdlFeedback()) {
+ tileHapticsViewModelFactory
+ } else {
+ null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index 1c7a334d3ef2..99a6cda8cbf5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -73,6 +73,7 @@ fun SceneScope.QuickQuickSettings(
squishiness = { squishiness },
coroutineScope = scope,
bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
+ tileHapticsViewModelFactoryProvider = viewModel.tileHapticsViewModelFactoryProvider,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 28f546300d40..978a3534e95b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -59,6 +59,7 @@ import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.thenIf
+import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
@@ -97,6 +98,7 @@ fun LargeTileContent(
onClick = toggleClick!!,
onLongClick = onLongClick,
onLongClickLabel = longPressLabel,
+ hapticFeedbackEnabled = !Flags.msdlFeedback(),
)
.thenIf(accessibilityUiState != null) {
Modifier.semantics {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 366bc9a7a29a..91f2da2b90f7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -28,6 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
+import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
@@ -49,6 +50,7 @@ class InfiniteGridLayout
constructor(
private val iconTilesViewModel: IconTilesViewModel,
private val viewModelFactory: InfiniteGridViewModel.Factory,
+ private val tileHapticsViewModelFactoryProvider: TileHapticsViewModelFactoryProvider,
) : PaginatableGridLayout {
@Composable
@@ -92,6 +94,7 @@ constructor(
iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
squishiness = { squishiness },
+ tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider,
coroutineScope = scope,
bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index 4104e53cfcb0..e1583e3fa3d3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -64,9 +64,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.Expandable
import com.android.compose.animation.bounceable
import com.android.compose.modifiers.thenIf
+import com.android.systemui.Flags
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.haptics.msdl.qs.TileHapticsViewModel
+import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.panels.ui.compose.BounceableInfo
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
@@ -109,6 +113,7 @@ fun Tile(
squishiness: () -> Float,
coroutineScope: CoroutineScope,
bounceableInfo: BounceableInfo,
+ tileHapticsViewModelFactoryProvider: TileHapticsViewModelFactoryProvider,
modifier: Modifier = Modifier,
) {
val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
@@ -116,6 +121,10 @@ fun Tile(
val resources = resources()
val uiState = remember(state, resources) { state.toUiState(resources) }
val colors = TileDefaults.getColorForState(uiState)
+ val hapticsViewModel: TileHapticsViewModel? =
+ rememberViewModel(traceName = "TileHapticsViewModel") {
+ tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile)
+ }
// TODO(b/361789146): Draw the shapes instead of clipping
val tileShape = TileDefaults.animateTileShape(uiState.state)
@@ -129,6 +138,7 @@ fun Tile(
},
shape = tileShape,
squishiness = squishiness,
+ hapticsViewModel = hapticsViewModel,
modifier =
modifier
.fillMaxWidth()
@@ -143,11 +153,19 @@ fun Tile(
TileContainer(
onClick = {
tile.onClick(expandable)
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.CLICKED
+ )
if (uiState.accessibilityUiState.toggleableState != null) {
coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() }
}
},
- onLongClick = { tile.onLongClick(expandable) },
+ onLongClick = {
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+ )
+ tile.onLongClick(expandable)
+ },
uiState = uiState,
iconOnly = iconOnly,
) {
@@ -161,9 +179,21 @@ fun Tile(
} else {
val iconShape = TileDefaults.animateIconShape(uiState.state)
val secondaryClick: (() -> Unit)? =
- { tile.onSecondaryClick() }.takeIf { uiState.handlesSecondaryClick }
+ {
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.CLICKED
+ )
+ tile.onSecondaryClick()
+ }
+ .takeIf { uiState.handlesSecondaryClick }
val longClick: (() -> Unit)? =
- { tile.onLongClick(expandable) }.takeIf { uiState.handlesLongClick }
+ {
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+ )
+ tile.onLongClick(expandable)
+ }
+ .takeIf { uiState.handlesLongClick }
LargeTileContent(
label = uiState.label,
secondaryLabel = uiState.secondaryLabel,
@@ -185,6 +215,7 @@ private fun TileExpandable(
color: Color,
shape: Shape,
squishiness: () -> Float,
+ hapticsViewModel: TileHapticsViewModel?,
modifier: Modifier = Modifier,
content: @Composable (Expandable) -> Unit,
) {
@@ -193,7 +224,7 @@ private fun TileExpandable(
shape = shape,
modifier = modifier.clip(shape).verticalSquish(squishiness),
) {
- content(it)
+ content(hapticsViewModel?.createStateAwareExpandable(it) ?: it)
}
}
@@ -254,6 +285,7 @@ fun Modifier.tileCombinedClickable(
onLongClick = onLongClick,
onClickLabel = uiState.accessibilityUiState.clickLabel,
onLongClickLabel = longPressLabel,
+ hapticFeedbackEnabled = !Flags.msdlFeedback(),
)
.semantics {
role = uiState.accessibilityUiState.accessibilityRole
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
index 72b586a2f4df..887a70f39f6a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
import com.android.systemui.qs.panels.domain.interactor.QuickQuickSettingsRowInteractor
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
@@ -45,6 +46,7 @@ constructor(
val squishinessViewModel: TileSquishinessViewModel,
private val iconTilesViewModel: IconTilesViewModel,
@Application private val applicationScope: CoroutineScope,
+ val tileHapticsViewModelFactoryProvider: TileHapticsViewModelFactoryProvider,
) {
val columns = qsColumnsViewModel.columns
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/TileHapticsViewModelFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/TileHapticsViewModelFactoryKosmos.kt
new file mode 100644
index 000000000000..a798eb746943
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/TileHapticsViewModelFactoryKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.msdl
+
+import com.android.systemui.haptics.msdl.qs.TileHapticsViewModel
+import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+
+val Kosmos.tileHapticsViewModelFactory by
+ Kosmos.Fixture {
+ object : TileHapticsViewModel.Factory {
+ override fun create(tileViewModel: TileViewModel): TileHapticsViewModel =
+ TileHapticsViewModel(fakeMSDLPlayer, tileViewModel)
+ }
+ }
+
+val Kosmos.tileHapticsViewModelFactoryProvider by
+ Kosmos.Fixture { TileHapticsViewModelFactoryProvider(tileHapticsViewModelFactory) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
index 093ebd6c6b63..562980d43485 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
@@ -20,13 +20,10 @@ import com.android.internal.logging.InstanceId
import com.android.systemui.animation.Expandable
import com.android.systemui.plugins.qs.QSTile
-class FakeQSTile(
- var user: Int,
- var available: Boolean = true,
-) : QSTile {
+class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
private var tileSpec: String? = null
var destroyed = false
- private val state = QSTile.State()
+ private var state = QSTile.State()
val callbacks = mutableListOf<QSTile.Callback>()
override fun getTileSpec(): String? {
@@ -93,4 +90,9 @@ class FakeQSTile(
override fun isListening(): Boolean {
return false
}
+
+ fun changeState(newState: QSTile.State) {
+ state = newState
+ callbacks.forEach { it.onStateChanged(state) }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index b6b0a4168c5d..b5a6bf126719 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -16,10 +16,17 @@
package com.android.systemui.qs.panels.domain.interactor
+import com.android.systemui.haptics.msdl.tileHapticsViewModelFactoryProvider
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.infiniteGridViewModelFactory
val Kosmos.infiniteGridLayout by
- Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, infiniteGridViewModelFactory) }
+ Kosmos.Fixture {
+ InfiniteGridLayout(
+ iconTilesViewModel,
+ infiniteGridViewModelFactory,
+ tileHapticsViewModelFactoryProvider,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
index 67d9e0ed552b..41ee260fd5dd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.panels.ui.viewmodel
+import com.android.systemui.haptics.msdl.tileHapticsViewModelFactoryProvider
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.qs.panels.domain.interactor.quickQuickSettingsRowInteractor
@@ -30,5 +31,6 @@ val Kosmos.quickQuickSettingsViewModel by
tileSquishinessViewModel,
iconTilesViewModel,
applicationCoroutineScope,
+ tileHapticsViewModelFactoryProvider,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModelKosmos.kt
new file mode 100644
index 000000000000..223755d7636d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModelKosmos.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.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.FakeQSTile
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+val Kosmos.fakeQsTile by Kosmos.Fixture { FakeQSTile(user = 0, available = true) }
+val Kosmos.tileViewModel by
+ Kosmos.Fixture { TileViewModel(fakeQsTile, TileSpec.Companion.create("test")) }