• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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