1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.statusbar.notification.stack.ui.viewmodel 19 20 import com.android.compose.animation.scene.ObservableTransitionState 21 import com.android.compose.animation.scene.SceneKey 22 import com.android.systemui.dagger.SysUISingleton 23 import com.android.systemui.dump.DumpManager 24 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 25 import com.android.systemui.scene.domain.interactor.SceneInteractor 26 import com.android.systemui.scene.shared.flag.SceneContainerFlag 27 import com.android.systemui.scene.shared.model.SceneFamilies 28 import com.android.systemui.scene.shared.model.Scenes 29 import com.android.systemui.shade.domain.interactor.ShadeInteractor 30 import com.android.systemui.shade.shared.model.ShadeMode 31 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor 32 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping 33 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape 34 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN 35 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA 36 import com.android.systemui.util.kotlin.FlowDumperImpl 37 import dagger.Lazy 38 import javax.inject.Inject 39 import kotlinx.coroutines.flow.Flow 40 import kotlinx.coroutines.flow.combine 41 import kotlinx.coroutines.flow.distinctUntilChanged 42 import kotlinx.coroutines.flow.flowOf 43 import kotlinx.coroutines.flow.map 44 45 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ 46 @SysUISingleton 47 class NotificationScrollViewModel 48 @Inject 49 constructor( 50 dumpManager: DumpManager, 51 stackAppearanceInteractor: NotificationStackAppearanceInteractor, 52 shadeInteractor: ShadeInteractor, 53 private val sceneInteractor: SceneInteractor, 54 // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released - 55 // while the flag is off, creating this object too early results in a crash 56 keyguardInteractor: Lazy<KeyguardInteractor>, 57 ) : FlowDumperImpl(dumpManager) { 58 /** 59 * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning 60 * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while 61 * transitioning from Shade to QuickSettings scenes. 62 */ 63 val expandFraction: Flow<Float> = 64 combine( 65 shadeInteractor.shadeExpansion, 66 shadeInteractor.shadeMode, 67 shadeInteractor.qsExpansion, 68 sceneInteractor.transitionState, 69 sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings), 70 ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene -> 71 when (transitionState) { 72 is ObservableTransitionState.Idle -> { 73 if (transitionState.currentScene == Scenes.Lockscreen) { 74 1f 75 } else { 76 shadeExpansion 77 } 78 } 79 is ObservableTransitionState.Transition -> { 80 if ( 81 (transitionState.fromScene in SceneFamilies.NotifShade && 82 transitionState.toScene == quickSettingsScene) || 83 (transitionState.fromScene in quickSettingsScene && 84 transitionState.toScene in SceneFamilies.NotifShade) || 85 (transitionState.fromScene == Scenes.Lockscreen && 86 transitionState.toScene in SceneFamilies.NotifShade) || 87 (transitionState.fromScene in SceneFamilies.NotifShade && 88 transitionState.toScene == Scenes.Lockscreen) 89 ) { 90 1f 91 } else if ( 92 shadeMode != ShadeMode.Split && 93 transitionState.fromScene in SceneFamilies.Home && 94 transitionState.toScene in quickSettingsScene 95 ) { 96 // during QS expansion, increase fraction at same rate as scrim alpha, 97 // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN. 98 (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - 99 EXPANSION_FOR_DELAYED_STACK_FADE_IN) 100 .coerceIn(0f, 1f) 101 } else { 102 shadeExpansion 103 } 104 } 105 } 106 } 107 .distinctUntilChanged() 108 .dumpWhileCollecting("expandFraction") 109 110 private operator fun SceneKey.contains(scene: SceneKey) = 111 sceneInteractor.isSceneInFamily(scene, this) 112 113 /** The bounds of the notification stack in the current scene. */ 114 private val shadeScrimClipping: Flow<ShadeScrimClipping?> = 115 combine( 116 stackAppearanceInteractor.shadeScrimBounds, 117 stackAppearanceInteractor.shadeScrimRounding, 118 ) { bounds, rounding -> 119 bounds?.let { ShadeScrimClipping(it, rounding) } 120 } 121 .dumpWhileCollecting("stackClipping") 122 123 fun shadeScrimShape( 124 cornerRadius: Flow<Int>, 125 viewLeftOffset: Flow<Int> 126 ): Flow<ShadeScrimShape?> = 127 combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset -> 128 if (clipping == null) return@combine null 129 ShadeScrimShape( 130 bounds = clipping.bounds.minus(leftOffset = leftOffset), 131 topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0, 132 bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0 133 ) 134 } 135 .dumpWhileCollecting("shadeScrimShape") 136 137 /** 138 * Max alpha to apply directly to the view based on the compose placeholder. 139 * 140 * TODO(b/338590620): Migrate alphas from [SharedNotificationContainerViewModel] into this flow 141 */ 142 val maxAlpha: Flow<Float> = 143 stackAppearanceInteractor.alphaForBrightnessMirror.dumpValue("maxAlpha") 144 145 /** 146 * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any 147 * further. 148 */ 149 val scrolledToTop: Flow<Boolean> = 150 stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop") 151 152 /** Receives the amount (px) that the stack should scroll due to internal expansion. */ 153 val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll 154 155 /** 156 * Receives whether the current touch gesture is overscroll as it has already been consumed by 157 * the stack. 158 */ 159 val currentGestureOverscrollConsumer: (Boolean) -> Unit = 160 stackAppearanceInteractor::setCurrentGestureOverscroll 161 162 /** Whether the notification stack is scrollable or not. */ 163 val isScrollable: Flow<Boolean> = sceneInteractor.currentScene.map { 164 sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) || it == Scenes.Lockscreen 165 }.dumpWhileCollecting("isScrollable") 166 167 /** Whether the notification stack is displayed in doze mode. */ 168 val isDozing: Flow<Boolean> by lazy { 169 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 170 flowOf(false) 171 } else { 172 keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing") 173 } 174 } 175 } 176