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.content.ComponentName 20 import android.content.Intent 21 import android.content.IntentFilter 22 import android.content.pm.UserInfo 23 import android.os.UserHandle 24 import android.os.UserManager 25 import android.provider.Settings 26 import com.android.app.tracing.coroutines.launchTraced as launch 27 import com.android.compose.animation.scene.ObservableTransitionState 28 import com.android.compose.animation.scene.SceneKey 29 import com.android.compose.animation.scene.TransitionKey 30 import com.android.systemui.Flags.communalResponsiveGrid 31 import com.android.systemui.Flags.glanceableHubBlurredBackground 32 import com.android.systemui.broadcast.BroadcastDispatcher 33 import com.android.systemui.communal.data.repository.CommunalMediaRepository 34 import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository 35 import com.android.systemui.communal.data.repository.CommunalWidgetRepository 36 import com.android.systemui.communal.domain.model.CommunalContentModel 37 import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent 38 import com.android.systemui.communal.shared.model.CommunalBackgroundType 39 import com.android.systemui.communal.shared.model.CommunalContentSize 40 import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.FULL 41 import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.HALF 42 import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.THIRD 43 import com.android.systemui.communal.shared.model.CommunalScenes 44 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel 45 import com.android.systemui.communal.shared.model.EditModeState 46 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter 47 import com.android.systemui.communal.widgets.WidgetConfigurator 48 import com.android.systemui.dagger.SysUISingleton 49 import com.android.systemui.dagger.qualifiers.Application 50 import com.android.systemui.dagger.qualifiers.Background 51 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 52 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 53 import com.android.systemui.keyguard.shared.model.Edge 54 import com.android.systemui.keyguard.shared.model.KeyguardState 55 import com.android.systemui.log.LogBuffer 56 import com.android.systemui.log.core.Logger 57 import com.android.systemui.log.dagger.CommunalLog 58 import com.android.systemui.log.dagger.CommunalTableLog 59 import com.android.systemui.log.table.TableLogBuffer 60 import com.android.systemui.log.table.logDiffsForTable 61 import com.android.systemui.plugins.ActivityStarter 62 import com.android.systemui.scene.domain.interactor.SceneInteractor 63 import com.android.systemui.scene.shared.flag.SceneContainerFlag 64 import com.android.systemui.scene.shared.model.Scenes 65 import com.android.systemui.settings.UserTracker 66 import com.android.systemui.statusbar.phone.ManagedProfileController 67 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 68 import com.android.systemui.util.kotlin.emitOnStart 69 import javax.inject.Inject 70 import kotlin.time.Duration.Companion.minutes 71 import kotlinx.coroutines.CoroutineDispatcher 72 import kotlinx.coroutines.CoroutineScope 73 import kotlinx.coroutines.channels.BufferOverflow 74 import kotlinx.coroutines.delay 75 import kotlinx.coroutines.flow.Flow 76 import kotlinx.coroutines.flow.MutableSharedFlow 77 import kotlinx.coroutines.flow.MutableStateFlow 78 import kotlinx.coroutines.flow.SharingStarted 79 import kotlinx.coroutines.flow.StateFlow 80 import kotlinx.coroutines.flow.asSharedFlow 81 import kotlinx.coroutines.flow.asStateFlow 82 import kotlinx.coroutines.flow.combine 83 import kotlinx.coroutines.flow.distinctUntilChanged 84 import kotlinx.coroutines.flow.emptyFlow 85 import kotlinx.coroutines.flow.filter 86 import kotlinx.coroutines.flow.flatMapLatest 87 import kotlinx.coroutines.flow.flow 88 import kotlinx.coroutines.flow.flowOf 89 import kotlinx.coroutines.flow.flowOn 90 import kotlinx.coroutines.flow.map 91 import kotlinx.coroutines.flow.onEach 92 import kotlinx.coroutines.flow.shareIn 93 import kotlinx.coroutines.flow.stateIn 94 95 /** Encapsulates business-logic related to communal mode. */ 96 @SysUISingleton 97 class CommunalInteractor 98 @Inject 99 constructor( 100 @Application val applicationScope: CoroutineScope, 101 @Background private val bgScope: CoroutineScope, 102 @Background val bgDispatcher: CoroutineDispatcher, 103 broadcastDispatcher: BroadcastDispatcher, 104 private val widgetRepository: CommunalWidgetRepository, 105 private val communalPrefsInteractor: CommunalPrefsInteractor, 106 private val mediaRepository: CommunalMediaRepository, 107 private val smartspaceRepository: CommunalSmartspaceRepository, 108 keyguardInteractor: KeyguardInteractor, 109 keyguardTransitionInteractor: KeyguardTransitionInteractor, 110 communalSettingsInteractor: CommunalSettingsInteractor, 111 private val editWidgetsActivityStarter: EditWidgetsActivityStarter, 112 private val userTracker: UserTracker, 113 private val activityStarter: ActivityStarter, 114 private val userManager: UserManager, 115 private val communalSceneInteractor: CommunalSceneInteractor, 116 sceneInteractor: SceneInteractor, 117 @CommunalLog logBuffer: LogBuffer, 118 @CommunalTableLog tableLogBuffer: TableLogBuffer, 119 private val managedProfileController: ManagedProfileController, 120 ) { 121 private val logger = Logger(logBuffer, "CommunalInteractor") 122 123 private val _editModeOpen = MutableStateFlow(false) 124 125 /** 126 * Whether edit mode is currently open. This will be true from onCreate to onDestroy in 127 * [EditWidgetsActivity] and thus does not correspond to whether or not the activity is visible. 128 * 129 * Note that since this is called in onDestroy, it's not guaranteed to ever be set to false when 130 * edit mode is closed, such as in the case that a user exits edit mode manually with a back 131 * gesture or navigation gesture. 132 */ 133 val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow() 134 135 private val _editActivityShowing = MutableStateFlow(false) 136 137 /** 138 * Whether the edit mode activity is currently showing. This is true from onStart to onStop in 139 * [EditWidgetsActivity] so may be false even when the user is in edit mode, such as when a 140 * widget's individual configuration activity has launched. 141 */ 142 val editActivityShowing: StateFlow<Boolean> = _editActivityShowing.asStateFlow() 143 144 private val _selectedKey: MutableStateFlow<String?> = MutableStateFlow(null) 145 146 val selectedKey: StateFlow<String?> = _selectedKey.asStateFlow() 147 148 /** Whether communal features are enabled. */ 149 val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled 150 151 /** Whether communal features are enabled and available. */ 152 @Deprecated("Use isCommunalEnabled instead", replaceWith = ReplaceWith("isCommunalEnabled")) 153 val isCommunalAvailable: Flow<Boolean> by lazy { 154 val availableFlow = 155 if (communalSettingsInteractor.isV2FlagEnabled()) { 156 communalSettingsInteractor.isCommunalEnabled 157 } else { 158 allOf( 159 communalSettingsInteractor.isCommunalEnabled, 160 keyguardInteractor.isKeyguardShowing, 161 ) 162 } 163 availableFlow 164 .onEach { available -> 165 logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { 166 bool1 = available 167 } 168 } 169 .logDiffsForTable( 170 tableLogBuffer = tableLogBuffer, 171 columnName = "isCommunalAvailable", 172 initialValue = false, 173 ) 174 .shareIn( 175 scope = applicationScope, 176 started = SharingStarted.WhileSubscribed(), 177 replay = 1, 178 ) 179 } 180 181 private val _isDisclaimerDismissed = MutableStateFlow(false) 182 val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() 183 184 fun setDisclaimerDismissed() { 185 bgScope.launch("$TAG#setDisclaimerDismissed") { 186 _isDisclaimerDismissed.value = true 187 delay(DISCLAIMER_RESET_MILLIS) 188 _isDisclaimerDismissed.value = false 189 } 190 } 191 192 fun setSelectedKey(key: String?) { 193 _selectedKey.value = key 194 } 195 196 /** Whether to show communal when exiting the occluded state. */ 197 val showCommunalFromOccluded: Flow<Boolean> = 198 keyguardTransitionInteractor.startedKeyguardTransitionStep 199 .filter { step -> step.to == KeyguardState.OCCLUDED } 200 .combine(isCommunalAvailable, ::Pair) 201 .map { (step, available) -> 202 available && 203 (step.from == KeyguardState.GLANCEABLE_HUB || 204 step.from == KeyguardState.DREAMING) 205 } 206 .flowOn(bgDispatcher) 207 .stateIn( 208 scope = applicationScope, 209 started = SharingStarted.WhileSubscribed(), 210 initialValue = false, 211 ) 212 213 /** Whether to start dreaming when returning from occluded */ 214 val dreamFromOccluded: Flow<Boolean> = 215 keyguardTransitionInteractor 216 .transition(Edge.create(to = KeyguardState.OCCLUDED)) 217 .map { it.from == KeyguardState.DREAMING } 218 .stateIn(scope = applicationScope, SharingStarted.Eagerly, false) 219 220 /** 221 * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene]. 222 * 223 * If [isCommunalAvailable] is false, will return [CommunalScenes.Blank] 224 */ 225 @Deprecated( 226 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 227 ) 228 val desiredScene: Flow<SceneKey> = communalSceneInteractor.currentScene 229 230 /** Transition state of the hub mode. */ 231 @Deprecated( 232 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 233 ) 234 val transitionState: StateFlow<ObservableTransitionState> = 235 communalSceneInteractor.transitionState 236 237 private val _userActivity: MutableSharedFlow<Unit> = 238 MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) 239 val userActivity: Flow<Unit> = _userActivity.asSharedFlow() 240 241 fun signalUserInteraction() { 242 _userActivity.tryEmit(Unit) 243 } 244 245 /** 246 * Repopulates the communal widgets database by first reading a backed-up state from disk and 247 * updating the widget ids indicated by [oldToNewWidgetIdMap]. The backed-up state is removed 248 * from disk afterwards. 249 */ 250 fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { 251 widgetRepository.restoreWidgets(oldToNewWidgetIdMap) 252 } 253 254 /** 255 * Aborts the task of restoring widgets from a backup. The backed up state stored on disk is 256 * removed. 257 */ 258 fun abortRestoreWidgets() { 259 widgetRepository.abortRestoreWidgets() 260 } 261 262 /** 263 * Updates the transition state of the hub [SceneTransitionLayout]. 264 * 265 * Note that you must call is with `null` when the UI is done or risk a memory leak. 266 */ 267 @Deprecated( 268 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 269 ) 270 fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) = 271 communalSceneInteractor.setTransitionState(transitionState) 272 273 /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */ 274 @Deprecated( 275 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 276 ) 277 fun transitionProgressToScene(targetScene: SceneKey) = 278 communalSceneInteractor.transitionProgressToScene(targetScene) 279 280 /** 281 * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is 282 * the [CommunalScenes.Communal]. 283 * 284 * This will be true as soon as the desired scene is set programmatically or at whatever point 285 * during a fling that SceneTransitionLayout determines that the end state will be the communal 286 * scene. The value also does not change while flinging away until the target scene is no longer 287 * communal. 288 * 289 * If you need a flow that is only true when communal is fully showing and not in transition, 290 * use [isIdleOnCommunal]. 291 */ 292 // TODO(b/323215860): rename to something more appropriate after cleaning up usages 293 val isCommunalShowing: Flow<Boolean> = 294 flow { emit(SceneContainerFlag.isEnabled) } 295 .flatMapLatest { sceneContainerEnabled -> 296 if (sceneContainerEnabled) { 297 sceneInteractor.currentScene.map { it == Scenes.Communal } 298 } else { 299 desiredScene.map { it == CommunalScenes.Communal } 300 } 301 } 302 .distinctUntilChanged() 303 .onEach { showing -> 304 logger.i({ "Communal is ${if (bool1) "showing" else "gone"}" }) { bool1 = showing } 305 } 306 .logDiffsForTable( 307 tableLogBuffer = tableLogBuffer, 308 columnName = "isCommunalShowing", 309 initialValue = false, 310 ) 311 .shareIn( 312 scope = applicationScope, 313 started = SharingStarted.WhileSubscribed(), 314 replay = 1, 315 ) 316 317 /** 318 * Flow that emits {@code true} whenever communal is influencing the shown background on the 319 * screen. This happens when the background for communal is set to blur and communal is visible. 320 * This is used by other components to determine when blur-related emitted values for communal 321 * should be considered. 322 */ 323 val isCommunalBlurring: StateFlow<Boolean> = 324 communalSceneInteractor.isCommunalVisible 325 .combine(communalSettingsInteractor.communalBackground) { showing, background -> 326 showing && 327 background == CommunalBackgroundType.BLUR && 328 glanceableHubBlurredBackground() 329 } 330 .stateIn( 331 scope = applicationScope, 332 started = SharingStarted.Eagerly, 333 initialValue = false, 334 ) 335 336 /** 337 * Flow that emits a boolean if the communal UI is fully visible and not in transition. 338 * 339 * This will not be true while transitioning to the hub and will turn false immediately when a 340 * swipe to exit the hub starts. 341 */ 342 @Deprecated( 343 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 344 ) 345 val isIdleOnCommunal: StateFlow<Boolean> = communalSceneInteractor.isIdleOnCommunal 346 347 /** 348 * Flow that emits a boolean if any portion of the communal UI is visible at all. 349 * 350 * This flow will be true during any transition and when idle on the communal scene. 351 */ 352 @Deprecated( 353 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 354 ) 355 val isCommunalVisible: Flow<Boolean> = communalSceneInteractor.isCommunalVisible 356 357 /** 358 * Asks for an asynchronous scene witch to [newScene], which will use the corresponding 359 * installed transition or the one specified by [transitionKey], if provided. 360 */ 361 @Deprecated( 362 "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead" 363 ) 364 fun changeScene( 365 newScene: SceneKey, 366 loggingReason: String, 367 transitionKey: TransitionKey? = null, 368 ) = communalSceneInteractor.changeScene(newScene, loggingReason, transitionKey) 369 370 fun setEditModeOpen(isOpen: Boolean) { 371 _editModeOpen.value = isOpen 372 } 373 374 fun setEditActivityShowing(isOpen: Boolean) { 375 _editActivityShowing.value = isOpen 376 } 377 378 /** Show the widget editor Activity. */ 379 fun showWidgetEditor(shouldOpenWidgetPickerOnStart: Boolean = false) { 380 communalSceneInteractor.setEditModeState(EditModeState.STARTING) 381 editWidgetsActivityStarter.startActivity(shouldOpenWidgetPickerOnStart) 382 } 383 384 /** 385 * Navigates to communal widget setting after user has unlocked the device. Currently, this 386 * setting resides within the Hub Mode settings screen. 387 */ 388 fun navigateToCommunalWidgetSettings() { 389 activityStarter.postStartActivityDismissingKeyguard( 390 Intent(Settings.ACTION_COMMUNAL_SETTING) 391 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP), 392 /* delay= */ 0, 393 ) 394 } 395 396 /** Dismiss the CTA tile from the hub in view mode. */ 397 suspend fun dismissCtaTile() = communalPrefsInteractor.setCtaDismissed() 398 399 /** 400 * Add a widget at the specified rank. If rank is not provided, the widget will be added at the 401 * end. 402 */ 403 fun addWidget( 404 componentName: ComponentName, 405 user: UserHandle, 406 rank: Int? = null, 407 configurator: WidgetConfigurator?, 408 ) = widgetRepository.addWidget(componentName, user, rank, configurator) 409 410 /** 411 * Delete a widget by id. Called when user deletes a widget from the hub or a widget is 412 * uninstalled from App widget host. 413 */ 414 fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) 415 416 /** 417 * Reorder the widgets. 418 * 419 * @param widgetIdToRankMap mapping of the widget ids to their new priorities. 420 */ 421 fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) = 422 widgetRepository.updateWidgetOrder(widgetIdToRankMap) 423 424 fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) { 425 widgetRepository.resizeWidget(appWidgetId, spanY, widgetIdToRankMap) 426 } 427 428 /** Request to unpause work profile that is currently in quiet mode. */ 429 fun unpauseWorkProfile() { 430 managedProfileController.setWorkModeEnabled(true) 431 } 432 433 /** Returns true if work profile is in quiet mode (disabled) for user handle. */ 434 private fun isQuietModeEnabled(userHandle: UserHandle): Boolean = 435 userManager.isManagedProfile(userHandle.identifier) && 436 userManager.isQuietModeEnabled(userHandle) 437 438 /** Emits whenever a work profile pause or unpause broadcast is received. */ 439 private val updateOnWorkProfileBroadcastReceived: Flow<Unit> = 440 broadcastDispatcher 441 .broadcastFlow( 442 filter = 443 IntentFilter().apply { 444 addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) 445 addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) 446 } 447 ) 448 .emitOnStart() 449 450 /** All widgets present in db. */ 451 val communalWidgets: Flow<List<CommunalWidgetContentModel>> = 452 isCommunalAvailable.flatMapLatest { available -> 453 if (!available) emptyFlow() else widgetRepository.communalWidgets 454 } 455 456 /** A list of widget content to be displayed in the communal hub. */ 457 val widgetContent: Flow<List<WidgetContent>> = 458 combine( 459 widgetRepository.communalWidgets 460 .map { filterWidgetsByExistingUsers(it) } 461 .combine(communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy) { 462 // exclude widgets under work profile if not allowed by device policy 463 widgets, 464 disallowedByPolicyUser -> 465 filterWidgetsAllowedByDevicePolicy(widgets, disallowedByPolicyUser) 466 }, 467 updateOnWorkProfileBroadcastReceived, 468 ) { widgets, _ -> 469 widgets.map { widget -> 470 when (widget) { 471 is CommunalWidgetContentModel.Available -> { 472 WidgetContent.Widget( 473 appWidgetId = widget.appWidgetId, 474 rank = widget.rank, 475 providerInfo = widget.providerInfo, 476 inQuietMode = isQuietModeEnabled(widget.providerInfo.profile), 477 size = CommunalContentSize.toSize(widget.spanY), 478 ) 479 } 480 481 is CommunalWidgetContentModel.Pending -> { 482 WidgetContent.PendingWidget( 483 appWidgetId = widget.appWidgetId, 484 rank = widget.rank, 485 componentName = widget.componentName, 486 icon = widget.icon, 487 size = CommunalContentSize.toSize(widget.spanY), 488 ) 489 } 490 } 491 } 492 } 493 494 /** Filter widgets based on whether their associated profile is allowed by device policy. */ 495 private fun filterWidgetsAllowedByDevicePolicy( 496 list: List<CommunalWidgetContentModel>, 497 disallowedByDevicePolicyUser: UserInfo?, 498 ): List<CommunalWidgetContentModel> = 499 if (disallowedByDevicePolicyUser == null) { 500 list 501 } else { 502 list.filter { model -> 503 val uid = 504 when (model) { 505 is CommunalWidgetContentModel.Available -> 506 model.providerInfo.profile.identifier 507 508 is CommunalWidgetContentModel.Pending -> model.user.identifier 509 } 510 uid != disallowedByDevicePolicyUser.id 511 } 512 } 513 514 /** CTA tile to be displayed in the glanceable hub (view mode). */ 515 val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> by lazy { 516 if (communalSettingsInteractor.isV2FlagEnabled()) { 517 flowOf(listOf<CommunalContentModel.CtaTileInViewMode>()) 518 } else { 519 communalPrefsInteractor.isCtaDismissed.map { isDismissed -> 520 if (isDismissed) listOf<CommunalContentModel.CtaTileInViewMode>() 521 else listOf(CommunalContentModel.CtaTileInViewMode()) 522 } 523 } 524 } 525 526 /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ 527 val tutorialContent: List<CommunalContentModel.Tutorial> = 528 listOf( 529 CommunalContentModel.Tutorial(id = 0, FULL), 530 CommunalContentModel.Tutorial(id = 1, THIRD), 531 CommunalContentModel.Tutorial(id = 2, THIRD), 532 CommunalContentModel.Tutorial(id = 3, THIRD), 533 CommunalContentModel.Tutorial(id = 4, HALF), 534 CommunalContentModel.Tutorial(id = 5, HALF), 535 CommunalContentModel.Tutorial(id = 6, HALF), 536 CommunalContentModel.Tutorial(id = 7, HALF), 537 ) 538 539 /** 540 * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and 541 * sized dynamically. 542 */ 543 fun ongoingContent(isMediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> = 544 combine(smartspaceRepository.timers, mediaRepository.mediaModel) { timers, media -> 545 val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>() 546 547 // Add smartspace timers 548 ongoingContent.addAll( 549 timers.map { timer -> 550 CommunalContentModel.Smartspace( 551 smartspaceTargetId = timer.smartspaceTargetId, 552 remoteViews = timer.remoteViews, 553 createdTimestampMillis = timer.createdTimestampMillis, 554 ) 555 } 556 ) 557 558 // Add UMO 559 if (isMediaHostVisible && media.hasAnyMediaOrRecommendation) { 560 ongoingContent.add( 561 CommunalContentModel.Umo( 562 createdTimestampMillis = media.createdTimestampMillis 563 ) 564 ) 565 } 566 567 // Order by creation time descending. 568 ongoingContent.sortByDescending { it.createdTimestampMillis } 569 // Resize the items. 570 if (!communalResponsiveGrid()) { 571 ongoingContent.resizeItems() 572 } 573 574 // Return the sorted and resized items. 575 ongoingContent 576 } 577 .flowOn(bgDispatcher) 578 579 /** 580 * Filter and retain widgets associated with an existing user, safeguarding against displaying 581 * stale data following user deletion. 582 */ 583 private fun filterWidgetsByExistingUsers( 584 list: List<CommunalWidgetContentModel> 585 ): List<CommunalWidgetContentModel> { 586 val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() 587 return list.filter { widget -> 588 when (widget) { 589 is CommunalWidgetContentModel.Available -> 590 currentUserIds.contains(widget.providerInfo.profile?.identifier) 591 592 is CommunalWidgetContentModel.Pending -> true 593 } 594 } 595 } 596 597 // Dynamically resizes the height of items in the list of ongoing items such that they fit in 598 // columns in as compact a space as possible. 599 // 600 // Currently there are three possible sizes. When the total number is 1, size for that content 601 // is [FULL], when the total number is 2, size for each is [HALF], and 3, size for each is 602 // [THIRD]. 603 // 604 // This algorithm also respects each item's minimum size. All items in a column will have the 605 // same size, and all items in a column will be no smaller than any item's minimum size. 606 private fun List<CommunalContentModel.Ongoing>.resizeItems() { 607 fun resizeColumn(c: List<CommunalContentModel.Ongoing>) { 608 if (c.isEmpty()) return 609 val newSize = CommunalContentSize.toSize(span = FULL.span / c.size) 610 c.forEach { item -> item.size = newSize } 611 } 612 613 val column = mutableListOf<CommunalContentModel.Ongoing>() 614 var available = FULL.span 615 616 forEach { item -> 617 if (available < item.minSize.span) { 618 resizeColumn(column) 619 column.clear() 620 available = FULL.span 621 } 622 623 column.add(item) 624 available -= item.minSize.span 625 } 626 627 // Make sure to resize the final column. 628 resizeColumn(column) 629 } 630 631 companion object { 632 const val TAG = "CommunalInteractor" 633 634 /** 635 * The amount of time between showing the widget disclaimer to the user as measured from the 636 * moment the disclaimer is dimsissed. 637 */ 638 val DISCLAIMER_RESET_MILLIS = 30.minutes 639 640 /** 641 * The user activity timeout which should be used when the communal hub is opened. A value 642 * of -1 means that the user's chosen screen timeout will be used instead. 643 */ 644 const val AWAKE_INTERVAL_MS = -1 645 } 646 647 /** 648 * {@link #setScrollPosition} persists the current communal grid scroll position (to volatile 649 * memory) so that the next presentation of the grid (either as glanceable hub or edit mode) can 650 * restore position. 651 */ 652 fun setScrollPosition(firstVisibleItemIndex: Int, firstVisibleItemOffset: Int) { 653 _firstVisibleItemIndex = firstVisibleItemIndex 654 _firstVisibleItemOffset = firstVisibleItemOffset 655 } 656 657 val firstVisibleItemIndex: Int 658 get() = _firstVisibleItemIndex 659 660 private var _firstVisibleItemIndex: Int = 0 661 662 val firstVisibleItemOffset: Int 663 get() = _firstVisibleItemOffset 664 665 private var _firstVisibleItemOffset: Int = 0 666 } 667