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