Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +18 −14 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.media.controls.data.repository import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope Loading @@ -44,16 +45,17 @@ class MediaFilterRepositoryTest : SysuiTestCase() { testScope.runTest { val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) val userMedia = MediaData().copy(active = true) val instanceId = InstanceId.fakeInstanceId(123) val userMedia = MediaData().copy(active = true, instanceId = instanceId) underTest.addSelectedUserMediaEntry(KEY, userMedia) underTest.addSelectedUserMediaEntry(userMedia) assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)).isEqualTo(userMedia) underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) underTest.addSelectedUserMediaEntry(userMedia.copy(active = false)) assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia) assertThat(selectedUserEntries?.get(KEY)?.active).isFalse() assertThat(selectedUserEntries?.get(instanceId)).isNotEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)?.active).isFalse() } @Test Loading @@ -61,13 +63,14 @@ class MediaFilterRepositoryTest : SysuiTestCase() { testScope.runTest { val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) val userMedia = MediaData() val instanceId = InstanceId.fakeInstanceId(123) val userMedia = MediaData().copy(instanceId = instanceId) underTest.addSelectedUserMediaEntry(KEY, userMedia) underTest.addSelectedUserMediaEntry(userMedia) assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)).isEqualTo(userMedia) assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() assertThat(underTest.removeSelectedUserMediaEntry(instanceId, userMedia)).isTrue() } @Test Loading @@ -75,13 +78,14 @@ class MediaFilterRepositoryTest : SysuiTestCase() { testScope.runTest { val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) val userMedia = MediaData() val instanceId = InstanceId.fakeInstanceId(123) val userMedia = MediaData().copy(instanceId = instanceId) underTest.addSelectedUserMediaEntry(KEY, userMedia) underTest.addSelectedUserMediaEntry(userMedia) assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)).isEqualTo(userMedia) assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia) assertThat(underTest.removeSelectedUserMediaEntry(instanceId)).isEqualTo(userMedia) } @Test Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +7 −6 Original line number Diff line number Diff line Loading @@ -56,13 +56,13 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val userMedia = MediaData().copy(active = true) mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasActiveMedia).isTrue() assertThat(hasAnyMedia).isTrue() mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) mediaFilterRepository.addSelectedUserMediaEntry(userMedia.copy(active = false)) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() Loading @@ -78,14 +78,16 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) val userMedia = MediaData().copy(active = false) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isTrue() assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() Loading Loading @@ -114,7 +116,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() Loading Loading @@ -193,7 +195,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } companion object { private const val KEY = "KEY" private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } } packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt +16 −14 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.media.controls.data.repository import com.android.internal.logging.InstanceId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData Loading @@ -28,17 +29,18 @@ import kotlinx.coroutines.flow.asStateFlow @SysUISingleton class MediaFilterRepository @Inject constructor() { /** Key of media control that recommendations card reactivated. */ private val _reactivatedKey: MutableStateFlow<String?> = MutableStateFlow(null) val reactivatedKey: StateFlow<String?> = _reactivatedKey.asStateFlow() /** Instance id of media control that recommendations card reactivated. */ private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null) val reactivatedId: StateFlow<InstanceId?> = _reactivatedId.asStateFlow() private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> = MutableStateFlow(SmartspaceMediaData()) val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow() private val _selectedUserEntries: MutableStateFlow<Map<String, MediaData>> = private val _selectedUserEntries: MutableStateFlow<Map<InstanceId, MediaData>> = MutableStateFlow(LinkedHashMap()) val selectedUserEntries: StateFlow<Map<String, MediaData>> = _selectedUserEntries.asStateFlow() val selectedUserEntries: StateFlow<Map<InstanceId, MediaData>> = _selectedUserEntries.asStateFlow() private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> = MutableStateFlow(LinkedHashMap()) Loading @@ -62,9 +64,9 @@ class MediaFilterRepository @Inject constructor() { return mediaData } fun addSelectedUserMediaEntry(key: String, data: MediaData) { val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) entries[key] = data fun addSelectedUserMediaEntry(data: MediaData) { val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value) entries[data.instanceId] = data _selectedUserEntries.value = entries } Loading @@ -73,8 +75,8 @@ class MediaFilterRepository @Inject constructor() { * * @return media data if an entry is actually removed, `null` otherwise. */ fun removeSelectedUserMediaEntry(key: String): MediaData? { val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) fun removeSelectedUserMediaEntry(key: InstanceId): MediaData? { val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value) val mediaData = entries.remove(key) _selectedUserEntries.value = entries return mediaData Loading @@ -85,8 +87,8 @@ class MediaFilterRepository @Inject constructor() { * * @return true if media data is removed, false otherwise. */ fun removeSelectedUserMediaEntry(key: String, data: MediaData): Boolean { val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) fun removeSelectedUserMediaEntry(key: InstanceId, data: MediaData): Boolean { val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value) val succeed = entries.remove(key, data) if (!succeed) { return false Loading @@ -105,7 +107,7 @@ class MediaFilterRepository @Inject constructor() { } /** Updates media control key that recommendations card reactivated. */ fun setReactivatedKey(key: String?) { _reactivatedKey.value = key fun setReactivatedId(instanceId: InstanceId?) { _reactivatedId.value = instanceId } } packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +82 −41 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.os.SystemProperties import android.util.Log import com.android.internal.annotations.KeepForWeakReference import com.android.internal.annotations.VisibleForTesting import com.android.internal.logging.InstanceId import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.data.repository.MediaFilterRepository Loading Loading @@ -66,8 +67,8 @@ constructor( private val mediaFlags: MediaFlags, private val mediaFilterRepository: MediaFilterRepository, ) : MediaDataManager.Listener { private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() val listeners: Set<MediaDataManager.Listener> private val _listeners: MutableSet<Listener> = mutableSetOf() val listeners: Set<Listener> get() = _listeners.toSet() lateinit var mediaDataManager: MediaDataManager Loading Loading @@ -108,13 +109,10 @@ constructor( return } if (oldKey != null && oldKey != key) { mediaFilterRepository.removeSelectedUserMediaEntry(oldKey) } mediaFilterRepository.addSelectedUserMediaEntry(key, data) mediaFilterRepository.addSelectedUserMediaEntry(data) // Notify listeners listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } listeners.forEach { it.onMediaDataLoaded(data.instanceId) } } override fun onSmartspaceMediaDataLoaded( Loading Loading @@ -160,11 +158,11 @@ constructor( // It could happen there are existing active media resume cards, then we don't need to // reactivate. if (shouldReactivate) { val lastActiveKey = sorted.lastKey() // most recently active val lastActiveId = sorted.lastKey() // most recently active id // Notify listeners to consider this media active Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") mediaFilterRepository.setReactivatedKey(lastActiveKey) val mediaData = sorted[lastActiveKey]!!.copy(active = true) Log.d(TAG, "reactivating $lastActiveId instead of smartspace") mediaFilterRepository.setReactivatedId(lastActiveId) val mediaData = sorted[lastActiveId]!!.copy(active = true) logger.logRecommendationActivated( mediaData.appUid, mediaData.packageName, Loading @@ -172,9 +170,7 @@ constructor( ) listeners.forEach { it.onMediaDataLoaded( lastActiveKey, lastActiveKey, mediaData, lastActiveId, receivedSmartspaceCardLatency = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) .toInt(), Loading @@ -196,27 +192,28 @@ constructor( smartspaceMediaData.packageName, smartspaceMediaData.instanceId ) listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } listeners.forEach { it.onSmartspaceMediaDataLoaded(key, shouldPrioritizeMutable) } } override fun onMediaDataRemoved(key: String) { mediaFilterRepository.removeMediaEntry(key) mediaFilterRepository.removeSelectedUserMediaEntry(key)?.let { mediaFilterRepository.removeMediaEntry(key)?.let { mediaData -> val instanceId = mediaData.instanceId mediaFilterRepository.removeSelectedUserMediaEntry(instanceId)?.let { // Only notify listeners if something actually changed listeners.forEach { it.onMediaDataRemoved(key) } listeners.forEach { it.onMediaDataRemoved(instanceId) } } } } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { // First check if we had reactivated media instead of forwarding smartspace mediaFilterRepository.reactivatedKey.value?.let { val lastActiveKey = it mediaFilterRepository.setReactivatedKey(null) Log.d(TAG, "expiring reactivated key $lastActiveKey") mediaFilterRepository.reactivatedId.value?.let { lastActiveId -> mediaFilterRepository.setReactivatedId(null) Log.d(TAG, "expiring reactivated key $lastActiveId") // Notify listeners to update with actual active value mediaFilterRepository.selectedUserEntries.value[lastActiveKey]?.let { mediaData -> mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let { listeners.forEach { listener -> listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) listener.onMediaDataLoaded(lastActiveId, immediately) } } } Loading @@ -240,8 +237,8 @@ constructor( if (!lockscreenUserManager.isProfileAvailable(data.userId)) { // Only remove media when the profile is unavailable. if (DEBUG) Log.d(TAG, "Removing $key after profile change") mediaFilterRepository.removeSelectedUserMediaEntry(key, data) listeners.forEach { listener -> listener.onMediaDataRemoved(key) } mediaFilterRepository.removeSelectedUserMediaEntry(data.instanceId, data) listeners.forEach { listener -> listener.onMediaDataRemoved(data.instanceId) } } } } Loading @@ -254,16 +251,16 @@ constructor( // Clear the list first, to make sure callbacks from listeners if we have any entries // are up to date mediaFilterRepository.clearSelectedUserMedia() keyCopy.forEach { if (DEBUG) Log.d(TAG, "Removing $it after user change") listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } keyCopy.forEach { instanceId -> if (DEBUG) Log.d(TAG, "Removing $instanceId after user change") listenersCopy.forEach { listener -> listener.onMediaDataRemoved(instanceId) } } mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> if (lockscreenUserManager.isCurrentProfile(data.userId)) { if (DEBUG) Log.d(TAG, "Re-adding $key after user change") mediaFilterRepository.addSelectedUserMediaEntry(key, data) listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } mediaFilterRepository.addSelectedUserMediaEntry(data) listenersCopy.forEach { listener -> listener.onMediaDataLoaded(data.instanceId) } } } } Loading @@ -271,10 +268,12 @@ constructor( /** Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() { if (DEBUG) Log.d(TAG, "Media carousel swiped away") val mediaKeys = mediaFilterRepository.selectedUserEntries.value.keys.toSet() mediaKeys.forEach { val mediaEntries = mediaFilterRepository.allUserEntries.value.entries mediaEntries.forEach { (key, data) -> if (mediaFilterRepository.selectedUserEntries.value.containsKey(data.instanceId)) { // Force updates to listeners, needed for re-activated card mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) mediaDataManager.setInactive(key, timedOut = true, forceUpdate = true) } } val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value if (smartspaceMediaData.isActive) { Loading Loading @@ -312,10 +311,10 @@ constructor( } /** Add a listener for filtered [MediaData] changes */ fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) fun addListener(listener: Listener) = _listeners.add(listener) /** Remove a listener that was registered with addListener */ fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) fun removeListener(listener: Listener) = _listeners.remove(listener) /** * Return the time since last active for the most-recent media. Loading @@ -325,15 +324,57 @@ constructor( * the present. MAX_VALUE will be returned if there is no media. */ private fun timeSinceActiveForMostRecentMedia( sortedEntries: SortedMap<String, MediaData> sortedEntries: SortedMap<InstanceId, MediaData> ): Long { if (sortedEntries.isEmpty()) { return Long.MAX_VALUE } val now = systemClock.elapsedRealtime() val lastActiveKey = sortedEntries.lastKey() // most recently active return sortedEntries[lastActiveKey]?.let { now - it.lastActive } ?: Long.MAX_VALUE val lastActiveInstanceId = sortedEntries.lastKey() // most recently active return sortedEntries[lastActiveInstanceId]?.let { now - it.lastActive } ?: Long.MAX_VALUE } interface Listener { /** * Called whenever there's new MediaData Loaded for the consumption in views. * * @param immediately indicates should apply the UI changes immediately, otherwise wait * until the next refresh-round before UI becomes visible. True by default to take in * place immediately. * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace * signal. * @param isSsReactivated indicates resume media card is reactivated by Smartspace * recommendation signal */ fun onMediaDataLoaded( instanceId: InstanceId, immediately: Boolean = true, receivedSmartspaceCardLatency: Int = 0, isSsReactivated: Boolean = false, ) /** * Called whenever there's new Smartspace media data loaded. * * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true, * it will be prioritized as the first card. Otherwise, it will show up as the last card * as default. */ fun onSmartspaceMediaDataLoaded(key: String, shouldPrioritize: Boolean = false) /** Called whenever a previously existing Media notification was removed. */ fun onMediaDataRemoved(instanceId: InstanceId) /** * Called whenever a previously existing Smartspace media data was removed. * * @param immediately indicates should apply the UI changes immediately, otherwise wait * until the next refresh-round before UI becomes visible. True by default to take in * place immediately. */ fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) } companion object { Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt +2 −2 Original line number Diff line number Diff line Loading @@ -27,10 +27,10 @@ import com.android.systemui.media.controls.shared.model.SmartspaceMediaData interface MediaDataManager { /** Add a listener for changes in this class */ fun addListener(listener: Listener) fun addListener(listener: Listener) {} /** Remove a listener for changes in this class */ fun removeListener(listener: Listener) fun removeListener(listener: Listener) {} /** * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +18 −14 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.media.controls.data.repository import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope Loading @@ -44,16 +45,17 @@ class MediaFilterRepositoryTest : SysuiTestCase() { testScope.runTest { val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) val userMedia = MediaData().copy(active = true) val instanceId = InstanceId.fakeInstanceId(123) val userMedia = MediaData().copy(active = true, instanceId = instanceId) underTest.addSelectedUserMediaEntry(KEY, userMedia) underTest.addSelectedUserMediaEntry(userMedia) assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)).isEqualTo(userMedia) underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) underTest.addSelectedUserMediaEntry(userMedia.copy(active = false)) assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia) assertThat(selectedUserEntries?.get(KEY)?.active).isFalse() assertThat(selectedUserEntries?.get(instanceId)).isNotEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)?.active).isFalse() } @Test Loading @@ -61,13 +63,14 @@ class MediaFilterRepositoryTest : SysuiTestCase() { testScope.runTest { val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) val userMedia = MediaData() val instanceId = InstanceId.fakeInstanceId(123) val userMedia = MediaData().copy(instanceId = instanceId) underTest.addSelectedUserMediaEntry(KEY, userMedia) underTest.addSelectedUserMediaEntry(userMedia) assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)).isEqualTo(userMedia) assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() assertThat(underTest.removeSelectedUserMediaEntry(instanceId, userMedia)).isTrue() } @Test Loading @@ -75,13 +78,14 @@ class MediaFilterRepositoryTest : SysuiTestCase() { testScope.runTest { val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) val userMedia = MediaData() val instanceId = InstanceId.fakeInstanceId(123) val userMedia = MediaData().copy(instanceId = instanceId) underTest.addSelectedUserMediaEntry(KEY, userMedia) underTest.addSelectedUserMediaEntry(userMedia) assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) assertThat(selectedUserEntries?.get(instanceId)).isEqualTo(userMedia) assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia) assertThat(underTest.removeSelectedUserMediaEntry(instanceId)).isEqualTo(userMedia) } @Test Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +7 −6 Original line number Diff line number Diff line Loading @@ -56,13 +56,13 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val userMedia = MediaData().copy(active = true) mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasActiveMedia).isTrue() assertThat(hasAnyMedia).isTrue() mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) mediaFilterRepository.addSelectedUserMediaEntry(userMedia.copy(active = false)) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() Loading @@ -78,14 +78,16 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) val userMedia = MediaData().copy(active = false) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isTrue() assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() Loading Loading @@ -114,7 +116,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() Loading Loading @@ -193,7 +195,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } companion object { private const val KEY = "KEY" private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } }
packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt +16 −14 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.media.controls.data.repository import com.android.internal.logging.InstanceId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData Loading @@ -28,17 +29,18 @@ import kotlinx.coroutines.flow.asStateFlow @SysUISingleton class MediaFilterRepository @Inject constructor() { /** Key of media control that recommendations card reactivated. */ private val _reactivatedKey: MutableStateFlow<String?> = MutableStateFlow(null) val reactivatedKey: StateFlow<String?> = _reactivatedKey.asStateFlow() /** Instance id of media control that recommendations card reactivated. */ private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null) val reactivatedId: StateFlow<InstanceId?> = _reactivatedId.asStateFlow() private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> = MutableStateFlow(SmartspaceMediaData()) val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow() private val _selectedUserEntries: MutableStateFlow<Map<String, MediaData>> = private val _selectedUserEntries: MutableStateFlow<Map<InstanceId, MediaData>> = MutableStateFlow(LinkedHashMap()) val selectedUserEntries: StateFlow<Map<String, MediaData>> = _selectedUserEntries.asStateFlow() val selectedUserEntries: StateFlow<Map<InstanceId, MediaData>> = _selectedUserEntries.asStateFlow() private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> = MutableStateFlow(LinkedHashMap()) Loading @@ -62,9 +64,9 @@ class MediaFilterRepository @Inject constructor() { return mediaData } fun addSelectedUserMediaEntry(key: String, data: MediaData) { val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) entries[key] = data fun addSelectedUserMediaEntry(data: MediaData) { val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value) entries[data.instanceId] = data _selectedUserEntries.value = entries } Loading @@ -73,8 +75,8 @@ class MediaFilterRepository @Inject constructor() { * * @return media data if an entry is actually removed, `null` otherwise. */ fun removeSelectedUserMediaEntry(key: String): MediaData? { val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) fun removeSelectedUserMediaEntry(key: InstanceId): MediaData? { val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value) val mediaData = entries.remove(key) _selectedUserEntries.value = entries return mediaData Loading @@ -85,8 +87,8 @@ class MediaFilterRepository @Inject constructor() { * * @return true if media data is removed, false otherwise. */ fun removeSelectedUserMediaEntry(key: String, data: MediaData): Boolean { val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) fun removeSelectedUserMediaEntry(key: InstanceId, data: MediaData): Boolean { val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value) val succeed = entries.remove(key, data) if (!succeed) { return false Loading @@ -105,7 +107,7 @@ class MediaFilterRepository @Inject constructor() { } /** Updates media control key that recommendations card reactivated. */ fun setReactivatedKey(key: String?) { _reactivatedKey.value = key fun setReactivatedId(instanceId: InstanceId?) { _reactivatedId.value = instanceId } }
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +82 −41 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.os.SystemProperties import android.util.Log import com.android.internal.annotations.KeepForWeakReference import com.android.internal.annotations.VisibleForTesting import com.android.internal.logging.InstanceId import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.data.repository.MediaFilterRepository Loading Loading @@ -66,8 +67,8 @@ constructor( private val mediaFlags: MediaFlags, private val mediaFilterRepository: MediaFilterRepository, ) : MediaDataManager.Listener { private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() val listeners: Set<MediaDataManager.Listener> private val _listeners: MutableSet<Listener> = mutableSetOf() val listeners: Set<Listener> get() = _listeners.toSet() lateinit var mediaDataManager: MediaDataManager Loading Loading @@ -108,13 +109,10 @@ constructor( return } if (oldKey != null && oldKey != key) { mediaFilterRepository.removeSelectedUserMediaEntry(oldKey) } mediaFilterRepository.addSelectedUserMediaEntry(key, data) mediaFilterRepository.addSelectedUserMediaEntry(data) // Notify listeners listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } listeners.forEach { it.onMediaDataLoaded(data.instanceId) } } override fun onSmartspaceMediaDataLoaded( Loading Loading @@ -160,11 +158,11 @@ constructor( // It could happen there are existing active media resume cards, then we don't need to // reactivate. if (shouldReactivate) { val lastActiveKey = sorted.lastKey() // most recently active val lastActiveId = sorted.lastKey() // most recently active id // Notify listeners to consider this media active Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") mediaFilterRepository.setReactivatedKey(lastActiveKey) val mediaData = sorted[lastActiveKey]!!.copy(active = true) Log.d(TAG, "reactivating $lastActiveId instead of smartspace") mediaFilterRepository.setReactivatedId(lastActiveId) val mediaData = sorted[lastActiveId]!!.copy(active = true) logger.logRecommendationActivated( mediaData.appUid, mediaData.packageName, Loading @@ -172,9 +170,7 @@ constructor( ) listeners.forEach { it.onMediaDataLoaded( lastActiveKey, lastActiveKey, mediaData, lastActiveId, receivedSmartspaceCardLatency = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) .toInt(), Loading @@ -196,27 +192,28 @@ constructor( smartspaceMediaData.packageName, smartspaceMediaData.instanceId ) listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } listeners.forEach { it.onSmartspaceMediaDataLoaded(key, shouldPrioritizeMutable) } } override fun onMediaDataRemoved(key: String) { mediaFilterRepository.removeMediaEntry(key) mediaFilterRepository.removeSelectedUserMediaEntry(key)?.let { mediaFilterRepository.removeMediaEntry(key)?.let { mediaData -> val instanceId = mediaData.instanceId mediaFilterRepository.removeSelectedUserMediaEntry(instanceId)?.let { // Only notify listeners if something actually changed listeners.forEach { it.onMediaDataRemoved(key) } listeners.forEach { it.onMediaDataRemoved(instanceId) } } } } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { // First check if we had reactivated media instead of forwarding smartspace mediaFilterRepository.reactivatedKey.value?.let { val lastActiveKey = it mediaFilterRepository.setReactivatedKey(null) Log.d(TAG, "expiring reactivated key $lastActiveKey") mediaFilterRepository.reactivatedId.value?.let { lastActiveId -> mediaFilterRepository.setReactivatedId(null) Log.d(TAG, "expiring reactivated key $lastActiveId") // Notify listeners to update with actual active value mediaFilterRepository.selectedUserEntries.value[lastActiveKey]?.let { mediaData -> mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let { listeners.forEach { listener -> listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) listener.onMediaDataLoaded(lastActiveId, immediately) } } } Loading @@ -240,8 +237,8 @@ constructor( if (!lockscreenUserManager.isProfileAvailable(data.userId)) { // Only remove media when the profile is unavailable. if (DEBUG) Log.d(TAG, "Removing $key after profile change") mediaFilterRepository.removeSelectedUserMediaEntry(key, data) listeners.forEach { listener -> listener.onMediaDataRemoved(key) } mediaFilterRepository.removeSelectedUserMediaEntry(data.instanceId, data) listeners.forEach { listener -> listener.onMediaDataRemoved(data.instanceId) } } } } Loading @@ -254,16 +251,16 @@ constructor( // Clear the list first, to make sure callbacks from listeners if we have any entries // are up to date mediaFilterRepository.clearSelectedUserMedia() keyCopy.forEach { if (DEBUG) Log.d(TAG, "Removing $it after user change") listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } keyCopy.forEach { instanceId -> if (DEBUG) Log.d(TAG, "Removing $instanceId after user change") listenersCopy.forEach { listener -> listener.onMediaDataRemoved(instanceId) } } mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> if (lockscreenUserManager.isCurrentProfile(data.userId)) { if (DEBUG) Log.d(TAG, "Re-adding $key after user change") mediaFilterRepository.addSelectedUserMediaEntry(key, data) listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } mediaFilterRepository.addSelectedUserMediaEntry(data) listenersCopy.forEach { listener -> listener.onMediaDataLoaded(data.instanceId) } } } } Loading @@ -271,10 +268,12 @@ constructor( /** Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() { if (DEBUG) Log.d(TAG, "Media carousel swiped away") val mediaKeys = mediaFilterRepository.selectedUserEntries.value.keys.toSet() mediaKeys.forEach { val mediaEntries = mediaFilterRepository.allUserEntries.value.entries mediaEntries.forEach { (key, data) -> if (mediaFilterRepository.selectedUserEntries.value.containsKey(data.instanceId)) { // Force updates to listeners, needed for re-activated card mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) mediaDataManager.setInactive(key, timedOut = true, forceUpdate = true) } } val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value if (smartspaceMediaData.isActive) { Loading Loading @@ -312,10 +311,10 @@ constructor( } /** Add a listener for filtered [MediaData] changes */ fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) fun addListener(listener: Listener) = _listeners.add(listener) /** Remove a listener that was registered with addListener */ fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) fun removeListener(listener: Listener) = _listeners.remove(listener) /** * Return the time since last active for the most-recent media. Loading @@ -325,15 +324,57 @@ constructor( * the present. MAX_VALUE will be returned if there is no media. */ private fun timeSinceActiveForMostRecentMedia( sortedEntries: SortedMap<String, MediaData> sortedEntries: SortedMap<InstanceId, MediaData> ): Long { if (sortedEntries.isEmpty()) { return Long.MAX_VALUE } val now = systemClock.elapsedRealtime() val lastActiveKey = sortedEntries.lastKey() // most recently active return sortedEntries[lastActiveKey]?.let { now - it.lastActive } ?: Long.MAX_VALUE val lastActiveInstanceId = sortedEntries.lastKey() // most recently active return sortedEntries[lastActiveInstanceId]?.let { now - it.lastActive } ?: Long.MAX_VALUE } interface Listener { /** * Called whenever there's new MediaData Loaded for the consumption in views. * * @param immediately indicates should apply the UI changes immediately, otherwise wait * until the next refresh-round before UI becomes visible. True by default to take in * place immediately. * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace * signal. * @param isSsReactivated indicates resume media card is reactivated by Smartspace * recommendation signal */ fun onMediaDataLoaded( instanceId: InstanceId, immediately: Boolean = true, receivedSmartspaceCardLatency: Int = 0, isSsReactivated: Boolean = false, ) /** * Called whenever there's new Smartspace media data loaded. * * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true, * it will be prioritized as the first card. Otherwise, it will show up as the last card * as default. */ fun onSmartspaceMediaDataLoaded(key: String, shouldPrioritize: Boolean = false) /** Called whenever a previously existing Media notification was removed. */ fun onMediaDataRemoved(instanceId: InstanceId) /** * Called whenever a previously existing Smartspace media data was removed. * * @param immediately indicates should apply the UI changes immediately, otherwise wait * until the next refresh-round before UI becomes visible. True by default to take in * place immediately. */ fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) } companion object { Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt +2 −2 Original line number Diff line number Diff line Loading @@ -27,10 +27,10 @@ import com.android.systemui.media.controls.shared.model.SmartspaceMediaData interface MediaDataManager { /** Add a listener for changes in this class */ fun addListener(listener: Listener) fun addListener(listener: Listener) {} /** Remove a listener for changes in this class */ fun removeListener(listener: Listener) fun removeListener(listener: Listener) {} /** * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This Loading