diff options
3 files changed, 169 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt index 5bdc67fd6e86..5432a189cf7c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt @@ -69,6 +69,10 @@ constructor( private val mediaFlags: MediaFlags, private val mediaFilterRepository: MediaFilterRepository, ) : MediaDataManager.Listener { + /** Non-UI listeners to media changes. */ + private val _listeners: MutableSet<MediaDataProcessor.Listener> = mutableSetOf() + val listeners: Set<MediaDataProcessor.Listener> + get() = _listeners.toSet() lateinit var mediaDataProcessor: MediaDataProcessor // Ensure the field (and associated reference) isn't removed during optimization. @@ -113,6 +117,9 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(data.instanceId) ) + + // Notify listeners + listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } } override fun onSmartspaceMediaDataLoaded( @@ -171,6 +178,20 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(lastActiveId) ) + listeners.forEach { listener -> + getKey(lastActiveId)?.let { lastActiveKey -> + listener.onMediaDataLoaded( + lastActiveKey, + lastActiveKey, + mediaData, + receivedSmartspaceCardLatency = + (systemClock.currentTimeMillis() - + data.headphoneConnectionTimeMillis) + .toInt(), + isSsReactivated = true + ) + } + } } } else if (data.isActive) { // Mark to prioritize Smartspace card if no recent media. @@ -189,6 +210,7 @@ constructor( mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable) ) + listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } } override fun onMediaDataRemoved(key: String) { @@ -198,6 +220,8 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(instanceId) ) + // Only notify listeners if something actually changed + listeners.forEach { it.onMediaDataRemoved(key) } } } } @@ -212,6 +236,11 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(lastActiveId, immediately) ) + listeners.forEach { listener -> + getKey(lastActiveId)?.let { lastActiveKey -> + listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, it, immediately) + } + } } } @@ -227,6 +256,7 @@ constructor( mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Removed(key, immediately) ) + listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } @VisibleForTesting @@ -240,6 +270,7 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(data.instanceId) ) + listeners.forEach { listener -> listener.onMediaDataRemoved(key) } } } } @@ -247,6 +278,7 @@ constructor( @VisibleForTesting internal fun handleUserSwitched() { // If the user changes, remove all current MediaData objects. + val listenersCopy = listeners val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList() // Clear the list first and update loading state to remove media from UI. mediaFilterRepository.clearSelectedUserMedia() @@ -255,6 +287,9 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(instanceId) ) + getKey(instanceId)?.let { + listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } + } } mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> @@ -268,6 +303,7 @@ constructor( mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(data.instanceId) ) + listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } } } } @@ -317,6 +353,12 @@ constructor( } } + /** Add a listener for filtered [MediaData] changes */ + fun addListener(listener: MediaDataProcessor.Listener) = _listeners.add(listener) + + /** Remove a listener that was registered with addListener */ + fun removeListener(listener: MediaDataProcessor.Listener) = _listeners.remove(listener) + /** * Return the time since last active for the most-recent media. * @@ -336,6 +378,16 @@ constructor( return sortedEntries[lastActiveInstanceId]?.let { now - it.lastActive } ?: Long.MAX_VALUE } + private fun getKey(instanceId: InstanceId): String? { + val allEntries = mediaFilterRepository.allUserEntries.value + val filteredEntries = allEntries.filter { (_, data) -> data.instanceId == instanceId } + return if (filteredEntries.isNotEmpty()) { + filteredEntries.keys.first() + } else { + null + } + } + companion object { /** * Maximum age of a media control to re-activate on smartspace signal. If there is no media diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt index 33d407c00b7a..b04e93835418 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -160,6 +160,14 @@ constructor( mediaDataFilter.mediaDataProcessor = mediaDataProcessor } + override fun addListener(listener: MediaDataManager.Listener) { + mediaDataFilter.addListener(listener) + } + + override fun removeListener(listener: MediaDataManager.Listener) { + mediaDataFilter.removeListener(listener) + } + override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) = unsupported override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt index 31658e8592ad..8f73811199ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock @@ -76,6 +77,7 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! @TestableLooper.RunWithLooper class MediaDataFilterImplTest : SysuiTestCase() { + @Mock private lateinit var listener: MediaDataProcessor.Listener @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var broadcastSender: BroadcastSender @Mock private lateinit var mediaDataProcessor: MediaDataProcessor @@ -115,6 +117,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { repository, ) mediaDataFilter.mediaDataProcessor = mediaDataProcessor + mediaDataFilter.addListener(listener) // Start all tests as main user setUser(USER_MAIN) @@ -167,6 +170,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false)) assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) } @@ -178,6 +183,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) assertThat(mediaDataLoadedStates).isNotEqualTo(mediaLoadedStatesModel) } @@ -196,6 +203,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaLoadedStatesModel.remove(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataRemoved(KEY) + verify(listener).onMediaDataRemoved(eq(KEY)) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) } @@ -208,6 +216,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) mediaDataFilter.onMediaDataRemoved(KEY) + verify(listener, never()).onMediaDataRemoved(eq(KEY)) assertThat(mediaDataLoadedStates).isEmpty() } @@ -226,6 +235,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { setUser(USER_GUEST) // THEN we should remove the main user's media + verify(listener).onMediaDataRemoved(eq(KEY)) assertThat(mediaDataLoadedStates).isEmpty() } @@ -243,6 +253,20 @@ class MediaDataFilterImplTest : SysuiTestCase() { // and we switch to guest user setUser(USER_GUEST) + // THEN we should add back the guest user media + verify(listener) + .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false)) + + // but not the main user's + verify(listener, never()) + .onMediaDataLoaded( + eq(KEY), + any(), + eq(dataMain), + anyBoolean(), + anyInt(), + anyBoolean() + ) assertThat(mediaDataLoadedStates).isEqualTo(guestLoadedStatesModel) assertThat(mediaDataLoadedStates).isNotEqualTo(mainLoadedStatesModel) } @@ -261,6 +285,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) // THEN we should remove the private profile media + verify(listener).onMediaDataRemoved(eq(KEY_ALT)) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) } @@ -507,6 +532,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isTrue() assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger, never()).logRecommendationActivated(any(), any(), any()) } @@ -534,6 +561,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isFalse() assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(logger, never()).logRecommendationAdded(any(), any()) verify(logger, never()).logRecommendationActivated(any(), any(), any()) } @@ -563,6 +593,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isTrue() assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger, never()).logRecommendationActivated(any(), any(), any()) } @@ -592,6 +624,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isFalse() assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(logger, never()).logRecommendationAdded(any(), any()) verify(logger, never()).logRecommendationActivated(any(), any(), any()) } @@ -614,6 +647,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) @@ -629,6 +664,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isFalse() assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(logger, never()).logRecommendationAdded(any(), any()) verify(logger, never()).logRecommendationActivated(any(), any(), any()) } @@ -649,12 +687,15 @@ class MediaDataFilterImplTest : SysuiTestCase() { val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal runCurrent() mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) assertThat( hasActiveMediaOrRecommendation( @@ -664,8 +705,18 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) ) .isTrue() + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) // Smartspace update shouldn't be propagated for the empty rec list. assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(logger, never()).logRecommendationAdded(any(), any()) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) } @@ -687,12 +738,24 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal runCurrent() mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) assertThat( hasActiveMediaOrRecommendation( @@ -704,6 +767,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { .isTrue() // Smartspace update should also be propagated but not prioritized. assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) } @@ -721,6 +786,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( @@ -746,15 +812,29 @@ class MediaDataFilterImplTest : SysuiTestCase() { val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) runCurrent() mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( @@ -781,6 +861,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( @@ -813,12 +895,18 @@ class MediaDataFilterImplTest : SysuiTestCase() { val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) // And an inactive recommendation is loaded mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // Smartspace is loaded but the media stays inactive + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( @@ -866,6 +954,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) // AND we get a smartspace signal with extra to trigger resume @@ -875,6 +965,16 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) assertThat( hasActiveMediaOrRecommendation( @@ -886,6 +986,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { .isTrue() // And update the smartspace data state, but not prioritized assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) } @Test @@ -901,6 +1003,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) // AND we get a smartspace signal with extra to not trigger resume @@ -908,7 +1012,12 @@ class MediaDataFilterImplTest : SysuiTestCase() { whenever(cardAction.extras).thenReturn(extras) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + // THEN listeners are not updated to show media + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true)) // But the smartspace update is still propagated + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) } |