1 /* 2 * 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 package com.android.systemui.statusbar.notification.stack.ui.viewmodel 18 19 import androidx.compose.runtime.getValue 20 import com.android.app.tracing.coroutines.launchTraced as launch 21 import com.android.compose.animation.scene.ContentKey 22 import com.android.compose.animation.scene.ObservableTransitionState 23 import com.android.systemui.dump.DumpManager 24 import com.android.systemui.flags.FeatureFlagsClassic 25 import com.android.systemui.flags.Flags 26 import com.android.systemui.lifecycle.ExclusiveActivatable 27 import com.android.systemui.lifecycle.Hydrator 28 import com.android.systemui.scene.domain.interactor.SceneInteractor 29 import com.android.systemui.scene.shared.flag.SceneContainerFlag 30 import com.android.systemui.scene.shared.model.Overlays 31 import com.android.systemui.scene.shared.model.Scenes 32 import com.android.systemui.shade.domain.interactor.ShadeInteractor 33 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor 34 import com.android.systemui.shade.shared.model.ShadeMode 35 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor 36 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor 37 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor 38 import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent 39 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds 40 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding 41 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState 42 import com.android.systemui.util.kotlin.ActivatableFlowDumper 43 import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl 44 import dagger.assisted.AssistedFactory 45 import dagger.assisted.AssistedInject 46 import java.util.function.Consumer 47 import kotlinx.coroutines.coroutineScope 48 import kotlinx.coroutines.flow.Flow 49 import kotlinx.coroutines.flow.filter 50 import kotlinx.coroutines.flow.map 51 52 /** 53 * ViewModel used by the Notification placeholders inside the scene container to update the 54 * [NotificationStackAppearanceInteractor], and by extension control the NSSL. 55 */ 56 class NotificationsPlaceholderViewModel 57 @AssistedInject 58 constructor( 59 private val interactor: NotificationStackAppearanceInteractor, 60 private val sceneInteractor: SceneInteractor, 61 private val shadeInteractor: ShadeInteractor, 62 shadeModeInteractor: ShadeModeInteractor, 63 private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, 64 remoteInputInteractor: RemoteInputInteractor, 65 featureFlags: FeatureFlagsClassic, 66 dumpManager: DumpManager, 67 ) : 68 ExclusiveActivatable(), 69 ActivatableFlowDumper by ActivatableFlowDumperImpl( 70 dumpManager = dumpManager, 71 tag = "NotificationsPlaceholderViewModel", <lambda>null72 ) { 73 74 private val hydrator = Hydrator("NotificationsPlaceholderViewModel") 75 76 /** The content key to use for the notification shade. */ 77 val notificationsShadeContentKey: ContentKey by 78 hydrator.hydratedStateOf( 79 traceName = "notificationsShadeContentKey", 80 initialValue = getNotificationsShadeContentKey(shadeModeInteractor.shadeMode.value), 81 source = shadeModeInteractor.shadeMode.map { getNotificationsShadeContentKey(it) }, 82 ) 83 84 /** The content key to use for the quick settings shade. */ 85 val quickSettingsShadeContentKey: ContentKey by 86 hydrator.hydratedStateOf( 87 traceName = "quickSettingsShadeContentKey", 88 initialValue = getQuickSettingsShadeContentKey(shadeModeInteractor.shadeMode.value), 89 source = shadeModeInteractor.shadeMode.map { getQuickSettingsShadeContentKey(it) }, 90 ) 91 92 /** 93 * Whether the current touch gesture is overscroll. If true, it means the NSSL has already 94 * consumed part of the gesture. 95 */ 96 val isCurrentGestureOverscroll: Boolean by 97 hydrator.hydratedStateOf( 98 traceName = "isCurrentGestureOverscroll", 99 initialValue = false, 100 source = interactor.isCurrentGestureOverscroll 101 ) 102 103 /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */ 104 val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES) 105 106 /** DEBUG: whether the debug logging should be output. */ 107 val isDebugLoggingEnabled: Boolean = SceneContainerFlag.isEnabled 108 109 override suspend fun onActivated(): Nothing { 110 coroutineScope { 111 launch { hydrator.activate() } 112 113 launch { 114 shadeInteractor.isAnyExpanded 115 .filter { it } 116 .collect { headsUpNotificationInteractor.unpinAll(true) } 117 } 118 119 launch { 120 sceneInteractor.transitionState 121 .filter { it is ObservableTransitionState.Idle } 122 .collect { headsUpNotificationInteractor.onTransitionIdle() } 123 } 124 } 125 activateFlowDumper() 126 } 127 128 /** Notifies that the bounds of the notification scrim have changed. */ 129 fun onScrimBoundsChanged(bounds: ShadeScrimBounds?) { 130 interactor.setNotificationShadeScrimBounds(bounds) 131 } 132 133 /** Sets the available space */ 134 fun onConstrainedAvailableSpaceChanged(height: Int) { 135 interactor.setConstrainedAvailableSpace(height) 136 } 137 138 /** Sets the content alpha for the current state of the brightness mirror */ 139 fun setAlphaForBrightnessMirror(alpha: Float) { 140 interactor.setAlphaForBrightnessMirror(alpha) 141 } 142 143 /** True when a HUN is pinned or animating away. */ 144 val isHeadsUpOrAnimatingAway: Flow<Boolean> = 145 headsUpNotificationInteractor.isHeadsUpOrAnimatingAway 146 147 /** Corner rounding of the stack */ 148 val shadeScrimRounding: Flow<ShadeScrimRounding> = 149 interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding") 150 151 /** 152 * The amount [0-1] that the shade or quick settings has been opened. At 0, the shade is closed; 153 * at 1, either the shade or quick settings is open. 154 */ 155 val expandFraction: Flow<Float> = shadeInteractor.anyExpansion.dumpValue("expandFraction") 156 157 /** 158 * The amount [0-1] that quick settings has been opened. At 0, the shade may be open or closed; 159 * at 1, the quick settings are open. 160 */ 161 val shadeToQsFraction: Flow<Float> = shadeInteractor.qsExpansion.dumpValue("shadeToQsFraction") 162 163 /** 164 * The amount in px that the notification stack should scroll due to internal expansion. This 165 * should only happen when a notification expansion hits the bottom of the screen, so it is 166 * necessary to scroll up to keep expanding the notification. 167 */ 168 val syntheticScroll: Flow<Float> = 169 interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll") 170 171 /** Whether remote input is currently active for any notification. */ 172 val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive 173 174 /** The bottom bound of the currently focused remote input notification row. */ 175 val remoteInputRowBottomBound = remoteInputInteractor.remoteInputRowBottomBound 176 177 /** Updates the current scroll state of the notification shade. */ 178 fun setScrollState(scrollState: ShadeScrollState) { 179 interactor.setScrollState(scrollState) 180 } 181 182 /** Sets whether the heads up notification is animating away. */ 183 fun setHeadsUpAnimatingAway(animatingAway: Boolean) { 184 headsUpNotificationInteractor.setHeadsUpAnimatingAway(animatingAway) 185 } 186 187 /** Snooze the currently pinned HUN. */ 188 fun snoozeHun() { 189 headsUpNotificationInteractor.snooze() 190 } 191 192 /** Set a consumer for accessibility events to be handled by the placeholder. */ 193 fun setAccessibilityScrollEventConsumer(consumer: Consumer<AccessibilityScrollEvent>?) { 194 interactor.setAccessibilityScrollEventConsumer(consumer) 195 } 196 197 private fun getNotificationsShadeContentKey(shadeMode: ShadeMode): ContentKey { 198 return if (shadeMode is ShadeMode.Dual) Overlays.NotificationsShade else Scenes.Shade 199 } 200 201 private fun getQuickSettingsShadeContentKey(shadeMode: ShadeMode): ContentKey { 202 return when (shadeMode) { 203 is ShadeMode.Single -> Scenes.QuickSettings 204 is ShadeMode.Split -> Scenes.Shade 205 is ShadeMode.Dual -> Overlays.QuickSettingsShade 206 } 207 } 208 209 @AssistedFactory 210 interface Factory { 211 fun create(): NotificationsPlaceholderViewModel 212 } 213 } 214 215 // Expansion fraction thresholds (between 0-1f) at which the corresponding value should be 216 // at its maximum, given they are at their minimum value at expansion = 0f. 217 object NotificationTransitionThresholds { 218 const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f 219 const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f 220 const val EXPANSION_FOR_DELAYED_STACK_FADE_IN = 0.5f 221 } 222