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.model.MediaCommonModel 27 import com.android.systemui.media.controls.util.MediaFlags 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 recommendationsViewModel: MediaRecommendationsViewModel, 53 private val logger: MediaUiEventLogger, 54 private val mediaFlags: MediaFlags, 55 ) { 56 57 val mediaItems: StateFlow<List<MediaCommonViewModel>> = 58 interactor.currentMedia 59 .map { sortedItems -> 60 val mediaList = buildList { 61 sortedItems.forEach { commonModel -> 62 // When view is started we should make sure to clean models that are pending 63 // removal. 64 // This action should only be triggered once. 65 if (!allowReorder || !modelsPendingRemoval.contains(commonModel)) { 66 when (commonModel) { 67 is MediaCommonModel.MediaControl -> add(toViewModel(commonModel)) 68 is MediaCommonModel.MediaRecommendations -> 69 add(toViewModel(commonModel)) 70 } 71 } 72 } 73 } 74 if (allowReorder) { 75 if (modelsPendingRemoval.size > 0) { 76 updateHostVisibility() 77 } 78 modelsPendingRemoval.clear() 79 } 80 allowReorder = false 81 82 mediaList 83 } 84 .stateIn( 85 scope = applicationScope, 86 started = SharingStarted.WhileSubscribed(), 87 initialValue = emptyList(), 88 ) 89 90 var updateHostVisibility: () -> Unit = {} 91 92 private val mediaControlByInstanceId = 93 mutableMapOf<InstanceId, MediaCommonViewModel.MediaControl>() 94 95 private var mediaRecs: MediaCommonViewModel.MediaRecommendations? = null 96 97 private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf() 98 99 private var allowReorder = false 100 101 fun onSwipeToDismiss() { 102 logger.logSwipeDismiss() 103 interactor.onSwipeToDismiss() 104 } 105 106 fun onReorderingAllowed() { 107 allowReorder = true 108 interactor.reorderMedia() 109 } 110 111 private fun toViewModel( 112 commonModel: MediaCommonModel.MediaControl 113 ): MediaCommonViewModel.MediaControl { 114 val instanceId = commonModel.mediaLoadedModel.instanceId 115 return mediaControlByInstanceId[instanceId]?.copy( 116 immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi 117 ) 118 ?: MediaCommonViewModel.MediaControl( 119 instanceId = instanceId, 120 immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi, 121 controlViewModel = createMediaControlViewModel(instanceId), 122 onAdded = { onMediaControlAddedOrUpdated(it, commonModel) }, 123 onRemoved = { 124 interactor.removeMediaControl(instanceId, delay = 0L) 125 mediaControlByInstanceId.remove(instanceId) 126 }, 127 onUpdated = { onMediaControlAddedOrUpdated(it, commonModel) }, 128 isMediaFromRec = commonModel.isMediaFromRec 129 ) 130 .also { mediaControlByInstanceId[instanceId] = it } 131 } 132 133 private fun createMediaControlViewModel(instanceId: InstanceId): MediaControlViewModel { 134 return MediaControlViewModel( 135 applicationContext = applicationContext, 136 backgroundDispatcher = backgroundDispatcher, 137 backgroundExecutor = backgroundExecutor, 138 interactor = controlInteractorFactory.create(instanceId), 139 logger = logger, 140 ) 141 } 142 143 private fun toViewModel( 144 commonModel: MediaCommonModel.MediaRecommendations 145 ): MediaCommonViewModel.MediaRecommendations { 146 return mediaRecs?.copy( 147 key = commonModel.recsLoadingModel.key, 148 loadingEnabled = 149 interactor.isRecommendationActive() || mediaFlags.isPersistentSsCardEnabled() 150 ) 151 ?: MediaCommonViewModel.MediaRecommendations( 152 key = commonModel.recsLoadingModel.key, 153 loadingEnabled = 154 interactor.isRecommendationActive() || 155 mediaFlags.isPersistentSsCardEnabled(), 156 recsViewModel = recommendationsViewModel, 157 onAdded = { commonViewModel -> 158 onMediaRecommendationAddedOrUpdated( 159 commonViewModel as MediaCommonViewModel.MediaRecommendations 160 ) 161 }, 162 onRemoved = { immediatelyRemove -> 163 onMediaRecommendationRemoved(commonModel, immediatelyRemove) 164 }, 165 onUpdated = { commonViewModel -> 166 onMediaRecommendationAddedOrUpdated( 167 commonViewModel as MediaCommonViewModel.MediaRecommendations 168 ) 169 }, 170 ) 171 .also { mediaRecs = it } 172 } 173 174 private fun onMediaControlAddedOrUpdated( 175 commonViewModel: MediaCommonViewModel, 176 commonModel: MediaCommonModel.MediaControl 177 ) { 178 // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED) 179 if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) { 180 // This media control is due for removal as it is now paused + timed out, and resumption 181 // setting is off. 182 if (isReorderingAllowed()) { 183 commonViewModel.onRemoved(true) 184 } else { 185 modelsPendingRemoval.add(commonModel) 186 } 187 } else { 188 modelsPendingRemoval.remove(commonModel) 189 } 190 } 191 192 private fun onMediaRecommendationAddedOrUpdated( 193 commonViewModel: MediaCommonViewModel.MediaRecommendations 194 ) { 195 if (!interactor.isRecommendationActive()) { 196 if (!mediaFlags.isPersistentSsCardEnabled()) { 197 commonViewModel.onRemoved(true) 198 } 199 } else { 200 // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED) 201 } 202 } 203 204 private fun onMediaRecommendationRemoved( 205 commonModel: MediaCommonModel.MediaRecommendations, 206 immediatelyRemove: Boolean 207 ) { 208 if (immediatelyRemove || isReorderingAllowed()) { 209 interactor.dismissSmartspaceRecommendation(commonModel.recsLoadingModel.key, 0L) 210 mediaRecs = null 211 if (!immediatelyRemove) { 212 // Although it wasn't requested, we were able to process the removal 213 // immediately since reordering is allowed. So, notify hosts to update 214 updateHostVisibility() 215 } 216 } else { 217 modelsPendingRemoval.add(commonModel) 218 } 219 } 220 221 private fun isReorderingAllowed(): Boolean { 222 return visualStabilityProvider.isReorderingAllowed 223 } 224 } 225