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
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java
index d154434..af5196f 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.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 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 @@
     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 @@
     private static final class KeyguardMediaViewModel {
 
         private final Context mContext;
+        private final MediaControllerFactory mMediaControllerFactory;
         private final MutableLiveData<KeyguardMedia> mMedia = new MutableLiveData<>();
         private final Object mActionsLock = new Object();
         private List<PendingIntent> 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 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 0000000..71bc7c2
--- /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 072bc44..4bcf917 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 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.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 @@
 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 @@
 
     @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 @@
         whenever<TextView>(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 @@
 
     @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)
     }
@@ -122,11 +142,22 @@
     }
 
     @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 @@
         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)