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.domain.interactor 18 19 import android.app.smartspace.SmartspaceTarget 20 import android.content.ComponentName 21 import android.content.Intent 22 import android.content.IntentFilter 23 import android.os.UserHandle 24 import android.os.UserManager 25 import android.provider.Settings 26 import com.android.compose.animation.scene.ObservableTransitionState 27 import com.android.compose.animation.scene.SceneKey 28 import com.android.compose.animation.scene.TransitionKey 29 import com.android.systemui.broadcast.BroadcastDispatcher 30 import com.android.systemui.communal.data.repository.CommunalMediaRepository 31 import com.android.systemui.communal.data.repository.CommunalPrefsRepository 32 import com.android.systemui.communal.data.repository.CommunalWidgetRepository 33 import com.android.systemui.communal.domain.model.CommunalContentModel 34 import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent 35 import com.android.systemui.communal.shared.model.CommunalContentSize 36 import com.android.systemui.communal.shared.model.CommunalContentSize.FULL 37 import com.android.systemui.communal.shared.model.CommunalContentSize.HALF 38 import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD 39 import com.android.systemui.communal.shared.model.CommunalScenes 40 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel 41 import com.android.systemui.communal.shared.model.EditModeState 42 import com.android.systemui.communal.widgets.CommunalAppWidgetHost 43 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter 44 import com.android.systemui.communal.widgets.WidgetConfigurator 45 import com.android.systemui.dagger.SysUISingleton 46 import com.android.systemui.dagger.qualifiers.Application 47 import com.android.systemui.dagger.qualifiers.Background 48 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 49 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 50 import com.android.systemui.keyguard.shared.model.Edge 51 import com.android.systemui.keyguard.shared.model.KeyguardState 52 import com.android.systemui.log.LogBuffer 53 import com.android.systemui.log.core.Logger 54 import com.android.systemui.log.dagger.CommunalLog 55 import com.android.systemui.log.dagger.CommunalTableLog 56 import com.android.systemui.log.table.TableLogBuffer 57 import com.android.systemui.log.table.logDiffsForTable 58 import com.android.systemui.plugins.ActivityStarter 59 import com.android.systemui.scene.domain.interactor.SceneInteractor 60 import com.android.systemui.scene.shared.flag.SceneContainerFlag 61 import com.android.systemui.scene.shared.model.Scenes 62 import com.android.systemui.settings.UserTracker 63 import com.android.systemui.smartspace.data.repository.SmartspaceRepository 64 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 65 import com.android.systemui.util.kotlin.BooleanFlowOperators.not 66 import com.android.systemui.util.kotlin.emitOnStart 67 import javax.inject.Inject 68 import kotlinx.coroutines.CoroutineDispatcher 69 import kotlinx.coroutines.CoroutineScope 70 import kotlinx.coroutines.ExperimentalCoroutinesApi 71 import kotlinx.coroutines.channels.BufferOverflow 72 import kotlinx.coroutines.flow.Flow 73 import kotlinx.coroutines.flow.MutableSharedFlow 74 import kotlinx.coroutines.flow.MutableStateFlow 75 import kotlinx.coroutines.flow.SharingStarted 76 import kotlinx.coroutines.flow.StateFlow 77 import kotlinx.coroutines.flow.asSharedFlow 78 import kotlinx.coroutines.flow.asStateFlow 79 import kotlinx.coroutines.flow.combine 80 import kotlinx.coroutines.flow.distinctUntilChanged 81 import kotlinx.coroutines.flow.emptyFlow 82 import kotlinx.coroutines.flow.filter 83 import kotlinx.coroutines.flow.flatMapLatest 84 import kotlinx.coroutines.flow.flow 85 import kotlinx.coroutines.flow.flowOf 86 import kotlinx.coroutines.flow.flowOn 87 import kotlinx.coroutines.flow.map 88 import kotlinx.coroutines.flow.onEach 89 import kotlinx.coroutines.flow.shareIn 90 import kotlinx.coroutines.flow.stateIn 91 92 /** Encapsulates business-logic related to communal mode. */ 93 @OptIn(ExperimentalCoroutinesApi::class) 94 @SysUISingleton 95 class CommunalInteractor 96 @Inject 97 constructor( 98 @Application val applicationScope: CoroutineScope, 99 @Background val bgDispatcher: CoroutineDispatcher, 100 broadcastDispatcher: BroadcastDispatcher, 101 private val widgetRepository: CommunalWidgetRepository, 102 private val communalPrefsRepository: CommunalPrefsRepository, 103 private val mediaRepository: CommunalMediaRepository, 104 smartspaceRepository: SmartspaceRepository, 105 keyguardInteractor: KeyguardInteractor, 106 keyguardTransitionInteractor: KeyguardTransitionInteractor, 107 communalSettingsInteractor: CommunalSettingsInteractor, 108 private val appWidgetHost: CommunalAppWidgetHost, 109 private val editWidgetsActivityStarter: EditWidgetsActivityStarter, 110 private val userTracker: UserTracker, 111 private val activityStarter: ActivityStarter, 112 private val userManager: UserManager, 113 private val communalSceneInteractor: CommunalSceneInteractor, 114 sceneInteractor: SceneInteractor, 115 @CommunalLog logBuffer: LogBuffer, 116 @CommunalTableLog tableLogBuffer: TableLogBuffer, 117 ) { 118 private val logger = Logger(logBuffer, "CommunalInteractor") 119 120 private val _editModeOpen = MutableStateFlow(false) 121 122 /** Whether edit mode is currently open. */ 123 val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow() 124 125 /** Whether communal features are enabled. */ 126 val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled 127 128 /** Whether communal features are enabled and available. */ 129 val isCommunalAvailable: Flow<Boolean> = 130 allOf( 131 communalSettingsInteractor.isCommunalEnabled, 132 not(keyguardInteractor.isEncryptedOrLockdown), 133 keyguardInteractor.isKeyguardShowing 134 ) 135 .distinctUntilChanged() 136 .onEach { available -> 137 logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { 138 bool1 = available 139 } 140 } 141 .logDiffsForTable( 142 tableLogBuffer = tableLogBuffer, 143 columnPrefix = "", 144 columnName = "isCommunalAvailable", 145 initialValue = false, 146 ) 147 .shareIn( 148 scope = applicationScope, 149 started = SharingStarted.WhileSubscribed(), 150 replay = 1, 151 ) 152 153 /** Whether to show communal when exiting the occluded state. */ 154 val showCommunalFromOccluded: Flow<Boolean> = 155 keyguardTransitionInteractor.startedKeyguardTransitionStep 156 .filter { step -> step.to == KeyguardState.OCCLUDED } 157 .combine(isCommunalAvailable, ::Pair) 158 .map { (step, available) -> available && step.from == KeyguardState.GLANCEABLE_HUB } 159 .flowOn(bgDispatcher) 160 .stateIn( 161 scope = applicationScope, 162 started = SharingStarted.WhileSubscribed(), 163 initialValue = false, 164 ) 165 166 /** Whether to start dreaming when returning from occluded */ 167 val dreamFromOccluded: Flow<Boolean> = 168 keyguardTransitionInteractor 169 .transition(Edge.create(to = KeyguardState.OCCLUDED)) 170 .map { it.from == KeyguardState.DREAMING } 171 .stateIn(scope = applicationScope, SharingStarted.Eagerly, false) 172 173 /** 174 * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene]. 175 * 176 * If [isCommunalAvailable] is false, will return [CommunalScenes.Blank] 177 */ 178 @Deprecated( 179 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 180 ) 181 val desiredScene: Flow<SceneKey> = communalSceneInteractor.currentScene 182 183 /** Transition state of the hub mode. */ 184 @Deprecated( 185 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 186 ) 187 val transitionState: StateFlow<ObservableTransitionState> = 188 communalSceneInteractor.transitionState 189 190 private val _userActivity: MutableSharedFlow<Unit> = 191 MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) 192 val userActivity: Flow<Unit> = _userActivity.asSharedFlow() 193 194 fun signalUserInteraction() { 195 _userActivity.tryEmit(Unit) 196 } 197 198 /** 199 * Repopulates the communal widgets database by first reading a backed-up state from disk and 200 * updating the widget ids indicated by [oldToNewWidgetIdMap]. The backed-up state is removed 201 * from disk afterwards. 202 */ 203 fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { 204 widgetRepository.restoreWidgets(oldToNewWidgetIdMap) 205 } 206 207 /** 208 * Aborts the task of restoring widgets from a backup. The backed up state stored on disk is 209 * removed. 210 */ 211 fun abortRestoreWidgets() { 212 widgetRepository.abortRestoreWidgets() 213 } 214 215 /** 216 * Updates the transition state of the hub [SceneTransitionLayout]. 217 * 218 * Note that you must call is with `null` when the UI is done or risk a memory leak. 219 */ 220 @Deprecated( 221 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 222 ) 223 fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) = 224 communalSceneInteractor.setTransitionState(transitionState) 225 226 /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */ 227 @Deprecated( 228 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 229 ) 230 fun transitionProgressToScene(targetScene: SceneKey) = 231 communalSceneInteractor.transitionProgressToScene(targetScene) 232 233 /** 234 * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is 235 * the [CommunalScenes.Communal]. 236 * 237 * This will be true as soon as the desired scene is set programmatically or at whatever point 238 * during a fling that SceneTransitionLayout determines that the end state will be the communal 239 * scene. The value also does not change while flinging away until the target scene is no longer 240 * communal. 241 * 242 * If you need a flow that is only true when communal is fully showing and not in transition, 243 * use [isIdleOnCommunal]. 244 */ 245 // TODO(b/323215860): rename to something more appropriate after cleaning up usages 246 val isCommunalShowing: Flow<Boolean> = 247 flow { emit(SceneContainerFlag.isEnabled) } 248 .flatMapLatest { sceneContainerEnabled -> 249 if (sceneContainerEnabled) { 250 sceneInteractor.currentScene.map { it == Scenes.Communal } 251 } else { 252 desiredScene.map { it == CommunalScenes.Communal } 253 } 254 } 255 .distinctUntilChanged() 256 .onEach { showing -> 257 logger.i({ "Communal is ${if (bool1) "showing" else "gone"}" }) { bool1 = showing } 258 } 259 .logDiffsForTable( 260 tableLogBuffer = tableLogBuffer, 261 columnPrefix = "", 262 columnName = "isCommunalShowing", 263 initialValue = false, 264 ) 265 .shareIn( 266 scope = applicationScope, 267 started = SharingStarted.WhileSubscribed(), 268 replay = 1, 269 ) 270 271 /** 272 * Flow that emits a boolean if the communal UI is fully visible and not in transition. 273 * 274 * This will not be true while transitioning to the hub and will turn false immediately when a 275 * swipe to exit the hub starts. 276 */ 277 @Deprecated( 278 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 279 ) 280 val isIdleOnCommunal: StateFlow<Boolean> = communalSceneInteractor.isIdleOnCommunal 281 282 /** 283 * Flow that emits a boolean if any portion of the communal UI is visible at all. 284 * 285 * This flow will be true during any transition and when idle on the communal scene. 286 */ 287 @Deprecated( 288 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 289 ) 290 val isCommunalVisible: Flow<Boolean> = communalSceneInteractor.isCommunalVisible 291 292 /** 293 * Asks for an asynchronous scene witch to [newScene], which will use the corresponding 294 * installed transition or the one specified by [transitionKey], if provided. 295 */ 296 @Deprecated( 297 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 298 ) 299 fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) = 300 communalSceneInteractor.changeScene(newScene, transitionKey) 301 302 fun setEditModeOpen(isOpen: Boolean) { 303 _editModeOpen.value = isOpen 304 } 305 306 /** Show the widget editor Activity. */ 307 fun showWidgetEditor( 308 preselectedKey: String? = null, 309 shouldOpenWidgetPickerOnStart: Boolean = false, 310 ) { 311 communalSceneInteractor.setEditModeState(EditModeState.STARTING) 312 editWidgetsActivityStarter.startActivity(preselectedKey, shouldOpenWidgetPickerOnStart) 313 } 314 315 /** 316 * Navigates to communal widget setting after user has unlocked the device. Currently, this 317 * setting resides within the Hub Mode settings screen. 318 */ 319 fun navigateToCommunalWidgetSettings() { 320 activityStarter.postStartActivityDismissingKeyguard( 321 Intent(Settings.ACTION_COMMUNAL_SETTING) 322 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP), 323 /* delay= */ 0, 324 ) 325 } 326 327 /** Dismiss the CTA tile from the hub in view mode. */ 328 suspend fun dismissCtaTile() = communalPrefsRepository.setCtaDismissedForCurrentUser() 329 330 /** Add a widget at the specified position. */ 331 fun addWidget( 332 componentName: ComponentName, 333 user: UserHandle, 334 priority: Int, 335 configurator: WidgetConfigurator?, 336 ) = widgetRepository.addWidget(componentName, user, priority, configurator) 337 338 /** 339 * Delete a widget by id. Called when user deletes a widget from the hub or a widget is 340 * uninstalled from App widget host. 341 */ 342 fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) 343 344 /** 345 * Reorder the widgets. 346 * 347 * @param widgetIdToPriorityMap mapping of the widget ids to their new priorities. 348 */ 349 fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) = 350 widgetRepository.updateWidgetOrder(widgetIdToPriorityMap) 351 352 /** Request to unpause work profile that is currently in quiet mode. */ 353 fun unpauseWorkProfile() { 354 userTracker.userProfiles 355 .find { it.isManagedProfile } 356 ?.userHandle 357 ?.let { userHandle -> 358 userManager.requestQuietModeEnabled(/* enableQuietMode */ false, userHandle) 359 } 360 } 361 362 /** Returns true if work profile is in quiet mode (disabled) for user handle. */ 363 private fun isQuietModeEnabled(userHandle: UserHandle): Boolean = 364 userManager.isManagedProfile(userHandle.identifier) && 365 userManager.isQuietModeEnabled(userHandle) 366 367 /** Emits whenever a work profile pause or unpause broadcast is received. */ 368 private val updateOnWorkProfileBroadcastReceived: Flow<Unit> = 369 broadcastDispatcher 370 .broadcastFlow( 371 filter = 372 IntentFilter().apply { 373 addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) 374 addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) 375 }, 376 ) 377 .emitOnStart() 378 379 /** All widgets present in db. */ 380 val communalWidgets: Flow<List<CommunalWidgetContentModel>> = 381 isCommunalAvailable.flatMapLatest { available -> 382 if (!available) emptyFlow() else widgetRepository.communalWidgets 383 } 384 385 /** A list of widget content to be displayed in the communal hub. */ 386 val widgetContent: Flow<List<WidgetContent>> = 387 combine( 388 widgetRepository.communalWidgets 389 .map { filterWidgetsByExistingUsers(it) } 390 .combine(communalSettingsInteractor.allowedByDevicePolicyForWorkProfile) { 391 // exclude widgets under work profile if not allowed by device policy 392 widgets, 393 allowedForWorkProfile -> 394 filterWidgetsAllowedByDevicePolicy(widgets, allowedForWorkProfile) 395 }, 396 communalSettingsInteractor.communalWidgetCategories, 397 updateOnWorkProfileBroadcastReceived, 398 ) { widgets, allowedCategories, _ -> 399 widgets.map { widget -> 400 when (widget) { 401 is CommunalWidgetContentModel.Available -> { 402 if (widget.providerInfo.widgetCategory and allowedCategories != 0) { 403 // At least one category this widget specified is allowed, so show it 404 WidgetContent.Widget( 405 appWidgetId = widget.appWidgetId, 406 providerInfo = widget.providerInfo, 407 appWidgetHost = appWidgetHost, 408 inQuietMode = isQuietModeEnabled(widget.providerInfo.profile) 409 ) 410 } else { 411 WidgetContent.DisabledWidget( 412 appWidgetId = widget.appWidgetId, 413 providerInfo = widget.providerInfo, 414 ) 415 } 416 } 417 is CommunalWidgetContentModel.Pending -> { 418 WidgetContent.PendingWidget( 419 appWidgetId = widget.appWidgetId, 420 packageName = widget.packageName, 421 icon = widget.icon, 422 ) 423 } 424 } 425 } 426 } 427 428 /** Filter widgets based on whether their associated profile is allowed by device policy. */ 429 private fun filterWidgetsAllowedByDevicePolicy( 430 list: List<CommunalWidgetContentModel>, 431 allowedByDevicePolicyForWorkProfile: Boolean 432 ): List<CommunalWidgetContentModel> = 433 if (allowedByDevicePolicyForWorkProfile) { 434 list 435 } else { 436 // Get associated work profile for the currently selected user. 437 val workProfile = userTracker.userProfiles.find { it.isManagedProfile } 438 list.filter { model -> 439 val uid = 440 when (model) { 441 is CommunalWidgetContentModel.Available -> 442 model.providerInfo.profile.identifier 443 is CommunalWidgetContentModel.Pending -> model.user.identifier 444 } 445 uid != workProfile?.id 446 } 447 } 448 449 /** A flow of available smartspace targets. Currently only showing timers. */ 450 private val smartspaceTargets: Flow<List<SmartspaceTarget>> = 451 if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) { 452 flowOf(emptyList()) 453 } else { 454 smartspaceRepository.communalSmartspaceTargets.map { targets -> 455 targets.filter { target -> 456 target.featureType == SmartspaceTarget.FEATURE_TIMER && 457 target.remoteViews != null 458 } 459 } 460 } 461 462 /** CTA tile to be displayed in the glanceable hub (view mode). */ 463 val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> = 464 communalPrefsRepository.isCtaDismissed.map { isDismissed -> 465 if (isDismissed) emptyList() else listOf(CommunalContentModel.CtaTileInViewMode()) 466 } 467 468 /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ 469 val tutorialContent: List<CommunalContentModel.Tutorial> = 470 listOf( 471 CommunalContentModel.Tutorial(id = 0, FULL), 472 CommunalContentModel.Tutorial(id = 1, THIRD), 473 CommunalContentModel.Tutorial(id = 2, THIRD), 474 CommunalContentModel.Tutorial(id = 3, THIRD), 475 CommunalContentModel.Tutorial(id = 4, HALF), 476 CommunalContentModel.Tutorial(id = 5, HALF), 477 CommunalContentModel.Tutorial(id = 6, HALF), 478 CommunalContentModel.Tutorial(id = 7, HALF), 479 ) 480 481 /** 482 * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and 483 * sized dynamically. 484 */ 485 fun getOngoingContent(mediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> = 486 combine(smartspaceTargets, mediaRepository.mediaModel) { smartspace, media -> 487 val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>() 488 489 // Add smartspace 490 ongoingContent.addAll( 491 smartspace.map { target -> 492 CommunalContentModel.Smartspace( 493 smartspaceTargetId = target.smartspaceTargetId, 494 remoteViews = target.remoteViews!!, 495 createdTimestampMillis = target.creationTimeMillis, 496 ) 497 } 498 ) 499 500 // Add UMO 501 if (mediaHostVisible && media.hasActiveMediaOrRecommendation) { 502 ongoingContent.add( 503 CommunalContentModel.Umo( 504 createdTimestampMillis = media.createdTimestampMillis, 505 ) 506 ) 507 } 508 509 // Order by creation time descending 510 ongoingContent.sortByDescending { it.createdTimestampMillis } 511 512 // Dynamic sizing 513 ongoingContent.forEachIndexed { index, model -> 514 model.size = dynamicContentSize(ongoingContent.size, index) 515 } 516 517 ongoingContent 518 } 519 .flowOn(bgDispatcher) 520 521 /** 522 * Filter and retain widgets associated with an existing user, safeguarding against displaying 523 * stale data following user deletion. 524 */ 525 private fun filterWidgetsByExistingUsers( 526 list: List<CommunalWidgetContentModel>, 527 ): List<CommunalWidgetContentModel> { 528 val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() 529 return list.filter { widget -> 530 when (widget) { 531 is CommunalWidgetContentModel.Available -> 532 currentUserIds.contains(widget.providerInfo.profile?.identifier) 533 is CommunalWidgetContentModel.Pending -> true 534 } 535 } 536 } 537 538 companion object { 539 /** 540 * The user activity timeout which should be used when the communal hub is opened. A value 541 * of -1 means that the user's chosen screen timeout will be used instead. 542 */ 543 const val AWAKE_INTERVAL_MS = -1 544 545 /** 546 * Calculates the content size dynamically based on the total number of contents of that 547 * type. 548 * 549 * Contents with the same type are expected to fill each column evenly. Currently there are 550 * three possible sizes. When the total number is 1, size for that content is [FULL], when 551 * the total number is 2, size for each is [HALF], and 3, size for each is [THIRD]. 552 * 553 * When dynamic contents fill in multiple columns, the first column follows the algorithm 554 * above, and the remaining contents are packed in [THIRD]s. For example, when the total 555 * number if 4, the first one is [FULL], filling the column, and the remaining 3 are 556 * [THIRD]. 557 * 558 * @param size The total number of contents of this type. 559 * @param index The index of the current content of this type. 560 */ 561 private fun dynamicContentSize(size: Int, index: Int): CommunalContentSize { 562 val remainder = size % CommunalContentSize.entries.size 563 return CommunalContentSize.toSize( 564 span = 565 FULL.span / 566 if (index > remainder - 1) CommunalContentSize.entries.size else remainder 567 ) 568 } 569 } 570 } 571