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