• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.media.controls.ui.viewmodel
18 
19 import android.content.Context
20 import com.android.internal.logging.InstanceId
21 import com.android.systemui.dagger.SysUISingleton
22 import com.android.systemui.dagger.qualifiers.Application
23 import com.android.systemui.dagger.qualifiers.Background
24 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
25 import com.android.systemui.media.controls.domain.pipeline.interactor.factory.MediaControlInteractorFactory
26 import com.android.systemui.media.controls.shared.MediaLogger
27 import com.android.systemui.media.controls.shared.model.MediaCommonModel
28 import com.android.systemui.media.controls.util.MediaUiEventLogger
29 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
30 import com.android.systemui.util.Utils
31 import java.util.concurrent.Executor
32 import javax.inject.Inject
33 import kotlinx.coroutines.CoroutineDispatcher
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.flow.SharingStarted
36 import kotlinx.coroutines.flow.StateFlow
37 import kotlinx.coroutines.flow.map
38 import kotlinx.coroutines.flow.stateIn
39 
40 /** Models UI state and handles user inputs for media carousel */
41 @SysUISingleton
42 class MediaCarouselViewModel
43 @Inject
44 constructor(
45     @Application private val applicationScope: CoroutineScope,
46     @Application private val applicationContext: Context,
47     @Background private val backgroundDispatcher: CoroutineDispatcher,
48     @Background private val backgroundExecutor: Executor,
49     private val visualStabilityProvider: VisualStabilityProvider,
50     private val interactor: MediaCarouselInteractor,
51     private val controlInteractorFactory: MediaControlInteractorFactory,
52     private val logger: MediaUiEventLogger,
53     private val mediaLogger: MediaLogger,
54 ) {
55 
56     val hasAnyMediaOrRecommendations: StateFlow<Boolean> = interactor.hasAnyMediaOrRecommendation
57     val hasActiveMediaOrRecommendations: StateFlow<Boolean> =
58         interactor.hasActiveMediaOrRecommendation
59     val mediaItems: StateFlow<List<MediaControlViewModel>> =
60         interactor.currentMedia
61             .map { sortedItems ->
62                 val mediaList = buildList {
63                     sortedItems.forEach { commonModel ->
64                         // When view is started we should make sure to clean models that are pending
65                         // removal. This action should only be triggered once.
66                         if (!allowReorder || !modelsPendingRemoval.contains(commonModel)) {
67                             add(toViewModel(commonModel))
68                         }
69                     }
70                 }
71                 if (allowReorder) {
72                     if (modelsPendingRemoval.size > 0) {
73                         updateHostVisibility()
74                     }
75                     modelsPendingRemoval.clear()
76                 }
77                 allowReorder = false
78 
79                 mediaList
80             }
81             .stateIn(
82                 scope = applicationScope,
83                 started = SharingStarted.WhileSubscribed(),
84                 initialValue = emptyList(),
85             )
86 
87     var updateHostVisibility: () -> Unit = {}
88 
89     private val mediaControlByInstanceId = mutableMapOf<InstanceId, MediaControlViewModel>()
90 
91     private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf()
92 
93     private var allowReorder = false
94 
95     fun onSwipeToDismiss() {
96         logger.logSwipeDismiss()
97         interactor.onSwipeToDismiss()
98     }
99 
100     fun onReorderingAllowed() {
101         allowReorder = true
102         interactor.reorderMedia()
103     }
104 
105     private fun toViewModel(commonModel: MediaCommonModel): MediaControlViewModel {
106         val instanceId = commonModel.mediaLoadedModel.instanceId
107         return mediaControlByInstanceId[instanceId]?.copy(updateTime = commonModel.updateTime)
108             ?: MediaControlViewModel(
109                     applicationContext = applicationContext,
110                     backgroundDispatcher = backgroundDispatcher,
111                     backgroundExecutor = backgroundExecutor,
112                     interactor = controlInteractorFactory.create(instanceId),
113                     logger = logger,
114                     instanceId = instanceId,
115                     onAdded = {
116                         mediaLogger.logMediaCardAdded(instanceId)
117                         onMediaControlAddedOrUpdated(it, commonModel)
118                     },
119                     onRemoved = {
120                         interactor.removeMediaControl(instanceId, delay = 0L)
121                         mediaControlByInstanceId.remove(instanceId)
122                         mediaLogger.logMediaCardRemoved(instanceId)
123                     },
124                     onUpdated = { onMediaControlAddedOrUpdated(it, commonModel) },
125                     updateTime = commonModel.updateTime,
126                 )
127                 .also { mediaControlByInstanceId[instanceId] = it }
128     }
129 
130     private fun onMediaControlAddedOrUpdated(
131         controlViewModel: MediaControlViewModel,
132         commonModel: MediaCommonModel,
133     ) {
134         if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) {
135             // This media control is due for removal as it is now paused + timed out, and resumption
136             // setting is off.
137             if (isReorderingAllowed()) {
138                 controlViewModel.onRemoved(true)
139             } else {
140                 modelsPendingRemoval.add(commonModel)
141             }
142         } else {
143             modelsPendingRemoval.remove(commonModel)
144         }
145     }
146 
147     private fun isReorderingAllowed(): Boolean {
148         return visualStabilityProvider.isReorderingAllowed
149     }
150 }
151