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

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

Merge "Adding MSDL haptic feedback when pulling down the shade." into main

parents 8f58d8e2 6e7adcf3
Loading
Loading
Loading
Loading
+0 −11
Original line number Diff line number Diff line
@@ -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)

+0 −8
Original line number Diff line number Diff line
@@ -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) {
+233 −0
Original line number Diff line number Diff line
/*
 * 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),
                )
        }
}
+7 −14
Original line number Diff line number Diff line
@@ -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
                },
            )
+5 −1
Original line number Diff line number Diff line
@@ -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 {
Loading