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.ComponentName 20 import com.android.app.tracing.coroutines.launchTraced as launch 21 import com.android.systemui.Flags 22 import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB 23 import com.android.systemui.communal.domain.interactor.CommunalInteractor 24 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 25 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor 26 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor 27 import com.android.systemui.communal.domain.model.CommunalContentModel 28 import com.android.systemui.communal.shared.log.CommunalMetricsLogger 29 import com.android.systemui.communal.shared.model.CommunalBackgroundType 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.dagger.qualifiers.Background 33 import com.android.systemui.dagger.qualifiers.Main 34 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 35 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 36 import com.android.systemui.keyguard.shared.model.KeyguardState 37 import com.android.systemui.keyguard.ui.transitions.BlurConfig 38 import com.android.systemui.log.LogBuffer 39 import com.android.systemui.log.core.Logger 40 import com.android.systemui.log.dagger.CommunalLog 41 import com.android.systemui.media.controls.ui.controller.MediaCarouselController 42 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager 43 import com.android.systemui.media.controls.ui.view.MediaHost 44 import com.android.systemui.media.controls.ui.view.MediaHostState 45 import com.android.systemui.media.dagger.MediaModule 46 import com.android.systemui.scene.shared.model.Scenes 47 import com.android.systemui.shade.domain.interactor.ShadeInteractor 48 import com.android.systemui.statusbar.KeyguardIndicationController 49 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 50 import com.android.systemui.util.kotlin.BooleanFlowOperators.not 51 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 52 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated 53 import javax.inject.Inject 54 import javax.inject.Named 55 import kotlinx.coroutines.CoroutineDispatcher 56 import kotlinx.coroutines.CoroutineScope 57 import kotlinx.coroutines.Job 58 import kotlinx.coroutines.channels.awaitClose 59 import kotlinx.coroutines.delay 60 import kotlinx.coroutines.flow.Flow 61 import kotlinx.coroutines.flow.MutableStateFlow 62 import kotlinx.coroutines.flow.SharingStarted 63 import kotlinx.coroutines.flow.StateFlow 64 import kotlinx.coroutines.flow.asStateFlow 65 import kotlinx.coroutines.flow.combine 66 import kotlinx.coroutines.flow.distinctUntilChanged 67 import kotlinx.coroutines.flow.flatMapLatest 68 import kotlinx.coroutines.flow.flowOf 69 import kotlinx.coroutines.flow.flowOn 70 import kotlinx.coroutines.flow.map 71 import kotlinx.coroutines.flow.onEach 72 import kotlinx.coroutines.flow.onStart 73 import kotlinx.coroutines.flow.stateIn 74 75 /** The default view model used for showing the communal hub. */ 76 @SysUISingleton 77 class CommunalViewModel 78 @Inject 79 constructor( 80 @Main val mainDispatcher: CoroutineDispatcher, 81 @Application private val scope: CoroutineScope, 82 @Background private val bgScope: CoroutineScope, 83 keyguardTransitionInteractor: KeyguardTransitionInteractor, 84 keyguardInteractor: KeyguardInteractor, 85 private val keyguardIndicationController: KeyguardIndicationController, 86 communalSceneInteractor: CommunalSceneInteractor, 87 private val communalInteractor: CommunalInteractor, 88 private val communalSettingsInteractor: CommunalSettingsInteractor, 89 tutorialInteractor: CommunalTutorialInteractor, 90 private val shadeInteractor: ShadeInteractor, 91 @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, 92 @CommunalLog logBuffer: LogBuffer, 93 private val metricsLogger: CommunalMetricsLogger, 94 mediaCarouselController: MediaCarouselController, 95 blurConfig: BlurConfig, 96 @Named(SWIPE_TO_HUB) private val swipeToHub: Boolean, 97 ) : 98 BaseCommunalViewModel( 99 communalSceneInteractor, 100 communalInteractor, 101 mediaHost, 102 mediaCarouselController, 103 ) { 104 105 private val logger = Logger(logBuffer, "CommunalViewModel") 106 107 private val isMediaHostVisible = 108 conflatedCallbackFlow { 109 val callback = { visible: Boolean -> 110 trySend(visible) 111 Unit 112 } 113 mediaHost.addVisibilityChangeListener(callback) 114 awaitClose { mediaHost.removeVisibilityChangeListener(callback) } 115 } 116 .onStart { 117 // Ensure the visibility state is correct when the hub is opened and this flow is 118 // started so that the UMO is shown when needed. The visibility state in MediaHost 119 // is not updated once its view has been detached, aka the hub is closed, which can 120 // result in this getting stuck as False and never being updated as the UMO is not 121 // shown. 122 mediaHost.updateViewVisibility() 123 emit(mediaHost.visible) 124 } 125 .distinctUntilChanged() 126 .onEach { logger.d({ "_isMediaHostVisible: $bool1" }) { bool1 = it } } 127 .flowOn(mainDispatcher) 128 129 /** Communal content saved from the previous emission when the flow is active (not "frozen"). */ 130 private var frozenCommunalContent: List<CommunalContentModel>? = null 131 132 private val ongoingContent = 133 isMediaHostVisible.flatMapLatest { isMediaHostVisible -> 134 communalInteractor.ongoingContent(isMediaHostVisible).onEach { 135 mediaHost.updateViewVisibility() 136 } 137 } 138 139 private val latestCommunalContent: Flow<List<CommunalContentModel>> = 140 tutorialInteractor.isTutorialAvailable 141 .flatMapLatest { isTutorialMode -> 142 if (isTutorialMode) { 143 return@flatMapLatest flowOf(communalInteractor.tutorialContent) 144 } 145 combine( 146 ongoingContent, 147 communalInteractor.widgetContent, 148 communalInteractor.ctaTileContent, 149 ) { ongoing, widgets, ctaTile -> 150 ongoing + widgets + ctaTile 151 } 152 } 153 .onEach { models -> 154 frozenCommunalContent = models 155 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } 156 } 157 158 override val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(true) 159 160 /** 161 * Freeze the content flow, when an activity is about to show, like starting a timer via voice: 162 * 1) in handheld mode, use the keyguard occluded state; 163 * 2) in dreaming mode, where keyguard is already occluded by dream, use the dream wakeup 164 * signal. Since in this case the shell transition info does not include 165 * KEYGUARD_VISIBILITY_TRANSIT_FLAGS, KeyguardTransitionHandler will not run the 166 * occludeAnimation on KeyguardViewMediator. 167 */ 168 override val isCommunalContentFlowFrozen: Flow<Boolean> = 169 allOf( 170 keyguardTransitionInteractor.isFinishedIn( 171 content = Scenes.Communal, 172 stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB, 173 ), 174 keyguardInteractor.isKeyguardOccluded, 175 not(keyguardInteractor.isAbleToDream), 176 ) 177 .distinctUntilChanged() 178 .onEach { logger.d("isCommunalContentFlowFrozen: $it") } 179 180 override val communalContent: Flow<List<CommunalContentModel>> = 181 isCommunalContentFlowFrozen 182 .flatMapLatestConflated { isFrozen -> 183 if (isFrozen) { 184 flowOf(frozenCommunalContent ?: emptyList()) 185 } else { 186 latestCommunalContent 187 } 188 } 189 .onEach { models -> 190 logger.d({ "CommunalContent: $str1" }) { str1 = models.joinToString { it.key } } 191 } 192 193 override val isEmptyState: Flow<Boolean> = 194 communalInteractor.widgetContent 195 .map { it.isEmpty() } 196 .distinctUntilChanged() 197 .onEach { logger.d("isEmptyState: $it") } 198 199 private val _currentPopup: MutableStateFlow<PopupType?> = MutableStateFlow(null) 200 override val currentPopup: Flow<PopupType?> = _currentPopup.asStateFlow() 201 202 // The widget is focusable for accessibility when the hub is fully visible and shade is not 203 // opened. 204 override val isFocusable: Flow<Boolean> = 205 combine( 206 keyguardTransitionInteractor.isFinishedIn( 207 content = Scenes.Communal, 208 stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB, 209 ), 210 communalInteractor.isIdleOnCommunal, 211 shadeInteractor.isAnyFullyExpanded, 212 ) { transitionedToGlanceableHub, isIdleOnCommunal, isAnyFullyExpanded -> 213 transitionedToGlanceableHub && isIdleOnCommunal && !isAnyFullyExpanded 214 } 215 .distinctUntilChanged() 216 217 private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) 218 val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow() 219 220 private val _isEnableWorkProfileDialogShowing: MutableStateFlow<Boolean> = 221 MutableStateFlow(false) 222 val isEnableWorkProfileDialogShowing: Flow<Boolean> = 223 _isEnableWorkProfileDialogShowing.asStateFlow() 224 225 val isUiBlurred: StateFlow<Boolean> = 226 if (Flags.bouncerUiRevamp()) { 227 keyguardInteractor.primaryBouncerShowing 228 } else { 229 MutableStateFlow(false) 230 } 231 232 val blurRadiusPx: Float = blurConfig.maxBlurRadiusPx 233 234 init { 235 // Initialize our media host for the UMO. This only needs to happen once and must be done 236 // before the MediaHierarchyManager attempts to move the UMO to the hub. 237 with(mediaHost) { 238 expansion = MediaHostState.EXPANDED 239 expandedMatchesParentHeight = true 240 if (v2FlagEnabled()) { 241 // Only show active media to match lock screen, not resumable media, which can 242 // persist 243 // for up to 2 days. 244 showsOnlyActiveMedia = true 245 } else { 246 // Maintain old behavior on tablet until V2 flag rolls out. 247 showsOnlyActiveMedia = false 248 } 249 falsingProtectionNeeded = false 250 disableScrolling = true 251 init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) 252 } 253 } 254 255 override fun onOpenWidgetEditor(shouldOpenWidgetPickerOnStart: Boolean) { 256 persistScrollPosition() 257 communalInteractor.showWidgetEditor(shouldOpenWidgetPickerOnStart) 258 } 259 260 override fun onDismissCtaTile() { 261 scope.launch { 262 communalInteractor.dismissCtaTile() 263 setCurrentPopupType(PopupType.CtaTile) 264 } 265 } 266 267 override fun onShowPreviousMedia() { 268 mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1) 269 } 270 271 override fun onShowNextMedia() { 272 mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1) 273 } 274 275 override fun onTapWidget(componentName: ComponentName, rank: Int) { 276 metricsLogger.logTapWidget(componentName.flattenToString(), rank) 277 } 278 279 fun onClick() { 280 keyguardIndicationController.showActionToUnlock() 281 } 282 283 override fun onLongClick() { 284 if (Flags.glanceableHubDirectEditMode()) { 285 onOpenWidgetEditor(false) 286 return 287 } 288 setCurrentPopupType(PopupType.CustomizeWidgetButton) 289 } 290 291 override fun onHidePopup() { 292 setCurrentPopupType(null) 293 } 294 295 override fun onOpenEnableWidgetDialog() { 296 setIsEnableWidgetDialogShowing(true) 297 } 298 299 fun onEnableWidgetDialogConfirm() { 300 communalInteractor.navigateToCommunalWidgetSettings() 301 setIsEnableWidgetDialogShowing(false) 302 } 303 304 fun onEnableWidgetDialogCancel() { 305 setIsEnableWidgetDialogShowing(false) 306 } 307 308 override fun onOpenEnableWorkProfileDialog() { 309 setIsEnableWorkProfileDialogShowing(true) 310 } 311 312 fun onEnableWorkProfileDialogConfirm() { 313 communalInteractor.unpauseWorkProfile() 314 setIsEnableWorkProfileDialogShowing(false) 315 } 316 317 fun onEnableWorkProfileDialogCancel() { 318 setIsEnableWorkProfileDialogShowing(false) 319 } 320 321 private fun setIsEnableWidgetDialogShowing(isVisible: Boolean) { 322 _isEnableWidgetDialogShowing.value = isVisible 323 } 324 325 private fun setIsEnableWorkProfileDialogShowing(isVisible: Boolean) { 326 _isEnableWorkProfileDialogShowing.value = isVisible 327 } 328 329 private fun setCurrentPopupType(popupType: PopupType?) { 330 _currentPopup.value = popupType 331 delayedHideCurrentPopupJob?.cancel() 332 333 if (popupType != null) { 334 delayedHideCurrentPopupJob = 335 scope.launch { 336 delay(POPUP_AUTO_HIDE_TIMEOUT_MS) 337 setCurrentPopupType(null) 338 } 339 } else { 340 delayedHideCurrentPopupJob = null 341 } 342 } 343 344 private var delayedHideCurrentPopupJob: Job? = null 345 346 /** Whether we can transition to a new scene based on a user gesture. */ 347 fun canChangeScene(): Boolean { 348 return !shadeInteractor.isAnyFullyExpanded.value 349 } 350 351 /** 352 * Whether touches should be disabled in communal. 353 * 354 * This is needed because the notification shade does not block touches in blank areas and these 355 * fall through to the glanceable hub, which we don't want. 356 * 357 * Using a StateFlow as the value does not necessarily change when hub becomes available. 358 */ 359 val touchesAllowed: StateFlow<Boolean> = 360 not(shadeInteractor.isAnyFullyExpanded) 361 .stateIn(bgScope, SharingStarted.Eagerly, initialValue = false) 362 363 /** The type of background to use for the hub. */ 364 val communalBackground: Flow<CommunalBackgroundType> = 365 communalSettingsInteractor.communalBackground 366 367 /** See [CommunalSettingsInteractor.isV2FlagEnabled] */ 368 fun v2FlagEnabled(): Boolean = communalSettingsInteractor.isV2FlagEnabled() 369 370 val swipeToHubEnabled: Flow<Boolean> by lazy { 371 val inAllowedDeviceState = 372 if (v2FlagEnabled()) { 373 communalSettingsInteractor.manualOpenEnabled 374 } else { 375 MutableStateFlow(swipeToHub) 376 } 377 378 if (v2FlagEnabled()) { 379 val inAllowedKeyguardState = 380 keyguardTransitionInteractor.startedKeyguardTransitionStep.map { 381 it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB 382 } 383 allOf(inAllowedDeviceState, inAllowedKeyguardState) 384 } else { 385 inAllowedDeviceState 386 } 387 } 388 389 val swipeFromHubInLandscape: Flow<Boolean> = communalSceneInteractor.willRotateToPortrait 390 391 fun onOrientationChange(orientation: Int) = 392 communalSceneInteractor.setCommunalContainerOrientation(orientation) 393 394 companion object { 395 const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L 396 } 397 } 398 399 sealed class PopupType { 400 data object CtaTile : PopupType() 401 402 data object CustomizeWidgetButton : PopupType() 403 } 404