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.dreams.homecontrols.domain.interactor
18
19 import android.annotation.SuppressLint
20 import android.app.DreamManager
21 import android.content.ComponentName
22 import android.os.PowerManager
23 import android.os.UserHandle
24 import com.android.systemui.common.domain.interactor.PackageChangeInteractor
25 import com.android.systemui.common.shared.model.PackageChangeModel
26 import com.android.systemui.controls.ControlsServiceInfo
27 import com.android.systemui.controls.dagger.ControlsComponent
28 import com.android.systemui.controls.management.ControlsListingController
29 import com.android.systemui.controls.panels.AuthorizedPanelsRepository
30 import com.android.systemui.controls.panels.SelectedComponentRepository
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dagger.qualifiers.Background
33 import com.android.systemui.user.data.repository.UserRepository
34 import com.android.systemui.util.kotlin.getOrNull
35 import com.android.systemui.util.kotlin.pairwiseBy
36 import com.android.systemui.util.kotlin.sample
37 import com.android.systemui.util.time.SystemClock
38 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
39 import javax.inject.Inject
40 import kotlin.math.abs
41 import kotlin.time.Duration.Companion.milliseconds
42 import kotlinx.coroutines.CoroutineScope
43 import kotlinx.coroutines.ExperimentalCoroutinesApi
44 import kotlinx.coroutines.channels.BufferOverflow
45 import kotlinx.coroutines.channels.awaitClose
46 import kotlinx.coroutines.flow.Flow
47 import kotlinx.coroutines.flow.MutableSharedFlow
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.emptyFlow
52 import kotlinx.coroutines.flow.filter
53 import kotlinx.coroutines.flow.filterNotNull
54 import kotlinx.coroutines.flow.flatMapLatest
55 import kotlinx.coroutines.flow.map
56 import kotlinx.coroutines.flow.onStart
57 import kotlinx.coroutines.flow.stateIn
58
59 @SysUISingleton
60 @OptIn(ExperimentalCoroutinesApi::class)
61 class HomeControlsComponentInteractor
62 @Inject
63 constructor(
64 private val selectedComponentRepository: SelectedComponentRepository,
65 controlsComponent: ControlsComponent,
66 authorizedPanelsRepository: AuthorizedPanelsRepository,
67 userRepository: UserRepository,
68 private val packageChangeInteractor: PackageChangeInteractor,
69 private val systemClock: SystemClock,
70 private val powerManager: PowerManager,
71 private val dreamManager: DreamManager,
72 @Background private val bgScope: CoroutineScope
73 ) {
74 private val controlsListingController: ControlsListingController? =
75 controlsComponent.getControlsListingController().getOrNull()
76
77 /** Gets the current user's selected panel, or null if there isn't one */
78 private val selectedPanel: Flow<SelectedComponentRepository.SelectedComponent?> =
79 userRepository.selectedUserInfo
80 .flatMapLatest { user ->
81 selectedComponentRepository.selectedComponentFlow(user.userHandle)
82 }
83 .map { if (it?.isPanel == true) it else null }
84
85 /** Gets the current user's authorized panels */
86 private val allAuthorizedPanels: Flow<Set<String>> =
87 userRepository.selectedUserInfo.flatMapLatest { user ->
88 authorizedPanelsRepository.observeAuthorizedPanels(user.userHandle)
89 }
90
91 /** Gets all the available services from [ControlsListingController] */
92 private fun allAvailableServices(): Flow<List<ControlsServiceInfo>> {
93 if (controlsListingController == null) {
94 return emptyFlow()
95 }
96 return conflatedCallbackFlow {
97 val listener =
98 object : ControlsListingController.ControlsListingCallback {
99 override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
100 trySend(serviceInfos)
101 }
102 }
103 controlsListingController.addCallback(listener)
104 awaitClose { controlsListingController.removeCallback(listener) }
105 }
106 .onStart { emit(controlsListingController.getCurrentServices()) }
107 }
108
109 /** Gets all panels which are available and authorized by the user */
110 private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> =
111 combine(
112 allAvailableServices(),
113 allAuthorizedPanels,
114 ) { serviceInfos, authorizedPanels ->
115 serviceInfos.mapNotNull {
116 val panelActivity = it.panelActivity
117 if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
118 PanelComponent(it.componentName, panelActivity)
119 } else {
120 null
121 }
122 }
123 }
124
125 val panelComponent: StateFlow<ComponentName?> =
126 combine(
127 allAvailableAndAuthorizedPanels,
128 selectedPanel,
129 ) { panels, selected ->
130 val item =
131 panels.firstOrNull { it.componentName == selected?.componentName }
132 ?: panels.firstOrNull()
133 item?.panelActivity
134 }
135 .stateIn(bgScope, SharingStarted.Eagerly, null)
136
137 private val taskFragmentFinished =
138 MutableSharedFlow<Long>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
139
140 fun onDreamEndUnexpectedly() {
141 powerManager.userActivity(
142 systemClock.uptimeMillis(),
143 PowerManager.USER_ACTIVITY_EVENT_OTHER,
144 PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS,
145 )
146 taskFragmentFinished.tryEmit(systemClock.currentTimeMillis())
147 }
148
149 /**
150 * Monitors if the current home panel package is updated and causes the dream to finish, and
151 * attempts to restart the dream in this case.
152 */
153 @SuppressLint("MissingPermission")
154 suspend fun monitorUpdatesAndRestart() {
155 taskFragmentFinished.resetReplayCache()
156 panelComponent
157 .flatMapLatest { component ->
158 if (component == null) return@flatMapLatest emptyFlow()
159 packageChangeInteractor.packageChanged(UserHandle.CURRENT, component.packageName)
160 }
161 .filter { it.isUpdate() }
162 // Wait for an UpdatedStarted - UpdateFinished pair to ensure the update has finished.
163 .pairwiseBy(::validateUpdatePair)
164 .filterNotNull()
165 .sample(taskFragmentFinished, ::Pair)
166 .filter { (updateStarted, lastFinishedTimestamp) ->
167 abs(updateStarted.timeMillis - lastFinishedTimestamp) <=
168 MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds
169 }
170 .collect { dreamManager.startDream() }
171 }
172
173 private data class PanelComponent(
174 val componentName: ComponentName,
175 val panelActivity: ComponentName,
176 )
177
178 companion object {
179 /**
180 * The maximum delay between a package update **starting** and the task fragment finishing
181 * which causes us to correlate the package update as the cause of the task fragment
182 * finishing.
183 */
184 val MAX_UPDATE_CORRELATION_DELAY = 500.milliseconds
185 }
186 }
187
PackageChangeModelnull188 private fun PackageChangeModel.isUpdate() =
189 this is PackageChangeModel.UpdateStarted || this is PackageChangeModel.UpdateFinished
190
191 private fun validateUpdatePair(
192 updateStarted: PackageChangeModel,
193 updateFinished: PackageChangeModel
194 ): PackageChangeModel.UpdateStarted? =
195 when {
196 !updateStarted.isSamePackage(updateFinished) -> null
197 updateStarted !is PackageChangeModel.UpdateStarted -> null
198 updateFinished !is PackageChangeModel.UpdateFinished -> null
199 else -> updateStarted
200 }
201