Loading packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +76 −54 Original line number Original line Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import android.view.View import android.view.ViewGroup import android.view.ViewGroup import android.widget.LinearLayout import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter Loading @@ -22,6 +23,7 @@ import com.android.systemui.util.Utils import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.DelayableExecutor import java.util.TreeMap import javax.inject.Inject import javax.inject.Inject import javax.inject.Provider import javax.inject.Provider import javax.inject.Singleton import javax.inject.Singleton Loading Loading @@ -103,9 +105,7 @@ class MediaCarouselController @Inject constructor( private val mediaCarousel: MediaScrollView private val mediaCarousel: MediaScrollView private val mediaCarouselScrollHandler: MediaCarouselScrollHandler private val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup val mediaFrame: ViewGroup val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() private lateinit var settingsButton: View private lateinit var settingsButton: View private val mediaData: MutableMap<String, MediaData> = mutableMapOf() private val mediaContent: ViewGroup private val mediaContent: ViewGroup private val pageIndicator: PageIndicator private val pageIndicator: PageIndicator private val visualStabilityCallback: VisualStabilityManager.Callback private val visualStabilityCallback: VisualStabilityManager.Callback Loading @@ -123,7 +123,7 @@ class MediaCarouselController @Inject constructor( set(value) { set(value) { if (field != value) { if (field != value) { field = value field = value for (player in mediaPlayers.values) { for (player in MediaPlayerData.players()) { player.setListening(field) player.setListening(field) } } } } Loading Loading @@ -168,20 +168,17 @@ class MediaCarouselController @Inject constructor( true /* persistent */) true /* persistent */) mediaManager.addListener(object : MediaDataManager.Listener { mediaManager.addListener(object : MediaDataManager.Listener { override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { oldKey?.let { mediaData.remove(it) } if (!data.active && !Utils.useMediaResumption(context)) { if (!data.active && !Utils.useMediaResumption(context)) { // This view is inactive, let's remove this! This happens e.g when dismissing / // This view is inactive, let's remove this! This happens e.g when dismissing / // timing out a view. We still have the data around because resumption could // timing out a view. We still have the data around because resumption could // be on, but we should save the resources and release this. // be on, but we should save the resources and release this. onMediaDataRemoved(key) onMediaDataRemoved(key) } else { } else { mediaData.put(key, data) addOrUpdatePlayer(key, oldKey, data) addOrUpdatePlayer(key, oldKey, data) } } } } override fun onMediaDataRemoved(key: String) { override fun onMediaDataRemoved(key: String) { mediaData.remove(key) removePlayer(key) removePlayer(key) } } }) }) Loading Loading @@ -224,67 +221,50 @@ class MediaCarouselController @Inject constructor( } } private fun reorderAllPlayers() { private fun reorderAllPlayers() { for (mediaPlayer in mediaPlayers.values) { mediaContent.removeAllViews() val view = mediaPlayer.view?.player for (mediaPlayer in MediaPlayerData.players()) { if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { mediaPlayer.view?.let { mediaContent.removeView(view) mediaContent.addView(it.player) mediaContent.addView(view, 0) } } } } mediaCarouselScrollHandler.onPlayersChanged() mediaCarouselScrollHandler.onPlayersChanged() } } private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { // If the key was changed, update entry val existingPlayer = MediaPlayerData.getMediaPlayer(key, oldKey) val oldData = mediaPlayers[oldKey] if (oldData != null) { val oldData = mediaPlayers.remove(oldKey) mediaPlayers.put(key, oldData!!)?.let { Log.wtf(TAG, "new key $key already exists when migrating from $oldKey") } } var existingPlayer = mediaPlayers[key] if (existingPlayer == null) { if (existingPlayer == null) { existingPlayer = mediaControlPanelFactory.get() var newPlayer = mediaControlPanelFactory.get() existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), newPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent)) mediaContent)) newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions MediaPlayerData.addMediaPlayer(key, data, newPlayer) mediaPlayers[key] = existingPlayer val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT) existingPlayer.view?.player?.setLayoutParams(lp) newPlayer.view?.player?.setLayoutParams(lp) existingPlayer.bind(data) newPlayer.bind(data) existingPlayer.setListening(currentlyExpanded) newPlayer.setListening(currentlyExpanded) updatePlayerToState(existingPlayer, noAnimation = true) updatePlayerToState(newPlayer, noAnimation = true) if (existingPlayer.isPlaying) { reorderAllPlayers() mediaContent.addView(existingPlayer.view?.player, 0) } else { mediaContent.addView(existingPlayer.view?.player) } } else { } else { existingPlayer.bind(data) existingPlayer.bind(data) if (existingPlayer.isPlaying && MediaPlayerData.addMediaPlayer(key, data, existingPlayer) mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { if (visualStabilityManager.isReorderingAllowed) { if (visualStabilityManager.isReorderingAllowed) { mediaContent.removeView(existingPlayer.view?.player) reorderAllPlayers() mediaContent.addView(existingPlayer.view?.player, 0) } else { } else { needsReordering = true needsReordering = true } } } } } updatePageIndicator() updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() mediaCarouselScrollHandler.onPlayersChanged() mediaCarousel.requiresRemeasuring = true mediaCarousel.requiresRemeasuring = true // Check postcondition: mediaContent should have the same number of children as there are // Check postcondition: mediaContent should have the same number of children as there are // elements in mediaPlayers. // elements in mediaPlayers. if (mediaPlayers.size != mediaContent.childCount) { if (MediaPlayerData.players().size != mediaContent.childCount) { Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") } } } } private fun removePlayer(key: String) { private fun removePlayer(key: String) { val removed = mediaPlayers.remove(key) val removed = MediaPlayerData.removeMediaPlayer(key) removed?.apply { removed?.apply { mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaContent.removeView(removed.view?.player) mediaContent.removeView(removed.view?.player) Loading @@ -295,12 +275,7 @@ class MediaCarouselController @Inject constructor( } } private fun recreatePlayers() { private fun recreatePlayers() { // Note that this will scramble the order of players. Actively playing sessions will, at MediaPlayerData.mediaData().forEach { (key, data) -> // least, still be put in the front. If we want to maintain order, then more work is // needed. mediaData.forEach { key, data -> removePlayer(key) addOrUpdatePlayer(key = key, oldKey = null, data = data) addOrUpdatePlayer(key = key, oldKey = null, data = data) } } } } Loading Loading @@ -338,7 +313,7 @@ class MediaCarouselController @Inject constructor( currentStartLocation = startLocation currentStartLocation = startLocation currentEndLocation = endLocation currentEndLocation = endLocation currentTransitionProgress = progress currentTransitionProgress = progress for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in MediaPlayerData.players()) { updatePlayerToState(mediaPlayer, immediately) updatePlayerToState(mediaPlayer, immediately) } } maybeResetSettingsCog() maybeResetSettingsCog() Loading Loading @@ -387,7 +362,7 @@ class MediaCarouselController @Inject constructor( private fun updateCarouselDimensions() { private fun updateCarouselDimensions() { var width = 0 var width = 0 var height = 0 var height = 0 for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in MediaPlayerData.players()) { val controller = mediaPlayer.mediaViewController val controller = mediaPlayer.mediaViewController // When transitioning the view to gone, the view gets smaller, but the translation // When transitioning the view to gone, the view gets smaller, but the translation // Doesn't, let's add the translation // Doesn't, let's add the translation Loading Loading @@ -449,7 +424,7 @@ class MediaCarouselController @Inject constructor( this.desiredLocation = desiredLocation this.desiredLocation = desiredLocation this.desiredHostState = it this.desiredHostState = it currentlyExpanded = it.expansion > 0 currentlyExpanded = it.expansion > 0 for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in MediaPlayerData.players()) { if (animate) { if (animate) { mediaPlayer.mediaViewController.animatePendingStateChange( mediaPlayer.mediaViewController.animatePendingStateChange( duration = duration, duration = duration, Loading @@ -471,7 +446,7 @@ class MediaCarouselController @Inject constructor( } } fun closeGuts() { fun closeGuts() { mediaPlayers.values.forEach { MediaPlayerData.players().forEach { it.closeGuts(true) it.closeGuts(true) } } } } Loading @@ -498,3 +473,50 @@ class MediaCarouselController @Inject constructor( } } } } } } @VisibleForTesting internal object MediaPlayerData { private data class MediaSortKey( val data: MediaData, val updateTime: Long = 0, val isPlaying: Boolean = false ) private val comparator = compareByDescending<MediaSortKey> { it.isPlaying } .thenByDescending { it.data.isLocalSession } .thenByDescending { !it.data.resumption } .thenByDescending { it.updateTime } private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel) { removeMediaPlayer(key) val sortKey = MediaSortKey(data, System.currentTimeMillis(), player.isPlaying()) mediaData.put(key, sortKey) mediaPlayers.put(sortKey, player) } fun getMediaPlayer(key: String, oldKey: String?): MediaControlPanel? { // If the key was changed, update entry oldKey?.let { if (it != key) { mediaData.remove(it)?.let { sortKey -> mediaData.put(key, sortKey) } } } return mediaData.get(key)?.let { mediaPlayers.get(it) } } fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { mediaPlayers.remove(it) } fun mediaData() = mediaData.entries.map { e -> Pair(e.key, e.value.data) } fun players() = mediaPlayers.values @VisibleForTesting fun clear() { mediaData.clear() mediaPlayers.clear() } } packages/SystemUI/src/com/android/systemui/media/MediaData.kt +4 −0 Original line number Original line Diff line number Diff line Loading @@ -81,6 +81,10 @@ data class MediaData( * Action that should be performed to restart a non active session. * Action that should be performed to restart a non active session. */ */ var resumeAction: Runnable?, var resumeAction: Runnable?, /** * Local or remote playback */ var isLocalSession: Boolean = true, /** /** * Indicates that this player is a resumption player (ie. It only shows a play actions which * Indicates that this player is a resumption player (ie. It only shows a play actions which * will start the app and start playing). * will start the app and start playing). Loading packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +8 −3 Original line number Original line Diff line number Diff line Loading @@ -31,6 +31,7 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.graphics.drawable.Icon import android.media.MediaDescription import android.media.MediaDescription import android.media.MediaMetadata import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.MediaSession import android.media.session.MediaSession import android.net.Uri import android.net.Uri import android.os.UserHandle import android.os.UserHandle Loading Loading @@ -337,7 +338,8 @@ class MediaDataManager( ) { ) { val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) as MediaSession.Token? as MediaSession.Token? val metadata = mediaControllerFactory.create(token).metadata val mediaController = mediaControllerFactory.create(token) val metadata = mediaController.metadata // Foreground and Background colors computed from album art // Foreground and Background colors computed from album art val notif: Notification = sbn.notification val notif: Notification = sbn.notification Loading Loading @@ -429,6 +431,9 @@ class MediaDataManager( } } } } val isLocalSession = mediaController.playbackInfo?.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ?: true foregroundExecutor.execute { foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true Loading @@ -436,8 +441,8 @@ class MediaDataManager( onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, smallIconDrawable, artist, song, artWorkIcon, actionIcons, smallIconDrawable, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, active, resumeAction = resumeAction, notificationKey = key, active, resumeAction = resumeAction, isLocalSession = isLocalSession, hasCheckedForResume = hasCheckedForResume)) notificationKey = key, hasCheckedForResume = hasCheckedForResume)) } } } } Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +2 −2 Original line number Original line Diff line number Diff line Loading @@ -83,8 +83,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mManager.addListener(mListener); mManager.addListener(mListener); mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, false, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true, KEY, false); false, KEY, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); } } Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt 0 → 100644 +122 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.media import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) public class MediaPlayerDataTest : SysuiTestCase() { companion object { val LOCAL = true val RESUMPTION = true } @Before fun setup() { MediaPlayerData.clear() } @Test fun addPlayingThenRemote() { val playerIsPlaying = mock(MediaControlPanel::class.java) whenever(playerIsPlaying.isPlaying).thenReturn(true) val dataIsPlaying = createMediaData(LOCAL, !RESUMPTION) val playerIsRemote = mock(MediaControlPanel::class.java) whenever(playerIsRemote.isPlaying).thenReturn(false) val dataIsRemote = createMediaData(!LOCAL, !RESUMPTION) MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying) MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote) val players = MediaPlayerData.players() assertThat(players).hasSize(2) assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder() } @Test fun switchPlayersPlaying() { val playerIsPlaying1 = mock(MediaControlPanel::class.java) whenever(playerIsPlaying1.isPlaying).thenReturn(true) val dataIsPlaying1 = createMediaData(LOCAL, !RESUMPTION) val playerIsPlaying2 = mock(MediaControlPanel::class.java) whenever(playerIsPlaying2.isPlaying).thenReturn(false) val dataIsPlaying2 = createMediaData(LOCAL, !RESUMPTION) MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1) MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2) whenever(playerIsPlaying1.isPlaying).thenReturn(false) whenever(playerIsPlaying2.isPlaying).thenReturn(true) MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1) MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2) val players = MediaPlayerData.players() assertThat(players).hasSize(2) assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder() } @Test fun fullOrderTest() { val playerIsPlaying = mock(MediaControlPanel::class.java) whenever(playerIsPlaying.isPlaying).thenReturn(true) val dataIsPlaying = createMediaData(LOCAL, !RESUMPTION) val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java) whenever(playerIsPlayingAndRemote.isPlaying).thenReturn(true) val dataIsPlayingAndRemote = createMediaData(!LOCAL, !RESUMPTION) val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java) whenever(playerIsStoppedAndLocal.isPlaying).thenReturn(false) val dataIsStoppedAndLocal = createMediaData(LOCAL, !RESUMPTION) val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java) whenever(playerIsStoppedAndLocal.isPlaying).thenReturn(false) val dataIsStoppedAndRemote = createMediaData(!LOCAL, !RESUMPTION) val playerCanResume = mock(MediaControlPanel::class.java) whenever(playerCanResume.isPlaying).thenReturn(false) val dataCanResume = createMediaData(LOCAL, RESUMPTION) MediaPlayerData.addMediaPlayer("3", dataIsStoppedAndLocal, playerIsStoppedAndLocal) MediaPlayerData.addMediaPlayer("5", dataIsStoppedAndRemote, playerIsStoppedAndRemote) MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume) MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying) MediaPlayerData.addMediaPlayer("2", dataIsPlayingAndRemote, playerIsPlayingAndRemote) val players = MediaPlayerData.players() assertThat(players).hasSize(5) assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote, playerIsStoppedAndLocal, playerCanResume, playerIsStoppedAndRemote).inOrder() } private fun createMediaData(isLocalSession: Boolean, resumption: Boolean) = MediaData(0, false, 0, null, null, null, null, null, emptyList(), emptyList<Int>(), "", null, null, null, true, null, isLocalSession, resumption, null, false) } Loading
packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +76 −54 Original line number Original line Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import android.view.View import android.view.ViewGroup import android.view.ViewGroup import android.widget.LinearLayout import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter Loading @@ -22,6 +23,7 @@ import com.android.systemui.util.Utils import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.DelayableExecutor import java.util.TreeMap import javax.inject.Inject import javax.inject.Inject import javax.inject.Provider import javax.inject.Provider import javax.inject.Singleton import javax.inject.Singleton Loading Loading @@ -103,9 +105,7 @@ class MediaCarouselController @Inject constructor( private val mediaCarousel: MediaScrollView private val mediaCarousel: MediaScrollView private val mediaCarouselScrollHandler: MediaCarouselScrollHandler private val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup val mediaFrame: ViewGroup val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() private lateinit var settingsButton: View private lateinit var settingsButton: View private val mediaData: MutableMap<String, MediaData> = mutableMapOf() private val mediaContent: ViewGroup private val mediaContent: ViewGroup private val pageIndicator: PageIndicator private val pageIndicator: PageIndicator private val visualStabilityCallback: VisualStabilityManager.Callback private val visualStabilityCallback: VisualStabilityManager.Callback Loading @@ -123,7 +123,7 @@ class MediaCarouselController @Inject constructor( set(value) { set(value) { if (field != value) { if (field != value) { field = value field = value for (player in mediaPlayers.values) { for (player in MediaPlayerData.players()) { player.setListening(field) player.setListening(field) } } } } Loading Loading @@ -168,20 +168,17 @@ class MediaCarouselController @Inject constructor( true /* persistent */) true /* persistent */) mediaManager.addListener(object : MediaDataManager.Listener { mediaManager.addListener(object : MediaDataManager.Listener { override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { oldKey?.let { mediaData.remove(it) } if (!data.active && !Utils.useMediaResumption(context)) { if (!data.active && !Utils.useMediaResumption(context)) { // This view is inactive, let's remove this! This happens e.g when dismissing / // This view is inactive, let's remove this! This happens e.g when dismissing / // timing out a view. We still have the data around because resumption could // timing out a view. We still have the data around because resumption could // be on, but we should save the resources and release this. // be on, but we should save the resources and release this. onMediaDataRemoved(key) onMediaDataRemoved(key) } else { } else { mediaData.put(key, data) addOrUpdatePlayer(key, oldKey, data) addOrUpdatePlayer(key, oldKey, data) } } } } override fun onMediaDataRemoved(key: String) { override fun onMediaDataRemoved(key: String) { mediaData.remove(key) removePlayer(key) removePlayer(key) } } }) }) Loading Loading @@ -224,67 +221,50 @@ class MediaCarouselController @Inject constructor( } } private fun reorderAllPlayers() { private fun reorderAllPlayers() { for (mediaPlayer in mediaPlayers.values) { mediaContent.removeAllViews() val view = mediaPlayer.view?.player for (mediaPlayer in MediaPlayerData.players()) { if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { mediaPlayer.view?.let { mediaContent.removeView(view) mediaContent.addView(it.player) mediaContent.addView(view, 0) } } } } mediaCarouselScrollHandler.onPlayersChanged() mediaCarouselScrollHandler.onPlayersChanged() } } private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { // If the key was changed, update entry val existingPlayer = MediaPlayerData.getMediaPlayer(key, oldKey) val oldData = mediaPlayers[oldKey] if (oldData != null) { val oldData = mediaPlayers.remove(oldKey) mediaPlayers.put(key, oldData!!)?.let { Log.wtf(TAG, "new key $key already exists when migrating from $oldKey") } } var existingPlayer = mediaPlayers[key] if (existingPlayer == null) { if (existingPlayer == null) { existingPlayer = mediaControlPanelFactory.get() var newPlayer = mediaControlPanelFactory.get() existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), newPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent)) mediaContent)) newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions MediaPlayerData.addMediaPlayer(key, data, newPlayer) mediaPlayers[key] = existingPlayer val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT) existingPlayer.view?.player?.setLayoutParams(lp) newPlayer.view?.player?.setLayoutParams(lp) existingPlayer.bind(data) newPlayer.bind(data) existingPlayer.setListening(currentlyExpanded) newPlayer.setListening(currentlyExpanded) updatePlayerToState(existingPlayer, noAnimation = true) updatePlayerToState(newPlayer, noAnimation = true) if (existingPlayer.isPlaying) { reorderAllPlayers() mediaContent.addView(existingPlayer.view?.player, 0) } else { mediaContent.addView(existingPlayer.view?.player) } } else { } else { existingPlayer.bind(data) existingPlayer.bind(data) if (existingPlayer.isPlaying && MediaPlayerData.addMediaPlayer(key, data, existingPlayer) mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { if (visualStabilityManager.isReorderingAllowed) { if (visualStabilityManager.isReorderingAllowed) { mediaContent.removeView(existingPlayer.view?.player) reorderAllPlayers() mediaContent.addView(existingPlayer.view?.player, 0) } else { } else { needsReordering = true needsReordering = true } } } } } updatePageIndicator() updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() mediaCarouselScrollHandler.onPlayersChanged() mediaCarousel.requiresRemeasuring = true mediaCarousel.requiresRemeasuring = true // Check postcondition: mediaContent should have the same number of children as there are // Check postcondition: mediaContent should have the same number of children as there are // elements in mediaPlayers. // elements in mediaPlayers. if (mediaPlayers.size != mediaContent.childCount) { if (MediaPlayerData.players().size != mediaContent.childCount) { Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") } } } } private fun removePlayer(key: String) { private fun removePlayer(key: String) { val removed = mediaPlayers.remove(key) val removed = MediaPlayerData.removeMediaPlayer(key) removed?.apply { removed?.apply { mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaContent.removeView(removed.view?.player) mediaContent.removeView(removed.view?.player) Loading @@ -295,12 +275,7 @@ class MediaCarouselController @Inject constructor( } } private fun recreatePlayers() { private fun recreatePlayers() { // Note that this will scramble the order of players. Actively playing sessions will, at MediaPlayerData.mediaData().forEach { (key, data) -> // least, still be put in the front. If we want to maintain order, then more work is // needed. mediaData.forEach { key, data -> removePlayer(key) addOrUpdatePlayer(key = key, oldKey = null, data = data) addOrUpdatePlayer(key = key, oldKey = null, data = data) } } } } Loading Loading @@ -338,7 +313,7 @@ class MediaCarouselController @Inject constructor( currentStartLocation = startLocation currentStartLocation = startLocation currentEndLocation = endLocation currentEndLocation = endLocation currentTransitionProgress = progress currentTransitionProgress = progress for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in MediaPlayerData.players()) { updatePlayerToState(mediaPlayer, immediately) updatePlayerToState(mediaPlayer, immediately) } } maybeResetSettingsCog() maybeResetSettingsCog() Loading Loading @@ -387,7 +362,7 @@ class MediaCarouselController @Inject constructor( private fun updateCarouselDimensions() { private fun updateCarouselDimensions() { var width = 0 var width = 0 var height = 0 var height = 0 for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in MediaPlayerData.players()) { val controller = mediaPlayer.mediaViewController val controller = mediaPlayer.mediaViewController // When transitioning the view to gone, the view gets smaller, but the translation // When transitioning the view to gone, the view gets smaller, but the translation // Doesn't, let's add the translation // Doesn't, let's add the translation Loading Loading @@ -449,7 +424,7 @@ class MediaCarouselController @Inject constructor( this.desiredLocation = desiredLocation this.desiredLocation = desiredLocation this.desiredHostState = it this.desiredHostState = it currentlyExpanded = it.expansion > 0 currentlyExpanded = it.expansion > 0 for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in MediaPlayerData.players()) { if (animate) { if (animate) { mediaPlayer.mediaViewController.animatePendingStateChange( mediaPlayer.mediaViewController.animatePendingStateChange( duration = duration, duration = duration, Loading @@ -471,7 +446,7 @@ class MediaCarouselController @Inject constructor( } } fun closeGuts() { fun closeGuts() { mediaPlayers.values.forEach { MediaPlayerData.players().forEach { it.closeGuts(true) it.closeGuts(true) } } } } Loading @@ -498,3 +473,50 @@ class MediaCarouselController @Inject constructor( } } } } } } @VisibleForTesting internal object MediaPlayerData { private data class MediaSortKey( val data: MediaData, val updateTime: Long = 0, val isPlaying: Boolean = false ) private val comparator = compareByDescending<MediaSortKey> { it.isPlaying } .thenByDescending { it.data.isLocalSession } .thenByDescending { !it.data.resumption } .thenByDescending { it.updateTime } private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel) { removeMediaPlayer(key) val sortKey = MediaSortKey(data, System.currentTimeMillis(), player.isPlaying()) mediaData.put(key, sortKey) mediaPlayers.put(sortKey, player) } fun getMediaPlayer(key: String, oldKey: String?): MediaControlPanel? { // If the key was changed, update entry oldKey?.let { if (it != key) { mediaData.remove(it)?.let { sortKey -> mediaData.put(key, sortKey) } } } return mediaData.get(key)?.let { mediaPlayers.get(it) } } fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { mediaPlayers.remove(it) } fun mediaData() = mediaData.entries.map { e -> Pair(e.key, e.value.data) } fun players() = mediaPlayers.values @VisibleForTesting fun clear() { mediaData.clear() mediaPlayers.clear() } }
packages/SystemUI/src/com/android/systemui/media/MediaData.kt +4 −0 Original line number Original line Diff line number Diff line Loading @@ -81,6 +81,10 @@ data class MediaData( * Action that should be performed to restart a non active session. * Action that should be performed to restart a non active session. */ */ var resumeAction: Runnable?, var resumeAction: Runnable?, /** * Local or remote playback */ var isLocalSession: Boolean = true, /** /** * Indicates that this player is a resumption player (ie. It only shows a play actions which * Indicates that this player is a resumption player (ie. It only shows a play actions which * will start the app and start playing). * will start the app and start playing). Loading
packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +8 −3 Original line number Original line Diff line number Diff line Loading @@ -31,6 +31,7 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.graphics.drawable.Icon import android.media.MediaDescription import android.media.MediaDescription import android.media.MediaMetadata import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.MediaSession import android.media.session.MediaSession import android.net.Uri import android.net.Uri import android.os.UserHandle import android.os.UserHandle Loading Loading @@ -337,7 +338,8 @@ class MediaDataManager( ) { ) { val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) as MediaSession.Token? as MediaSession.Token? val metadata = mediaControllerFactory.create(token).metadata val mediaController = mediaControllerFactory.create(token) val metadata = mediaController.metadata // Foreground and Background colors computed from album art // Foreground and Background colors computed from album art val notif: Notification = sbn.notification val notif: Notification = sbn.notification Loading Loading @@ -429,6 +431,9 @@ class MediaDataManager( } } } } val isLocalSession = mediaController.playbackInfo?.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ?: true foregroundExecutor.execute { foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true Loading @@ -436,8 +441,8 @@ class MediaDataManager( onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, smallIconDrawable, artist, song, artWorkIcon, actionIcons, smallIconDrawable, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, active, resumeAction = resumeAction, notificationKey = key, active, resumeAction = resumeAction, isLocalSession = isLocalSession, hasCheckedForResume = hasCheckedForResume)) notificationKey = key, hasCheckedForResume = hasCheckedForResume)) } } } } Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +2 −2 Original line number Original line Diff line number Diff line Loading @@ -83,8 +83,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mManager.addListener(mListener); mManager.addListener(mListener); mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, false, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true, KEY, false); false, KEY, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); } } Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt 0 → 100644 +122 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.media import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) public class MediaPlayerDataTest : SysuiTestCase() { companion object { val LOCAL = true val RESUMPTION = true } @Before fun setup() { MediaPlayerData.clear() } @Test fun addPlayingThenRemote() { val playerIsPlaying = mock(MediaControlPanel::class.java) whenever(playerIsPlaying.isPlaying).thenReturn(true) val dataIsPlaying = createMediaData(LOCAL, !RESUMPTION) val playerIsRemote = mock(MediaControlPanel::class.java) whenever(playerIsRemote.isPlaying).thenReturn(false) val dataIsRemote = createMediaData(!LOCAL, !RESUMPTION) MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying) MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote) val players = MediaPlayerData.players() assertThat(players).hasSize(2) assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder() } @Test fun switchPlayersPlaying() { val playerIsPlaying1 = mock(MediaControlPanel::class.java) whenever(playerIsPlaying1.isPlaying).thenReturn(true) val dataIsPlaying1 = createMediaData(LOCAL, !RESUMPTION) val playerIsPlaying2 = mock(MediaControlPanel::class.java) whenever(playerIsPlaying2.isPlaying).thenReturn(false) val dataIsPlaying2 = createMediaData(LOCAL, !RESUMPTION) MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1) MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2) whenever(playerIsPlaying1.isPlaying).thenReturn(false) whenever(playerIsPlaying2.isPlaying).thenReturn(true) MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1) MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2) val players = MediaPlayerData.players() assertThat(players).hasSize(2) assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder() } @Test fun fullOrderTest() { val playerIsPlaying = mock(MediaControlPanel::class.java) whenever(playerIsPlaying.isPlaying).thenReturn(true) val dataIsPlaying = createMediaData(LOCAL, !RESUMPTION) val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java) whenever(playerIsPlayingAndRemote.isPlaying).thenReturn(true) val dataIsPlayingAndRemote = createMediaData(!LOCAL, !RESUMPTION) val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java) whenever(playerIsStoppedAndLocal.isPlaying).thenReturn(false) val dataIsStoppedAndLocal = createMediaData(LOCAL, !RESUMPTION) val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java) whenever(playerIsStoppedAndLocal.isPlaying).thenReturn(false) val dataIsStoppedAndRemote = createMediaData(!LOCAL, !RESUMPTION) val playerCanResume = mock(MediaControlPanel::class.java) whenever(playerCanResume.isPlaying).thenReturn(false) val dataCanResume = createMediaData(LOCAL, RESUMPTION) MediaPlayerData.addMediaPlayer("3", dataIsStoppedAndLocal, playerIsStoppedAndLocal) MediaPlayerData.addMediaPlayer("5", dataIsStoppedAndRemote, playerIsStoppedAndRemote) MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume) MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying) MediaPlayerData.addMediaPlayer("2", dataIsPlayingAndRemote, playerIsPlayingAndRemote) val players = MediaPlayerData.players() assertThat(players).hasSize(5) assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote, playerIsStoppedAndLocal, playerCanResume, playerIsStoppedAndRemote).inOrder() } private fun createMediaData(isLocalSession: Boolean, resumption: Boolean) = MediaData(0, false, 0, null, null, null, null, null, emptyList(), emptyList<Int>(), "", null, null, null, true, null, isLocalSession, resumption, null, false) }