From 13bc4269d6c73e2dc51d8d873009ae31049ef674 Mon Sep 17 00:00:00 2001 From: Robert Snoeberger Date: Tue, 7 Apr 2020 15:00:36 -0400 Subject: [PATCH] Close LS media player when session is destroyed When updating media player, check the playing state. It is null after the media session has been destroyed. This is necessary since the NotificationMediaManager is used as the source of information about the "current" session. It still sends updates after a media session has been destroyed. Fixes: 152821122 Test: play podcast, pause and dismiss app. Go to lock screen. Verify that LS media player has closed. Change-Id: I52445d18eb945127f6c9af0908e154ac983e88cf --- .../android/keyguard/KeyguardMediaPlayer.java | 23 ++++++++-- .../media/MediaControllerFactory.java | 45 +++++++++++++++++++ .../keyguard/KeyguardMediaPlayerTest.kt | 43 +++++++++++++++--- 3 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java index d1544346a25a4..af5196f92bcba 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java @@ -24,6 +24,8 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; import android.util.Log; import android.view.View; import android.widget.ImageButton; @@ -40,6 +42,7 @@ import androidx.palette.graphics.Palette; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.media.MediaControllerFactory; import com.android.systemui.statusbar.notification.MediaNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.stack.MediaHeaderView; @@ -71,10 +74,11 @@ public class KeyguardMediaPlayer { private KeyguardMediaObserver mObserver; @Inject - public KeyguardMediaPlayer(Context context, @Background Executor backgroundExecutor) { + public KeyguardMediaPlayer(Context context, MediaControllerFactory factory, + @Background Executor backgroundExecutor) { mContext = context; mBackgroundExecutor = backgroundExecutor; - mViewModel = new KeyguardMediaViewModel(context); + mViewModel = new KeyguardMediaViewModel(context, factory); } /** Binds media controls to a view hierarchy. */ @@ -139,14 +143,16 @@ public class KeyguardMediaPlayer { private static final class KeyguardMediaViewModel { private final Context mContext; + private final MediaControllerFactory mMediaControllerFactory; private final MutableLiveData mMedia = new MutableLiveData<>(); private final Object mActionsLock = new Object(); private List mActions; private float mAlbumArtRadius; private int mAlbumArtSize; - KeyguardMediaViewModel(Context context) { + KeyguardMediaViewModel(Context context, MediaControllerFactory factory) { mContext = context; + mMediaControllerFactory = factory; loadDimens(); } @@ -162,6 +168,17 @@ public class KeyguardMediaPlayer { public void updateControls(NotificationEntry entry, Icon appIcon, MediaMetadata mediaMetadata) { + // Check the playback state of the media controller. If it is null, then the session was + // probably destroyed. Don't update in this case. + final MediaSession.Token token = entry.getSbn().getNotification().extras + .getParcelable(Notification.EXTRA_MEDIA_SESSION); + final MediaController controller = token != null + ? mMediaControllerFactory.create(token) : null; + if (controller != null && controller.getPlaybackState() == null) { + clearControls(); + return; + } + // Foreground and Background colors computed from album art Notification notif = entry.getSbn().getNotification(); int fgColor = notif.color; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java new file mode 100644 index 0000000000000..71bc7c20c0264 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java @@ -0,0 +1,45 @@ +/* + * 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.content.Context; +import android.media.session.MediaController; +import android.media.session.MediaSession; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link MediaController} constructor. + */ +public class MediaControllerFactory { + + private final Context mContext; + + @Inject + public MediaControllerFactory(Context context) { + mContext = context; + } + + /** + * Creates a new MediaController from a session's token. + * + * @param token The token for the session. This value must never be null. + */ + public MediaController create(MediaSession.Token token) { + return new MediaController(mContext, token); + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt index 072bc446fd21d..4bcf917fa95d4 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt @@ -16,8 +16,12 @@ package com.android.keyguard +import android.app.Notification import android.graphics.drawable.Icon import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -28,7 +32,9 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.media.MediaControllerFactory import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -38,6 +44,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.any import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -48,9 +55,12 @@ import org.mockito.Mockito.`when` as whenever public class KeyguardMediaPlayerTest : SysuiTestCase() { private lateinit var keyguardMediaPlayer: KeyguardMediaPlayer + @Mock private lateinit var mockMediaFactory: MediaControllerFactory + @Mock private lateinit var mockMediaController: MediaController + private lateinit var playbackState: PlaybackState private lateinit var fakeExecutor: FakeExecutor private lateinit var mediaMetadata: MediaMetadata.Builder - private lateinit var entry: NotificationEntryBuilder + private lateinit var entry: NotificationEntry @Mock private lateinit var mockView: View private lateinit var songView: TextView private lateinit var artistView: TextView @@ -70,8 +80,16 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() { @Before public fun setup() { + playbackState = PlaybackState.Builder().run { + build() + } + mockMediaController = mock(MediaController::class.java) + whenever(mockMediaController.getPlaybackState()).thenReturn(playbackState) + mockMediaFactory = mock(MediaControllerFactory::class.java) + whenever(mockMediaFactory.create(any())).thenReturn(mockMediaController) + fakeExecutor = FakeExecutor(FakeSystemClock()) - keyguardMediaPlayer = KeyguardMediaPlayer(context, fakeExecutor) + keyguardMediaPlayer = KeyguardMediaPlayer(context, mockMediaFactory, fakeExecutor) mockIcon = mock(Icon::class.java) mockView = mock(View::class.java) @@ -81,7 +99,9 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() { whenever(mockView.findViewById(R.id.header_artist)).thenReturn(artistView) mediaMetadata = MediaMetadata.Builder() - entry = NotificationEntryBuilder() + entry = NotificationEntryBuilder().build() + entry.getSbn().getNotification().extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token(1, null)) ArchTaskExecutor.getInstance().setDelegate(taskExecutor) @@ -109,7 +129,7 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() { @Test public fun testUpdateControls() { - keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build()) + keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) FakeExecutor.exhaustExecutors(fakeExecutor) verify(mockView).setVisibility(View.VISIBLE) } @@ -121,12 +141,23 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() { verify(mockView).setVisibility(View.GONE) } + @Test + public fun testUpdateControlsNullPlaybackState() { + // GIVEN that the playback state is null (ie. the media session was destroyed) + whenever(mockMediaController.getPlaybackState()).thenReturn(null) + // WHEN updated + keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) + FakeExecutor.exhaustExecutors(fakeExecutor) + // THEN the controls are cleared (ie. visibility is set to GONE) + verify(mockView).setVisibility(View.GONE) + } + @Test public fun testSongName() { val song: String = "Song" mediaMetadata.putText(MediaMetadata.METADATA_KEY_TITLE, song) - keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build()) + keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) assertThat(fakeExecutor.runAllReady()).isEqualTo(1) assertThat(songView.getText()).isEqualTo(song) @@ -137,7 +168,7 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() { val artist: String = "Artist" mediaMetadata.putText(MediaMetadata.METADATA_KEY_ARTIST, artist) - keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build()) + keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) assertThat(fakeExecutor.runAllReady()).isEqualTo(1) assertThat(artistView.getText()).isEqualTo(artist)