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 package com.android.systemui.communal.ui.viewmodel 18 19 import android.content.res.Resources 20 import android.view.View 21 import android.view.accessibility.AccessibilityNodeInfo 22 import com.android.systemui.communal.domain.interactor.CommunalInteractor 23 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 24 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor 25 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor 26 import com.android.systemui.communal.domain.model.CommunalContentModel 27 import com.android.systemui.communal.shared.model.CommunalBackgroundType 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.dagger.qualifiers.Main 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.log.LogBuffer 35 import com.android.systemui.log.core.Logger 36 import com.android.systemui.log.dagger.CommunalLog 37 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager 38 import com.android.systemui.media.controls.ui.view.MediaHost 39 import com.android.systemui.media.controls.ui.view.MediaHostState 40 import com.android.systemui.media.dagger.MediaModule 41 import com.android.systemui.res.R 42 import com.android.systemui.shade.domain.interactor.ShadeInteractor 43 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 44 import com.android.systemui.util.kotlin.BooleanFlowOperators.not 45 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 46 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated 47 import javax.inject.Inject 48 import javax.inject.Named 49 import kotlinx.coroutines.CoroutineDispatcher 50 import kotlinx.coroutines.CoroutineScope 51 import kotlinx.coroutines.ExperimentalCoroutinesApi 52 import kotlinx.coroutines.Job 53 import kotlinx.coroutines.channels.awaitClose 54 import kotlinx.coroutines.delay 55 import kotlinx.coroutines.flow.Flow 56 import kotlinx.coroutines.flow.MutableStateFlow 57 import kotlinx.coroutines.flow.asStateFlow 58 import kotlinx.coroutines.flow.combine 59 import kotlinx.coroutines.flow.distinctUntilChanged 60 import kotlinx.coroutines.flow.flatMapLatest 61 import kotlinx.coroutines.flow.flowOf 62 import kotlinx.coroutines.flow.flowOn 63 import kotlinx.coroutines.flow.map 64 import kotlinx.coroutines.flow.onEach 65 import kotlinx.coroutines.flow.onStart 66 import kotlinx.coroutines.launch 67 68 /** The default view model used for showing the communal hub. */ 69 @OptIn(ExperimentalCoroutinesApi::class) 70 @SysUISingleton 71 class CommunalViewModel 72 @Inject 73 constructor( 74 @Main val mainDispatcher: CoroutineDispatcher, 75 @Application private val scope: CoroutineScope, 76 @Main private val resources: Resources, 77 keyguardTransitionInteractor: KeyguardTransitionInteractor, 78 keyguardInteractor: KeyguardInteractor, 79 communalSceneInteractor: CommunalSceneInteractor, 80 private val communalInteractor: CommunalInteractor, 81 private val communalSettingsInteractor: CommunalSettingsInteractor, 82 tutorialInteractor: CommunalTutorialInteractor, 83 private val shadeInteractor: ShadeInteractor, 84 @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, 85 @CommunalLog logBuffer: LogBuffer, 86 ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) { 87 88 private val _isMediaHostVisible = 89 conflatedCallbackFlow<Boolean> { 90 val callback = { visible: Boolean -> 91 trySend(visible) 92 Unit 93 } 94 mediaHost.addVisibilityChangeListener(callback) 95 awaitClose { mediaHost.removeVisibilityChangeListener(callback) } 96 } 97 .onStart { emit(mediaHost.visible) } 98 .flowOn(mainDispatcher) 99 100 private val logger = Logger(logBuffer, "CommunalViewModel") 101 102 /** Communal content saved from the previous emission when the flow is active (not "frozen"). */ 103 private var frozenCommunalContent: List<CommunalContentModel>? = null 104 105 @OptIn(ExperimentalCoroutinesApi::class) 106 private val latestCommunalContent: Flow<List<CommunalContentModel>> = 107 tutorialInteractor.isTutorialAvailable 108 .flatMapLatest { isTutorialMode -> 109 if (isTutorialMode) { 110 return@flatMapLatest flowOf(communalInteractor.tutorialContent) 111 } 112 val ongoingContent = 113 _isMediaHostVisible.flatMapLatest { communalInteractor.getOngoingContent(it) } 114 combine( 115 ongoingContent, 116 communalInteractor.widgetContent, 117 communalInteractor.ctaTileContent, 118 ) { ongoing, widgets, ctaTile, 119 -> 120 ongoing + widgets + ctaTile 121 } 122 } 123 .onEach { models -> 124 frozenCommunalContent = models 125 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } 126 } 127 128 override val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(true) 129 130 /** 131 * Freeze the content flow, when an activity is about to show, like starting a timer via voice: 132 * 1) in handheld mode, use the keyguard occluded state; 133 * 2) in dreaming mode, where keyguard is already occluded by dream, use the dream wakeup 134 * signal. Since in this case the shell transition info does not include 135 * KEYGUARD_VISIBILITY_TRANSIT_FLAGS, KeyguardTransitionHandler will not run the 136 * occludeAnimation on KeyguardViewMediator. 137 */ 138 override val isCommunalContentFlowFrozen: Flow<Boolean> = 139 allOf( 140 keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB), 141 keyguardInteractor.isKeyguardOccluded, 142 not(keyguardInteractor.isAbleToDream) 143 ) 144 .distinctUntilChanged() 145 .onEach { logger.d("isCommunalContentFlowFrozen: $it") } 146 147 override val communalContent: Flow<List<CommunalContentModel>> = 148 isCommunalContentFlowFrozen 149 .flatMapLatestConflated { isFrozen -> 150 if (isFrozen) { 151 flowOf(frozenCommunalContent ?: emptyList()) 152 } else { 153 latestCommunalContent 154 } 155 } 156 .onEach { models -> 157 logger.d({ "CommunalContent: $str1" }) { str1 = models.joinToString { it.key } } 158 } 159 160 override val isEmptyState: Flow<Boolean> = 161 communalInteractor.widgetContent 162 .map { it.isEmpty() } 163 .distinctUntilChanged() 164 .onEach { logger.d("isEmptyState: $it") } 165 166 private val _currentPopup: MutableStateFlow<PopupType?> = MutableStateFlow(null) 167 override val currentPopup: Flow<PopupType?> = _currentPopup.asStateFlow() 168 169 // The widget is focusable for accessibility when the hub is fully visible and shade is not 170 // opened. 171 override val isFocusable: Flow<Boolean> = 172 combine( 173 keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB), 174 communalInteractor.isIdleOnCommunal, 175 shadeInteractor.isAnyFullyExpanded, 176 ) { transitionedToGlanceableHub, isIdleOnCommunal, isAnyFullyExpanded -> 177 transitionedToGlanceableHub && isIdleOnCommunal && !isAnyFullyExpanded 178 } 179 .distinctUntilChanged() 180 181 override val widgetAccessibilityDelegate = 182 object : View.AccessibilityDelegate() { 183 override fun onInitializeAccessibilityNodeInfo( 184 host: View, 185 info: AccessibilityNodeInfo 186 ) { 187 super.onInitializeAccessibilityNodeInfo(host, info) 188 // Hint user to long press in order to enter edit mode 189 info.addAction( 190 AccessibilityNodeInfo.AccessibilityAction( 191 AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id, 192 resources 193 .getString(R.string.accessibility_action_label_edit_widgets) 194 .lowercase() 195 ) 196 ) 197 } 198 } 199 200 private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) 201 val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow() 202 203 private val _isEnableWorkProfileDialogShowing: MutableStateFlow<Boolean> = 204 MutableStateFlow(false) 205 val isEnableWorkProfileDialogShowing: Flow<Boolean> = 206 _isEnableWorkProfileDialogShowing.asStateFlow() 207 208 init { 209 // Initialize our media host for the UMO. This only needs to happen once and must be done 210 // before the MediaHierarchyManager attempts to move the UMO to the hub. 211 with(mediaHost) { 212 expansion = MediaHostState.EXPANDED 213 expandedMatchesParentHeight = true 214 showsOnlyActiveMedia = true 215 falsingProtectionNeeded = false 216 init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) 217 } 218 } 219 220 override fun onOpenWidgetEditor( 221 preselectedKey: String?, 222 shouldOpenWidgetPickerOnStart: Boolean, 223 ) = communalInteractor.showWidgetEditor(preselectedKey, shouldOpenWidgetPickerOnStart) 224 225 override fun onDismissCtaTile() { 226 scope.launch { 227 communalInteractor.dismissCtaTile() 228 setCurrentPopupType(PopupType.CtaTile) 229 } 230 } 231 232 override fun onShowCustomizeWidgetButton() { 233 setCurrentPopupType(PopupType.CustomizeWidgetButton) 234 } 235 236 override fun onHidePopup() { 237 setCurrentPopupType(null) 238 } 239 240 override fun onOpenEnableWidgetDialog() { 241 setIsEnableWidgetDialogShowing(true) 242 } 243 244 fun onEnableWidgetDialogConfirm() { 245 communalInteractor.navigateToCommunalWidgetSettings() 246 setIsEnableWidgetDialogShowing(false) 247 } 248 249 fun onEnableWidgetDialogCancel() { 250 setIsEnableWidgetDialogShowing(false) 251 } 252 253 override fun onOpenEnableWorkProfileDialog() { 254 setIsEnableWorkProfileDialogShowing(true) 255 } 256 257 fun onEnableWorkProfileDialogConfirm() { 258 communalInteractor.unpauseWorkProfile() 259 setIsEnableWorkProfileDialogShowing(false) 260 } 261 262 fun onEnableWorkProfileDialogCancel() { 263 setIsEnableWorkProfileDialogShowing(false) 264 } 265 266 private fun setIsEnableWidgetDialogShowing(isVisible: Boolean) { 267 _isEnableWidgetDialogShowing.value = isVisible 268 } 269 270 private fun setIsEnableWorkProfileDialogShowing(isVisible: Boolean) { 271 _isEnableWorkProfileDialogShowing.value = isVisible 272 } 273 274 private fun setCurrentPopupType(popupType: PopupType?) { 275 _currentPopup.value = popupType 276 delayedHideCurrentPopupJob?.cancel() 277 278 if (popupType != null) { 279 delayedHideCurrentPopupJob = 280 scope.launch { 281 delay(POPUP_AUTO_HIDE_TIMEOUT_MS) 282 setCurrentPopupType(null) 283 } 284 } else { 285 delayedHideCurrentPopupJob = null 286 } 287 } 288 289 private var delayedHideCurrentPopupJob: Job? = null 290 291 /** Whether we can transition to a new scene based on a user gesture. */ 292 fun canChangeScene(): Boolean { 293 return !shadeInteractor.isAnyFullyExpanded.value 294 } 295 296 /** 297 * Whether touches should be disabled in communal. 298 * 299 * This is needed because the notification shade does not block touches in blank areas and these 300 * fall through to the glanceable hub, which we don't want. 301 */ 302 val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded) 303 304 // TODO(b/339667383): remove this temporary swipe gesture handle 305 /** 306 * The dream overlay has its own gesture handle as the SysUI window is not visible above the 307 * dream. This flow will be false when dreaming so that we don't show a duplicate handle when 308 * opening the hub over the dream. 309 */ 310 val showGestureIndicator: Flow<Boolean> = not(keyguardInteractor.isDreaming) 311 312 /** The type of background to use for the hub. */ 313 val communalBackground: Flow<CommunalBackgroundType> = 314 communalSettingsInteractor.communalBackground 315 316 companion object { 317 const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L 318 } 319 } 320 321 sealed class PopupType { 322 object CtaTile : PopupType() 323 324 object CustomizeWidgetButton : PopupType() 325 } 326