Use same steps to test MBS connection as when actually connecting
Bug: 163817074
Test: manual
Test: atest MediaResumeListenerTest ResumeMediaBrowserTest
Change-Id: I68b3f98dbcc984822b737887c222be771093e9c6
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
new file mode 100644
index 0000000..aca033e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ComponentName;
+import android.content.Context;
+import android.media.browse.MediaBrowser;
+import android.os.Bundle;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link MediaBrowser} constructor
+ */
+public class MediaBrowserFactory {
+ private final Context mContext;
+
+ @Inject
+ public MediaBrowserFactory(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Creates a new MediaBrowser
+ *
+ * @param serviceComponent
+ * @param callback
+ * @param rootHints
+ * @return
+ */
+ public MediaBrowser create(ComponentName serviceComponent,
+ MediaBrowser.ConnectionCallback callback, Bundle rootHints) {
+ return new MediaBrowser(mContext, serviceComponent, callback, rootHints);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
index 5b59214..5c1c60c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
@@ -28,6 +28,7 @@
import android.provider.Settings
import android.service.media.MediaBrowserService
import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -47,7 +48,8 @@
private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor,
- private val tunerService: TunerService
+ private val tunerService: TunerService,
+ private val mediaBrowserFactory: ResumeMediaBrowserFactory
) : MediaDataManager.Listener {
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
@@ -59,7 +61,8 @@
private var mediaBrowser: ResumeMediaBrowser? = null
private var currentUserId: Int = context.userId
- private val userChangeReceiver = object : BroadcastReceiver() {
+ @VisibleForTesting
+ val userChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_USER_UNLOCKED == intent.action) {
loadMediaResumptionControls()
@@ -152,7 +155,7 @@
resumeComponents.forEach {
if (!blockedApps.contains(it.packageName)) {
- val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it)
+ val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
browser.findRecentMedia()
}
}
@@ -193,14 +196,10 @@
private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
Log.d(TAG, "Testing if we can connect to $componentName")
mediaBrowser?.disconnect()
- mediaBrowser = ResumeMediaBrowser(context,
+ mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
- Log.d(TAG, "yes we can resume with $componentName")
- mediaDataManager.setResumeAction(key, getResumeAction(componentName))
- updateResumptionList(componentName)
- mediaBrowser?.disconnect()
- mediaBrowser = null
+ Log.d(TAG, "Connected to $componentName")
}
override fun onError() {
@@ -209,6 +208,19 @@
mediaBrowser?.disconnect()
mediaBrowser = null
}
+
+ override fun addTrack(
+ desc: MediaDescription,
+ component: ComponentName,
+ browser: ResumeMediaBrowser
+ ) {
+ // Since this is a test, just save the component for later
+ Log.d(TAG, "Can get resumable media from $componentName")
+ mediaDataManager.setResumeAction(key, getResumeAction(componentName))
+ updateResumptionList(componentName)
+ mediaBrowser?.disconnect()
+ mediaBrowser = null
+ }
},
componentName)
mediaBrowser?.testConnection()
@@ -245,7 +257,7 @@
private fun getResumeAction(componentName: ComponentName): Runnable {
return Runnable {
mediaBrowser?.disconnect()
- mediaBrowser = ResumeMediaBrowser(context,
+ mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
if (mediaBrowser?.token == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
index 68b6785..a4d4436 100644
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
@@ -30,6 +30,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.List;
/**
@@ -46,6 +48,7 @@
private static final String TAG = "ResumeMediaBrowser";
private final Context mContext;
private final Callback mCallback;
+ private MediaBrowserFactory mBrowserFactory;
private MediaBrowser mMediaBrowser;
private ComponentName mComponentName;
@@ -55,10 +58,12 @@
* @param callback used to report media items found
* @param componentName Component name of the MediaBrowserService this browser will connect to
*/
- public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+ public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName,
+ MediaBrowserFactory browserFactory) {
mContext = context;
mCallback = callback;
mComponentName = componentName;
+ mBrowserFactory = browserFactory;
}
/**
@@ -74,7 +79,7 @@
disconnect();
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
- mMediaBrowser = new MediaBrowser(mContext,
+ mMediaBrowser = mBrowserFactory.create(
mComponentName,
mConnectionCallback,
rootHints);
@@ -88,17 +93,19 @@
List<MediaBrowser.MediaItem> children) {
if (children.size() == 0) {
Log.d(TAG, "No children found for " + mComponentName);
- return;
- }
- // We ask apps to return a playable item as the first child when sending
- // a request with EXTRA_RECENT; if they don't, no resume controls
- MediaBrowser.MediaItem child = children.get(0);
- MediaDescription desc = child.getDescription();
- if (child.isPlayable() && mMediaBrowser != null) {
- mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
- ResumeMediaBrowser.this);
+ mCallback.onError();
} else {
- Log.d(TAG, "Child found but not playable for " + mComponentName);
+ // We ask apps to return a playable item as the first child when sending
+ // a request with EXTRA_RECENT; if they don't, no resume controls
+ MediaBrowser.MediaItem child = children.get(0);
+ MediaDescription desc = child.getDescription();
+ if (child.isPlayable() && mMediaBrowser != null) {
+ mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
+ ResumeMediaBrowser.this);
+ } else {
+ Log.d(TAG, "Child found but not playable for " + mComponentName);
+ mCallback.onError();
+ }
}
disconnect();
}
@@ -131,7 +138,7 @@
Log.d(TAG, "Service connected for " + mComponentName);
if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
String root = mMediaBrowser.getRoot();
- if (!TextUtils.isEmpty(root)) {
+ if (!TextUtils.isEmpty(root) && mMediaBrowser != null) {
mCallback.onConnected();
mMediaBrowser.subscribe(root, mSubscriptionCallback);
return;
@@ -182,7 +189,7 @@
disconnect();
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
- mMediaBrowser = new MediaBrowser(mContext, mComponentName,
+ mMediaBrowser = mBrowserFactory.create(mComponentName,
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
@@ -192,7 +199,7 @@
return;
}
MediaSession.Token token = mMediaBrowser.getSessionToken();
- MediaController controller = new MediaController(mContext, token);
+ MediaController controller = createMediaController(token);
controller.getTransportControls();
controller.getTransportControls().prepare();
controller.getTransportControls().play();
@@ -212,6 +219,11 @@
mMediaBrowser.connect();
}
+ @VisibleForTesting
+ protected MediaController createMediaController(MediaSession.Token token) {
+ return new MediaController(mContext, token);
+ }
+
/**
* Get the media session token
* @return the token, or null if the MediaBrowser is null or disconnected
@@ -235,42 +247,19 @@
/**
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
- * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
- * depending on whether it was successful.
+ * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is
+ * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more
+ * detailed logging if the service has issues. If it cannot connect, or cannot find valid media,
+ * then ResumeMediaBrowser.Callback#onError will be called.
* ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/
public void testConnection() {
disconnect();
- final MediaBrowser.ConnectionCallback connectionCallback =
- new MediaBrowser.ConnectionCallback() {
- @Override
- public void onConnected() {
- Log.d(TAG, "connected");
- if (mMediaBrowser == null || !mMediaBrowser.isConnected()
- || TextUtils.isEmpty(mMediaBrowser.getRoot())) {
- mCallback.onError();
- } else {
- mCallback.onConnected();
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- Log.d(TAG, "suspended");
- mCallback.onError();
- }
-
- @Override
- public void onConnectionFailed() {
- Log.d(TAG, "failed");
- mCallback.onError();
- }
- };
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
- mMediaBrowser = new MediaBrowser(mContext,
+ mMediaBrowser = mBrowserFactory.create(
mComponentName,
- connectionCallback,
+ mConnectionCallback,
rootHints);
mMediaBrowser.connect();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
new file mode 100644
index 0000000..2261aa5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
@@ -0,0 +1,48 @@
+/*
+ * 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.ComponentName;
+import android.content.Context;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link ResumeMediaBrowser} constructor
+ */
+public class ResumeMediaBrowserFactory {
+ private final Context mContext;
+ private final MediaBrowserFactory mBrowserFactory;
+
+ @Inject
+ public ResumeMediaBrowserFactory(Context context, MediaBrowserFactory browserFactory) {
+ mContext = context;
+ mBrowserFactory = browserFactory;
+ }
+
+ /**
+ * Creates a new ResumeMediaBrowser.
+ *
+ * @param callback will be called on connection or error, and addTrack when media item found
+ * @param componentName component to browse
+ * @return
+ */
+ public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
+ ComponentName componentName) {
+ return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
new file mode 100644
index 0000000..5d81de6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
@@ -0,0 +1,258 @@
+/*
+ * 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.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.graphics.Color
+import android.media.MediaDescription
+import android.media.session.MediaSession
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.After
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+private const val KEY = "TEST_KEY"
+private const val OLD_KEY = "RESUME_KEY"
+private const val APP = "APP"
+private const val BG_COLOR = Color.RED
+private const val PACKAGE_NAME = "PKG"
+private const val CLASS_NAME = "CLASS"
+private const val ARTIST = "ARTIST"
+private const val TITLE = "TITLE"
+private const val USER_ID = 0
+private const val MEDIA_PREFERENCES = "media_control_prefs"
+private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
+
+private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+private fun <T> any(): T = Mockito.any<T>()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaResumeListenerTest : SysuiTestCase() {
+
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var mediaDataManager: MediaDataManager
+ @Mock private lateinit var device: MediaDeviceData
+ @Mock private lateinit var token: MediaSession.Token
+ @Mock private lateinit var tunerService: TunerService
+ @Mock private lateinit var resumeBrowserFactory: ResumeMediaBrowserFactory
+ @Mock private lateinit var resumeBrowser: ResumeMediaBrowser
+ @Mock private lateinit var sharedPrefs: SharedPreferences
+ @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor
+ @Mock private lateinit var mockContext: Context
+ @Mock private lateinit var pendingIntent: PendingIntent
+
+ @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
+
+ private lateinit var executor: FakeExecutor
+ private lateinit var data: MediaData
+ private lateinit var resumeListener: MediaResumeListener
+
+ private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
+ Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
+ private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ Settings.Global.putInt(context.contentResolver,
+ Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
+ Settings.Secure.putInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
+
+ whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
+ .thenReturn(resumeBrowser)
+
+ // resume components are stored in sharedpreferences
+ whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
+ .thenReturn(sharedPrefs)
+ whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
+ whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
+ whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
+ whenever(mockContext.packageManager).thenReturn(context.packageManager)
+ whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
+
+ executor = FakeExecutor(FakeSystemClock())
+ resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
+ tunerService, resumeBrowserFactory)
+ resumeListener.setManager(mediaDataManager)
+ mediaDataManager.addListener(resumeListener)
+
+ data = MediaData(
+ userId = USER_ID,
+ initialized = true,
+ backgroundColor = BG_COLOR,
+ app = APP,
+ appIcon = null,
+ artist = ARTIST,
+ song = TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE_NAME,
+ token = token,
+ clickIntent = null,
+ device = device,
+ active = true,
+ notificationKey = KEY,
+ resumeAction = null)
+ }
+
+ @After
+ fun tearDown() {
+ Settings.Global.putInt(context.contentResolver,
+ Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting)
+ Settings.Secure.putInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting)
+ }
+
+ @Test
+ fun testWhenNoResumption_doesNothing() {
+ Settings.Secure.putInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+ // When listener is created, we do NOT register a user change listener
+ val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
+ resumeBrowserFactory)
+ listener.setManager(mediaDataManager)
+ verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
+ any(), any(), any())
+
+ // When data is loaded, we do NOT execute or update anything
+ listener.onMediaDataLoaded(KEY, OLD_KEY, data)
+ assertThat(executor.numPending()).isEqualTo(0)
+ verify(mediaDataManager, never()).setResumeAction(any(), any())
+ }
+
+ @Test
+ fun testOnLoad_checksForResume_noService() {
+ // When media data is loaded that has not been checked yet, and does not have a MBS
+ resumeListener.onMediaDataLoaded(KEY, null, data)
+
+ // Then we report back to the manager
+ verify(mediaDataManager).setResumeAction(KEY, null)
+ }
+
+ @Test
+ fun testOnLoad_checksForResume_hasService() {
+ // Set up mocks to successfully find a MBS that returns valid media
+ val pm = mock(PackageManager::class.java)
+ whenever(mockContext.packageManager).thenReturn(pm)
+ val resolveInfo = ResolveInfo()
+ val serviceInfo = ServiceInfo()
+ serviceInfo.packageName = PACKAGE_NAME
+ resolveInfo.serviceInfo = serviceInfo
+ resolveInfo.serviceInfo.name = CLASS_NAME
+ val resumeInfo = listOf(resolveInfo)
+ whenever(pm.queryIntentServices(any(), anyInt())).thenReturn(resumeInfo)
+
+ val description = MediaDescription.Builder().setTitle(TITLE).build()
+ val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+ whenever(resumeBrowser.testConnection()).thenAnswer {
+ callbackCaptor.value.addTrack(description, component, resumeBrowser)
+ }
+
+ // When media data is loaded that has not been checked yet, and does have a MBS
+ val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
+ resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+ // Then we test whether the service is valid
+ executor.runAllReady()
+ verify(resumeBrowser).testConnection()
+
+ // And since it is, we report back to the manager
+ verify(mediaDataManager).setResumeAction(eq(KEY), any())
+
+ // But we do not tell it to add new controls
+ verify(mediaDataManager, never())
+ .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
+
+ // Finally, make sure the resume browser disconnected
+ verify(resumeBrowser).disconnect()
+ }
+
+ @Test
+ fun testOnLoad_doesNotCheckAgain() {
+ // When a media data is loaded that has been checked already
+ var dataCopy = data.copy(hasCheckedForResume = true)
+ resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+ // Then we should not check it again
+ verify(resumeBrowser, never()).testConnection()
+ verify(mediaDataManager, never()).setResumeAction(KEY, null)
+ }
+
+ @Test
+ fun testOnUserUnlock_loadsTracks() {
+ // Set up mock service to successfully find valid media
+ val description = MediaDescription.Builder().setTitle(TITLE).build()
+ val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+ whenever(resumeBrowser.token).thenReturn(token)
+ whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
+ whenever(resumeBrowser.findRecentMedia()).thenAnswer {
+ callbackCaptor.value.addTrack(description, component, resumeBrowser)
+ }
+
+ // Make sure broadcast receiver is registered
+ resumeListener.setManager(mediaDataManager)
+ verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver),
+ any(), any(), any())
+
+ // When we get an unlock event
+ val intent = Intent(Intent.ACTION_USER_UNLOCKED)
+ resumeListener.userChangeReceiver.onReceive(context, intent)
+
+ // Then we should attempt to find recent media for each saved component
+ verify(resumeBrowser, times(3)).findRecentMedia()
+
+ // Then since the mock service found media, the manager should be informed
+ verify(mediaDataManager, times(3)).addResumptionControls(anyInt(),
+ any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
new file mode 100644
index 0000000..d26229e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
@@ -0,0 +1,287 @@
+/*
+ * 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.ComponentName
+import android.content.Context
+import android.media.MediaDescription
+import android.media.browse.MediaBrowser
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.service.media.MediaBrowserService
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
+
+private const val PACKAGE_NAME = "package"
+private const val CLASS_NAME = "class"
+private const val TITLE = "song title"
+private const val MEDIA_ID = "media ID"
+private const val ROOT = "media browser root"
+
+private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+private fun <T> any(): T = Mockito.any<T>()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class ResumeMediaBrowserTest : SysuiTestCase() {
+
+ private lateinit var resumeBrowser: TestableResumeMediaBrowser
+ private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+ private val description = MediaDescription.Builder()
+ .setTitle(TITLE)
+ .setMediaId(MEDIA_ID)
+ .build()
+
+ @Mock lateinit var callback: ResumeMediaBrowser.Callback
+ @Mock lateinit var listener: MediaResumeListener
+ @Mock lateinit var service: MediaBrowserService
+ @Mock lateinit var browserFactory: MediaBrowserFactory
+ @Mock lateinit var browser: MediaBrowser
+ @Mock lateinit var token: MediaSession.Token
+ @Mock lateinit var mediaController: MediaController
+ @Mock lateinit var transportControls: MediaController.TransportControls
+
+ @Captor lateinit var connectionCallback: ArgumentCaptor<MediaBrowser.ConnectionCallback>
+ @Captor lateinit var subscriptionCallback: ArgumentCaptor<MediaBrowser.SubscriptionCallback>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(browserFactory.create(any(), capture(connectionCallback), any()))
+ .thenReturn(browser)
+
+ whenever(mediaController.transportControls).thenReturn(transportControls)
+
+ resumeBrowser = TestableResumeMediaBrowser(context, callback, component, browserFactory,
+ mediaController)
+ }
+
+ @Test
+ fun testConnection_connectionFails_callsOnError() {
+ // When testConnection cannot connect to the service
+ setupBrowserFailed()
+ resumeBrowser.testConnection()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testConnection_connects_onConnected() {
+ // When testConnection can connect to the service
+ setupBrowserConnection()
+ resumeBrowser.testConnection()
+
+ // Then it calls onConnected
+ verify(callback).onConnected()
+ }
+
+ @Test
+ fun testConnection_noValidMedia_error() {
+ // When testConnection can connect to the service, and does not find valid media
+ setupBrowserConnectionNoResults()
+ resumeBrowser.testConnection()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testConnection_hasValidMedia_addTrack() {
+ // When testConnection can connect to the service, and finds valid media
+ setupBrowserConnectionValidMedia()
+ resumeBrowser.testConnection()
+
+ // Then it calls addTrack
+ verify(callback).onConnected()
+ verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
+ }
+
+ @Test
+ fun testFindRecentMedia_connectionFails_error() {
+ // When findRecentMedia is called and we cannot connect
+ setupBrowserFailed()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_noRoot_error() {
+ // When findRecentMedia is called and does not get a valid root
+ setupBrowserConnection()
+ whenever(browser.getRoot()).thenReturn(null)
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_connects_onConnected() {
+ // When findRecentMedia is called and we connect
+ setupBrowserConnection()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onConnected
+ verify(callback).onConnected()
+ }
+
+ @Test
+ fun testFindRecentMedia_noChildren_error() {
+ // When findRecentMedia is called and we connect, but do not get any results
+ setupBrowserConnectionNoResults()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_notPlayable_error() {
+ // When findRecentMedia is called and we connect, but do not get a playable child
+ setupBrowserConnectionNotPlayable()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_hasValidMedia_addTrack() {
+ // When findRecentMedia is called and we can connect and get playable media
+ setupBrowserConnectionValidMedia()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls addTrack
+ verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
+ }
+
+ @Test
+ fun testRestart_connectionFails_error() {
+ // When restart is called and we cannot connect
+ setupBrowserFailed()
+ resumeBrowser.restart()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testRestart_connects() {
+ // When restart is called and we connect successfully
+ setupBrowserConnection()
+ resumeBrowser.restart()
+
+ // Then it creates a new controller and sends play command
+ verify(transportControls).prepare()
+ verify(transportControls).play()
+
+ // Then it calls onConnected
+ verify(callback).onConnected()
+ }
+
+ /**
+ * Helper function to mock a failed connection
+ */
+ private fun setupBrowserFailed() {
+ whenever(browser.connect()).thenAnswer {
+ connectionCallback.value.onConnectionFailed()
+ }
+ }
+
+ /**
+ * Helper function to mock a successful connection only
+ */
+ private fun setupBrowserConnection() {
+ whenever(browser.connect()).thenAnswer {
+ connectionCallback.value.onConnected()
+ }
+ whenever(browser.isConnected()).thenReturn(true)
+ whenever(browser.getRoot()).thenReturn(ROOT)
+ whenever(browser.sessionToken).thenReturn(token)
+ }
+
+ /**
+ * Helper function to mock a successful connection, but no media results
+ */
+ private fun setupBrowserConnectionNoResults() {
+ setupBrowserConnection()
+ whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+ subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
+ }
+ }
+
+ /**
+ * Helper function to mock a successful connection, but no playable results
+ */
+ private fun setupBrowserConnectionNotPlayable() {
+ setupBrowserConnection()
+
+ val child = MediaBrowser.MediaItem(description, 0)
+
+ whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+ subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
+ }
+ }
+
+ /**
+ * Helper function to mock a successful connection with playable media
+ */
+ private fun setupBrowserConnectionValidMedia() {
+ setupBrowserConnection()
+
+ val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
+
+ whenever(browser.serviceComponent).thenReturn(component)
+ whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+ subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
+ }
+ }
+
+ /**
+ * Override so media controller use is testable
+ */
+ private class TestableResumeMediaBrowser(
+ context: Context,
+ callback: Callback,
+ componentName: ComponentName,
+ browserFactory: MediaBrowserFactory,
+ private val fakeController: MediaController
+ ) : ResumeMediaBrowser(context, callback, componentName, browserFactory) {
+
+ override fun createMediaController(token: MediaSession.Token): MediaController {
+ return fakeController
+ }
+ }
+}
\ No newline at end of file