summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
author Juan Sebastian Martinez <juansmartinez@google.com> 2024-09-30 15:36:00 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-09-30 15:36:00 +0000
commitfc644a3383dca2e1e9fd8b14c4ebb2ca36c20b6f (patch)
treec7abf9a634d77f8a4612d26d0a621952bcb4e5f6 /packages
parent8f58d8e262b77f3006a2307083d131b25348aa81 (diff)
parent6e7adcf3c5ffa38e0206ff56cc0ae65d6131d4ac (diff)
Merge "Adding MSDL haptic feedback when pulling down the shade." into main
Diffstat (limited to 'packages')
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt11
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt233
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt90
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt25
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt55
9 files changed, 393 insertions, 64 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 4162891c0e0b..6f1349f20e4d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -18,7 +18,6 @@
package com.android.systemui.shade.ui.composable
-import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -40,20 +39,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.windowsizeclass.LocalWindowSizeClass
-import com.android.systemui.scene.shared.model.Scenes
/** Renders a lightweight shade UI container, as an overlay. */
@Composable
@@ -62,13 +58,6 @@ fun SceneScope.OverlayShade(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
- val view = LocalView.current
- LaunchedEffect(Unit) {
- if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
- view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
- }
- }
-
Box(modifier) {
Scrim(onClicked = onScrimClicked)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index db0fe3e3f79d..ef415b151200 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -16,7 +16,6 @@
package com.android.systemui.shade.ui.composable
-import android.view.HapticFeedbackConstants
import android.view.ViewGroup
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
@@ -60,7 +59,6 @@ import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
@@ -226,12 +224,6 @@ private fun SceneScope.ShadeScene(
shadeSession: SaveableSession,
usingCollapsedLandscapeMedia: Boolean,
) {
- val view = LocalView.current
- LaunchedEffect(Unit) {
- if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
- view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
- }
- }
val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle()
when (shadeMode) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
new file mode 100644
index 000000000000..664315d19494
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.HapticFeedbackConstants
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
+import com.android.compose.animation.scene.OverlayKey
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.sceneContainerHapticsViewModelFactory
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+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.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class SceneContainerHapticsViewModelTest() : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val msdlPlayer = kosmos.fakeMSDLPlayer
+ private val view = mock<View>()
+
+ private lateinit var underTest: SceneContainerHapticsViewModel
+
+ @Before
+ fun setup() {
+ underTest = kosmos.sceneContainerHapticsViewModelFactory.create(view)
+ underTest.activateIn(testScope)
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @DisableFlags(Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onValidSceneTransition_withMSDL_playsMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected token plays without interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @DisableFlags(Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onInValidSceneTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the no token plays with no interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onValidSceneTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected haptic feedback constant plays
+ verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START))
+ }
+
+ @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onInValidSceneTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the view does not play a haptic feedback constant
+ verifyZeroInteractions(view)
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onValidOverlayTransition_withMSDL_playsMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition =
+ createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected token plays without interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
+ @Test
+ fun onInValidOverlayTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition =
+ createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the no token plays with no interaction properties
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+ }
+
+ @EnableFlags(Flags.FLAG_DUAL_SHADE)
+ @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onValidOverlayTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN a valid scene transition to play haptics
+ val validTransition =
+ createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(validTransition))
+ runCurrent()
+
+ // THEN the expected haptic feedback constant plays
+ verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START))
+ }
+
+ @EnableFlags(Flags.FLAG_DUAL_SHADE)
+ @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ @Test
+ fun onInValidOverlayTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() =
+ testScope.runTest {
+ // GIVEN an invalid scene transition to play haptics
+ val invalidTransition =
+ createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade)
+
+ // WHEN the transition occurs
+ sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition))
+ runCurrent()
+
+ // THEN the view does not play a haptic feedback constant
+ verifyZeroInteractions(view)
+ }
+
+ private fun createTransitionState(from: SceneKey, to: ContentKey) =
+ when (to) {
+ is SceneKey ->
+ ObservableTransitionState.Transition(
+ fromScene = from,
+ toScene = to,
+ currentScene = flowOf(from),
+ progress = MutableStateFlow(0.2f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ )
+ is OverlayKey ->
+ ShowOrHideOverlay(
+ overlay = to,
+ fromContent = from,
+ toContent = to,
+ currentScene = from,
+ currentOverlays = sceneInteractor.currentOverlays,
+ progress = MutableStateFlow(0.2f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index e60e742e9917..a37f511cef69 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -21,23 +21,21 @@ package com.android.systemui.scene.ui.viewmodel
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.MotionEvent
+import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.classifier.fakeFalsingManager
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.data.repository.fakePowerRepository
-import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.fakeOverlaysByKeys
import com.android.systemui.scene.sceneContainerConfig
-import com.android.systemui.scene.sceneContainerGestureFilterFactory
-import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.sceneContainerViewModelFactory
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -72,6 +70,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
private val falsingManager by lazy { kosmos.fakeFalsingManager }
+ private val view = mock<View>()
private lateinit var underTest: SceneContainerViewModel
@@ -81,16 +80,10 @@ class SceneContainerViewModelTest : SysuiTestCase() {
@Before
fun setUp() {
underTest =
- SceneContainerViewModel(
- sceneInteractor = sceneInteractor,
- falsingInteractor = kosmos.falsingInteractor,
- powerInteractor = kosmos.powerInteractor,
- shadeInteractor = kosmos.shadeInteractor,
- splitEdgeDetector = kosmos.splitEdgeDetector,
- logger = kosmos.sceneLogger,
- gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
- displayId = kosmos.displayTracker.defaultDisplayId,
- motionEventHandlerReceiver = { motionEventHandler ->
+ kosmos.sceneContainerViewModelFactory.create(
+ view,
+ kosmos.displayTracker.defaultDisplayId,
+ { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
},
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index a7e7d8bb34dc..a8be5804d04a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -108,7 +108,11 @@ object SceneWindowRootViewBinder {
traceName = "SceneWindowRootViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
factory = {
- viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+ viewModelFactory.create(
+ view,
+ view.context.displayId,
+ motionEventHandlerReceiver,
+ )
},
) { viewModel ->
try {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt
new file mode 100644
index 000000000000..4ef8e0fc3167
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+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 kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * Models haptics UI state for the scene container.
+ *
+ * This model gets a [View] to play haptics using the [View.performHapticFeedback] API. This should
+ * be the only purpose of this reference.
+ */
+class SceneContainerHapticsViewModel
+@AssistedInject
+constructor(
+ @Assisted private val view: View,
+ sceneInteractor: SceneInteractor,
+ shadeInteractor: ShadeInteractor,
+ private val msdlPlayer: MSDLPlayer,
+) : ExclusiveActivatable() {
+
+ /** Should haptics be played by pulling down the shade */
+ private val isShadePullHapticsRequired: Flow<Boolean> =
+ combine(shadeInteractor.isUserInteracting, sceneInteractor.transitionState) {
+ interacting,
+ transitionState ->
+ interacting && transitionState.isValidForShadePullHaptics()
+ }
+ .distinctUntilChanged()
+
+ override suspend fun onActivated(): Nothing {
+ isShadePullHapticsRequired.collect { playShadePullHaptics ->
+ if (!playShadePullHaptics) return@collect
+
+ if (Flags.msdlFeedback()) {
+ msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+ } else {
+ view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
+ }
+ }
+ awaitCancellation()
+ }
+
+ private fun ObservableTransitionState.isValidForShadePullHaptics(): Boolean {
+ val validOrigin =
+ isTransitioning(from = Scenes.Gone) || isTransitioning(from = Scenes.Lockscreen)
+ val validDestination =
+ isTransitioning(to = Scenes.Shade) ||
+ isTransitioning(to = Scenes.QuickSettings) ||
+ isTransitioning(to = Overlays.QuickSettingsShade) ||
+ isTransitioning(to = Overlays.NotificationsShade)
+ return validOrigin && validDestination
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(view: View): SceneContainerHapticsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 0bf2d499721b..f5053853846c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -17,8 +17,10 @@
package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
+import android.view.View
import androidx.compose.runtime.getValue
import androidx.compose.ui.geometry.Offset
+import com.android.app.tracing.coroutines.launch
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
@@ -60,6 +62,8 @@ constructor(
private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
gestureFilterFactory: SceneContainerGestureFilter.Factory,
+ hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
+ @Assisted view: View,
@Assisted displayId: Int,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -72,6 +76,8 @@ constructor(
/** Whether the container is visible. */
val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
+ private val hapticsViewModel = hapticsViewModelFactory.create(view)
+
/**
* The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
* [UserAction]s for this container.
@@ -107,6 +113,7 @@ constructor(
coroutineScope {
launch { hydrator.activate() }
launch { gestureFilter.activate() }
+ launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() }
}
awaitCancellation()
} finally {
@@ -281,6 +288,7 @@ constructor(
@AssistedFactory
interface Factory {
fun create(
+ view: View,
displayId: Int,
motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
): SceneContainerViewModel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 5cc64547aa6b..0d369a3ea80c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.composable
import android.app.AlertDialog
import android.platform.test.annotations.MotionTest
import android.testing.TestableLooper.RunWithLooper
+import android.view.View
import androidx.activity.BackEventCompat
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
@@ -52,27 +53,21 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
-import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.motion.createSysUiComposeMotionTestRule
-import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.sceneContainerGestureFilterFactory
-import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.sceneContainerViewModelFactory
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
import com.android.systemui.scene.ui.composable.Scene
import com.android.systemui.scene.ui.composable.SceneContainer
-import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
import com.android.systemui.settings.displayTracker
-import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.awaitCancellation
@@ -85,6 +80,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
import platform.test.motion.compose.ComposeRecordingSpec
import platform.test.motion.compose.MotionControl
@@ -121,24 +117,17 @@ class BouncerPredictiveBackTest : SysuiTestCase() {
val navigationDistances = mapOf(Scenes.Lockscreen to 1, Scenes.Bouncer to 0)
SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
}
+ private val view = mock<View>()
private val transitionState by lazy {
MutableStateFlow<ObservableTransitionState>(
ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
)
}
+
private val sceneContainerViewModel by lazy {
- SceneContainerViewModel(
- sceneInteractor = kosmos.sceneInteractor,
- falsingInteractor = kosmos.falsingInteractor,
- powerInteractor = kosmos.powerInteractor,
- shadeInteractor = kosmos.shadeInteractor,
- splitEdgeDetector = kosmos.splitEdgeDetector,
- logger = kosmos.sceneLogger,
- gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
- displayId = kosmos.displayTracker.defaultDisplayId,
- motionEventHandlerReceiver = {},
- )
+ kosmos.sceneContainerViewModelFactory
+ .create(view, kosmos.displayTracker.defaultDisplayId, {})
.apply { setTransitionState(transitionState) }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 737aaf22b557..f842db4c0026 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -1,7 +1,9 @@
package com.android.systemui.scene
+import android.view.View
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -13,11 +15,13 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
+import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
+import org.mockito.kotlin.mock
var Kosmos.sceneKeys by Fixture {
listOf(
@@ -68,18 +72,32 @@ val Kosmos.transitionState by Fixture {
}
val Kosmos.sceneContainerViewModel by Fixture {
- SceneContainerViewModel(
- sceneInteractor = sceneInteractor,
- falsingInteractor = falsingInteractor,
- powerInteractor = powerInteractor,
- shadeInteractor = shadeInteractor,
- splitEdgeDetector = splitEdgeDetector,
- gestureFilterFactory = sceneContainerGestureFilterFactory,
- displayId = displayTracker.defaultDisplayId,
- motionEventHandlerReceiver = {},
- logger = sceneLogger,
- )
- .apply { setTransitionState(transitionState) }
+ sceneContainerViewModelFactory.create(mock<View>(), displayTracker.defaultDisplayId, {}).apply {
+ setTransitionState(transitionState)
+ }
+}
+
+val Kosmos.sceneContainerViewModelFactory by Fixture {
+ object : SceneContainerViewModel.Factory {
+ override fun create(
+ view: View,
+ displayId: Int,
+ motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit,
+ ): SceneContainerViewModel =
+ SceneContainerViewModel(
+ sceneInteractor = sceneInteractor,
+ falsingInteractor = falsingInteractor,
+ powerInteractor = powerInteractor,
+ shadeInteractor = shadeInteractor,
+ splitEdgeDetector = splitEdgeDetector,
+ logger = sceneLogger,
+ gestureFilterFactory = sceneContainerGestureFilterFactory,
+ hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
+ view = view,
+ displayId = displayId,
+ motionEventHandlerReceiver = motionEventHandlerReceiver,
+ )
+ }
}
val Kosmos.sceneContainerGestureFilterFactory by Fixture {
@@ -92,3 +110,16 @@ val Kosmos.sceneContainerGestureFilterFactory by Fixture {
}
}
}
+
+val Kosmos.sceneContainerHapticsViewModelFactory by Fixture {
+ object : SceneContainerHapticsViewModel.Factory {
+ override fun create(view: View): SceneContainerHapticsViewModel {
+ return SceneContainerHapticsViewModel(
+ view = view,
+ sceneInteractor = sceneInteractor,
+ shadeInteractor = shadeInteractor,
+ msdlPlayer = msdlPlayer,
+ )
+ }
+ }
+}