1 /* <lambda>null2 * Copyright (C) 2024 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.communal 18 19 import android.provider.Settings 20 import com.android.compose.animation.scene.SceneKey 21 import com.android.systemui.CoreStartable 22 import com.android.systemui.communal.domain.interactor.CommunalInteractor 23 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 24 import com.android.systemui.communal.shared.model.CommunalScenes 25 import com.android.systemui.communal.shared.model.CommunalTransitionKeys 26 import com.android.systemui.dagger.SysUISingleton 27 import com.android.systemui.dagger.qualifiers.Application 28 import com.android.systemui.dagger.qualifiers.Background 29 import com.android.systemui.dagger.qualifiers.Main 30 import com.android.systemui.dock.DockManager 31 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 32 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 33 import com.android.systemui.keyguard.shared.model.KeyguardState 34 import com.android.systemui.keyguard.shared.model.TransitionStep 35 import com.android.systemui.statusbar.NotificationShadeWindowController 36 import com.android.systemui.statusbar.phone.CentralSurfaces 37 import com.android.systemui.util.kotlin.emitOnStart 38 import com.android.systemui.util.kotlin.getValue 39 import com.android.systemui.util.kotlin.sample 40 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow 41 import com.android.systemui.util.settings.SystemSettings 42 import java.util.Optional 43 import javax.inject.Inject 44 import kotlin.time.Duration.Companion.milliseconds 45 import kotlin.time.Duration.Companion.seconds 46 import kotlinx.coroutines.CoroutineDispatcher 47 import kotlinx.coroutines.CoroutineScope 48 import kotlinx.coroutines.ExperimentalCoroutinesApi 49 import kotlinx.coroutines.Job 50 import kotlinx.coroutines.delay 51 import kotlinx.coroutines.flow.collectLatest 52 import kotlinx.coroutines.flow.combine 53 import kotlinx.coroutines.flow.filterNotNull 54 import kotlinx.coroutines.flow.launchIn 55 import kotlinx.coroutines.flow.mapLatest 56 import kotlinx.coroutines.flow.onEach 57 import kotlinx.coroutines.launch 58 import kotlinx.coroutines.withContext 59 60 /** 61 * A [CoreStartable] responsible for automatically navigating between communal scenes when certain 62 * conditions are met. 63 */ 64 @OptIn(ExperimentalCoroutinesApi::class) 65 @SysUISingleton 66 class CommunalSceneStartable 67 @Inject 68 constructor( 69 private val dockManager: DockManager, 70 private val communalInteractor: CommunalInteractor, 71 private val communalSceneInteractor: CommunalSceneInteractor, 72 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 73 private val keyguardInteractor: KeyguardInteractor, 74 private val systemSettings: SystemSettings, 75 centralSurfacesOpt: Optional<CentralSurfaces>, 76 private val notificationShadeWindowController: NotificationShadeWindowController, 77 @Application private val applicationScope: CoroutineScope, 78 @Background private val bgScope: CoroutineScope, 79 @Main private val mainDispatcher: CoroutineDispatcher, 80 ) : CoreStartable { 81 private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT 82 83 private var timeoutJob: Job? = null 84 85 private var isDreaming: Boolean = false 86 87 private val centralSurfaces: CentralSurfaces? by centralSurfacesOpt 88 89 override fun start() { 90 // Handle automatically switching based on keyguard state. 91 keyguardTransitionInteractor.startedKeyguardTransitionStep 92 .mapLatest(::determineSceneAfterTransition) 93 .filterNotNull() 94 .onEach { nextScene -> 95 communalSceneInteractor.changeScene(nextScene, CommunalTransitionKeys.SimpleFade) 96 } 97 .launchIn(applicationScope) 98 99 // TODO(b/322787129): re-enable once custom animations are in place 100 // Handle automatically switching to communal when docked. 101 // dockManager 102 // .retrieveIsDocked() 103 // // Allow some time after docking to ensure the dream doesn't start. If the 104 // dream 105 // // starts, then we don't want to automatically transition to glanceable hub. 106 // .debounce(DOCK_DEBOUNCE_DELAY) 107 // .sample(keyguardTransitionInteractor.startedKeyguardState, ::Pair) 108 // .onEach { (docked, lastStartedState) -> 109 // if (docked && lastStartedState == KeyguardState.LOCKSCREEN) { 110 // communalInteractor.onSceneChanged(CommunalScenes.Communal) 111 // } 112 // } 113 // .launchIn(bgScope) 114 115 systemSettings 116 .observerFlow(Settings.System.SCREEN_OFF_TIMEOUT) 117 // Read the setting value on start. 118 .emitOnStart() 119 .onEach { 120 screenTimeout = 121 systemSettings.getInt( 122 Settings.System.SCREEN_OFF_TIMEOUT, 123 DEFAULT_SCREEN_TIMEOUT 124 ) 125 } 126 .launchIn(bgScope) 127 128 // The hub mode timeout should start as soon as the user enters hub mode. At the end of the 129 // timer, if the device is dreaming, hub mode should closed and reveal the dream. If the 130 // dream is not running, nothing will happen. However if the dream starts again underneath 131 // hub mode after the initial timeout expires, such as if the device is docked or the dream 132 // app is updated by the Play store, a new timeout should be started. 133 bgScope.launch { 134 combine( 135 communalSceneInteractor.currentScene, 136 // Emit a value on start so the combine starts. 137 communalInteractor.userActivity.emitOnStart() 138 ) { scene, _ -> 139 // Only timeout if we're on the hub is open. 140 scene == CommunalScenes.Communal 141 } 142 .collectLatest { shouldTimeout -> 143 cancelHubTimeout() 144 if (shouldTimeout) { 145 startHubTimeout() 146 } 147 } 148 } 149 bgScope.launch { 150 keyguardInteractor.isDreaming 151 .sample(communalSceneInteractor.currentScene, ::Pair) 152 .collectLatest { (isDreaming, scene) -> 153 this@CommunalSceneStartable.isDreaming = isDreaming 154 if (scene == CommunalScenes.Communal && isDreaming && timeoutJob == null) { 155 // If dreaming starts after timeout has expired, ex. if dream restarts under 156 // the hub, just close the hub immediately. 157 communalSceneInteractor.changeScene(CommunalScenes.Blank) 158 } 159 } 160 } 161 162 bgScope.launch { 163 communalSceneInteractor.isIdleOnCommunal.collectLatest { 164 withContext(mainDispatcher) { 165 notificationShadeWindowController.setGlanceableHubShowing(it) 166 } 167 } 168 } 169 } 170 171 private fun cancelHubTimeout() { 172 timeoutJob?.cancel() 173 timeoutJob = null 174 } 175 176 private fun startHubTimeout() { 177 if (timeoutJob == null) { 178 timeoutJob = 179 bgScope.launch { 180 delay(screenTimeout.milliseconds) 181 if (isDreaming) { 182 communalSceneInteractor.changeScene(CommunalScenes.Blank) 183 } 184 timeoutJob = null 185 } 186 } 187 } 188 189 private suspend fun determineSceneAfterTransition( 190 lastStartedTransition: TransitionStep, 191 ): SceneKey? { 192 val to = lastStartedTransition.to 193 val from = lastStartedTransition.from 194 val docked = dockManager.isDocked 195 val launchingActivityOverLockscreen = 196 centralSurfaces?.isLaunchingActivityOverLockscreen ?: false 197 198 return when { 199 to == KeyguardState.OCCLUDED && !launchingActivityOverLockscreen -> { 200 // Hide communal when an activity is started on keyguard, to ensure the activity 201 // underneath the hub is shown. When launching activities over lockscreen, we only 202 // change scenes once the activity launch animation is finished, so avoid 203 // changing the scene here. 204 CommunalScenes.Blank 205 } 206 to == KeyguardState.GLANCEABLE_HUB && from == KeyguardState.OCCLUDED -> { 207 // When transitioning to the hub from an occluded state, fade out the hub without 208 // doing any translation. 209 CommunalScenes.Communal 210 } 211 // Transitioning to Blank scene when entering the edit mode will be handled separately 212 // with custom animations. 213 to == KeyguardState.GONE && !communalInteractor.editModeOpen.value -> 214 CommunalScenes.Blank 215 !docked && !KeyguardState.deviceIsAwakeInState(to) -> { 216 // If the user taps the screen and wakes the device within this timeout, we don't 217 // want to dismiss the hub 218 delay(AWAKE_DEBOUNCE_DELAY) 219 CommunalScenes.Blank 220 } 221 else -> null 222 } 223 } 224 225 companion object { 226 val AWAKE_DEBOUNCE_DELAY = 5.seconds 227 val DOCK_DEBOUNCE_DELAY = 1.seconds 228 val DEFAULT_SCREEN_TIMEOUT = 15000 229 } 230 } 231