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

Commit a5968b37 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[bc25] Create the `NotificationsShade` scene." into main

parents 78119bfa dd2165df
Loading
Loading
Loading
Loading
+29 −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

import com.android.systemui.notifications.ui.composable.NotificationsShadeScene
import com.android.systemui.scene.shared.model.Scene
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet

@Module
interface NotificationsShadeSceneModule {

    @Binds @IntoSet fun notificationsShade(scene: NotificationsShadeScene): Scene
}
+71 −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.notifications.ui.composable

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.viewmodel.NotificationsShadeSceneViewModel
import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow

@SysUISingleton
class NotificationsShadeScene
@Inject
constructor(
    viewModel: NotificationsShadeSceneViewModel,
    private val overlayShadeViewModel: OverlayShadeViewModel,
) : ComposableScene {

    override val key = Scenes.NotificationsShade

    override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
        viewModel.destinationScenes

    @Composable
    override fun SceneScope.Content(
        modifier: Modifier,
    ) {
        OverlayShade(
            viewModel = overlayShadeViewModel,
            modifier = modifier,
            horizontalArrangement = Arrangement.Start,
        ) {
            Text(
                text = "Notifications list",
                modifier = Modifier.padding(NotificationsShade.Dimensions.Padding)
            )
        }
    }
}

object NotificationsShade {
    object Dimensions {
        val Padding = 16.dp
    }
}
+25 −8
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.shade.domain.startable

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
@@ -37,6 +39,7 @@ import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.shade.ShadeExpansionListener
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
@@ -49,7 +52,6 @@ import kotlinx.coroutines.flow.flowOf
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
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
@@ -67,7 +69,7 @@ class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
    private val fakeConfigurationRepository by lazy { kosmos.fakeConfigurationRepository }
    private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }

    private lateinit var underTest: ShadeStartable
    private val underTest: ShadeStartable = kosmos.shadeStartable

    companion object {
        @JvmStatic
@@ -81,13 +83,9 @@ class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
        mSetFlagsRule.setFlagsParameterization(flags!!)
    }

    @Before
    fun setup() {
        underTest = kosmos.shadeStartable
    }

    @Test
    fun hydrateShadeMode() =
    @DisableFlags(DualShade.FLAG_NAME)
    fun hydrateShadeMode_dualShadeDisabled() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            val shadeMode by collectLastValue(shadeInteractor.shadeMode)
@@ -104,6 +102,25 @@ class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
            assertThat(shadeMode).isEqualTo(ShadeMode.Single)
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun hydrateShadeMode_dualShadeEnabled() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            val shadeMode by collectLastValue(shadeInteractor.shadeMode)

            underTest.start()
            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)

            overrideResource(R.bool.config_use_split_notification_shade, true)
            fakeConfigurationRepository.onAnyConfigurationChange()
            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)

            overrideResource(R.bool.config_use_split_notification_shade, false)
            fakeConfigurationRepository.onAnyConfigurationChange()
            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
        }

    @Test
    @EnableSceneContainer
    fun hydrateShadeExpansionStateManager() =
+123 −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.shade.ui.viewmodel

import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.Swipe
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
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.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
class NotificationsShadeSceneViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val sceneInteractor = kosmos.sceneInteractor
    private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor

    private val underTest = kosmos.notificationsShadeSceneViewModel

    @Test
    fun upTransitionSceneKey_deviceLocked_lockscreen() =
        testScope.runTest {
            val destinationScenes by collectLastValue(underTest.destinationScenes)
            lockDevice()

            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
        }

    @Test
    fun upTransitionSceneKey_deviceUnlocked_gone() =
        testScope.runTest {
            val destinationScenes by collectLastValue(underTest.destinationScenes)
            lockDevice()
            unlockDevice()

            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
        }

    @Test
    fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
        testScope.runTest {
            val destinationScenes by collectLastValue(underTest.destinationScenes)
            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.None
            )
            sceneInteractor.changeScene(Scenes.Lockscreen, "reason")

            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
        }

    @Test
    fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
        testScope.runTest {
            val destinationScenes by collectLastValue(underTest.destinationScenes)
            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.None
            )
            runCurrent()
            sceneInteractor.changeScene(Scenes.Gone, "reason")

            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
        }

    private fun TestScope.lockDevice() {
        val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)

        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
        assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
        sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
        runCurrent()
    }

    private fun TestScope.unlockDevice() {
        val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)

        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
            SuccessFingerprintAuthenticationStatus(0, true)
        )
        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
        sceneInteractor.changeScene(Scenes.Gone, "reason")
        runCurrent()
    }
}
+6 −7
Original line number Diff line number Diff line
@@ -88,12 +88,11 @@ constructor(
        isCommunalAvailable: Boolean,
        shadeMode: ShadeMode,
    ): Map<UserAction, UserActionResult> {
        val shadeSceneKey =
            if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade

        val quickSettingsIfSingleShade =
            if (shadeMode is ShadeMode.Single) {
                Scenes.QuickSettings
            } else {
                Scenes.Shade
            }
            if (shadeMode is ShadeMode.Single) Scenes.QuickSettings else shadeSceneKey

        return mapOf(
                Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable },
@@ -104,8 +103,8 @@ constructor(
                swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade,

                // Swiping down, not from the edge, always navigates to the shade scene.
                swipeDown(pointerCount = 1) to Scenes.Shade,
                swipeDown(pointerCount = 2) to Scenes.Shade,
                swipeDown(pointerCount = 1) to shadeSceneKey,
                swipeDown(pointerCount = 2) to shadeSceneKey,
            )
            .filterValues { it != null }
            .mapValues { checkNotNull(it.value) }
Loading