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

Commit b202c844 authored by burakov's avatar burakov
Browse files

[bc25] Introduce NotificationsShadeOverlay.

This CL only adds the overlay, but does not wire the corresponding user
actions or deprecate the existing NotificationsShadeScene which it will
replace.

Bug: 359173565
Flag: com.android.systemui.dual_shade
Flag: com.android.systemui.scene_container
Test: Added NotificationsShadeOverlayActionsViewModelTest.
Test: Existing unit tests still pass.
Change-Id: Icdce2f71c35c7f292f7765cd06f4850213b6cbce
parent 60b76eb8
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.NotificationsShadeOverlay
import com.android.systemui.scene.ui.composable.Overlay
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet

@Module
interface NotificationsShadeOverlayModule {

    @Binds @IntoSet fun notificationsShade(overlay: NotificationsShadeOverlay): Overlay
}
+114 −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.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ContentScope
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.phone.ui.TintedIconManager
import dagger.Lazy
import java.util.Optional
import javax.inject.Inject

@SysUISingleton
class NotificationsShadeOverlay
@Inject
constructor(
    private val actionsViewModelFactory: NotificationsShadeOverlayActionsViewModel.Factory,
    private val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory,
    private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
    private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
    private val tintedIconManagerFactory: TintedIconManager.Factory,
    private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
    private val statusBarIconController: StatusBarIconController,
    private val shadeSession: SaveableSession,
    private val stackScrollView: Lazy<NotificationScrollView>,
) : Overlay {

    override val key = Overlays.NotificationsShade

    private val actionsViewModel: NotificationsShadeOverlayActionsViewModel by lazy {
        actionsViewModelFactory.create()
    }

    override suspend fun activate(): Nothing {
        actionsViewModel.activate()
    }

    @Composable
    override fun ContentScope.Content(
        modifier: Modifier,
    ) {
        OverlayShade(
            modifier = modifier,
            viewModelFactory = overlayShadeViewModelFactory,
            lockscreenContent = { Optional.empty() },
        ) {
            Column {
                val placeholderViewModel =
                    rememberViewModel("NotificationsShadeOverlay") {
                        notificationsPlaceholderViewModelFactory.create()
                    }

                ExpandedShadeHeader(
                    viewModelFactory = shadeHeaderViewModelFactory,
                    createTintedIconManager = tintedIconManagerFactory::create,
                    createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
                    statusBarIconController = statusBarIconController,
                    modifier = Modifier.padding(horizontal = 16.dp),
                )

                NotificationScrollingStack(
                    shadeSession = shadeSession,
                    stackScrollView = stackScrollView.get(),
                    viewModel = placeholderViewModel,
                    maxScrimTop = { 0f },
                    shouldPunchHoleBehindScrim = false,
                    shouldFillMaxSize = false,
                    shouldReserveSpaceForNavBar = false,
                    shadeMode = ShadeMode.Dual,
                    modifier = Modifier.fillMaxWidth(),
                )

                // Communicates the bottom position of the drawable area within the shade to NSSL.
                NotificationStackCutoffGuideline(
                    stackScrollView = stackScrollView.get(),
                    viewModel = placeholderViewModel,
                )
            }
        }
    }
}
+84 −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.viewmodel

import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.SysuiTestCase
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.scene.shared.model.Overlays
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }

    private val underTest by lazy { kosmos.notificationsShadeOverlayActionsViewModel }

    @Test
    fun upTransitionSceneKey_topAligned_hidesShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)
            fakeShadeRepository.setDualShadeAlignedToBottom(false)
            underTest.activateIn(this)

            assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay)
                .isEqualTo(Overlays.NotificationsShade)
            assertThat(actions?.get(Swipe.Down)).isNull()
        }

    @Test
    fun upTransitionSceneKey_bottomAligned_doesNothing() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)
            fakeShadeRepository.setDualShadeAlignedToBottom(true)
            underTest.activateIn(this)

            assertThat(actions?.get(Swipe.Up)).isNull()
            assertThat((actions?.get(Swipe.Down) as? UserActionResult.HideOverlay)?.overlay)
                .isEqualTo(Overlays.NotificationsShade)
        }

    @Test
    fun back_hidesShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)
            underTest.activateIn(this)

            assertThat((actions?.get(Back) as? UserActionResult.HideOverlay)?.overlay)
                .isEqualTo(Overlays.NotificationsShade)
        }
}
+59 −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.viewmodel

import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.TransitionKeys
import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeAlignment
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/** Models the UI state for the user actions for navigating to other scenes or overlays. */
class NotificationsShadeOverlayActionsViewModel
@AssistedInject
constructor(
    private val shadeInteractor: ShadeInteractor,
) : SceneActionsViewModel() {

    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
        setActions(
            mapOf(
                if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) {
                    Swipe.Up to UserActionResult.HideOverlay(Overlays.NotificationsShade)
                } else {
                    Swipe.Down to
                        UserActionResult.HideOverlay(
                            overlay = Overlays.NotificationsShade,
                            transitionKey = TransitionKeys.OpenBottomShade,
                        )
                },
                Back to UserActionResult.HideOverlay(Overlays.NotificationsShade),
            )
        )
    }

    @AssistedFactory
    interface Factory {
        fun create(): NotificationsShadeOverlayActionsViewModel
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartabl
import com.android.systemui.scene.domain.startable.SceneContainerStartable
import com.android.systemui.scene.domain.startable.ScrimStartable
import com.android.systemui.scene.domain.startable.StatusBarStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.shared.flag.DualShade
@@ -42,6 +43,7 @@ import dagger.multibindings.IntoMap
        [
            EmptySceneModule::class,
            GoneSceneModule::class,
            NotificationsShadeOverlayModule::class,
            NotificationsShadeSceneModule::class,
            NotificationsShadeSessionModule::class,
            QuickSettingsSceneModule::class,
@@ -99,6 +101,10 @@ interface KeyguardlessSceneContainerFrameworkModule {
                        Scenes.Shade.takeUnless { DualShade.isEnabled },
                    ),
                initialSceneKey = Scenes.Gone,
                overlayKeys =
                    listOfNotNull(
                        Overlays.NotificationsShade.takeIf { DualShade.isEnabled },
                    ),
                navigationDistances =
                    mapOf(
                            Scenes.Gone to 0,
Loading