Use same steps to test MBS connection as when actually connecting

Bug: 163817074
Test: manual
Test: atest MediaResumeListenerTest ResumeMediaBrowserTest
Change-Id: I68b3f98dbcc984822b737887c222be771093e9c6
This commit is contained in:
Beth Thibodeau
2020-09-21 22:06:21 -04:00
parent b4ada77807
commit 7d8b80e602
6 changed files with 697 additions and 54 deletions

View File

@@ -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);
}
}

View File

@@ -28,6 +28,7 @@ import android.os.UserHandle
import android.provider.Settings import android.provider.Settings
import android.service.media.MediaBrowserService import android.service.media.MediaBrowserService
import android.util.Log import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
@@ -47,7 +48,8 @@ class MediaResumeListener @Inject constructor(
private val context: Context, private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor, @Background private val backgroundExecutor: Executor,
private val tunerService: TunerService private val tunerService: TunerService,
private val mediaBrowserFactory: ResumeMediaBrowserFactory
) : MediaDataManager.Listener { ) : MediaDataManager.Listener {
private var useMediaResumption: Boolean = Utils.useMediaResumption(context) private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
@@ -59,7 +61,8 @@ class MediaResumeListener @Inject constructor(
private var mediaBrowser: ResumeMediaBrowser? = null private var mediaBrowser: ResumeMediaBrowser? = null
private var currentUserId: Int = context.userId private var currentUserId: Int = context.userId
private val userChangeReceiver = object : BroadcastReceiver() { @VisibleForTesting
val userChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_USER_UNLOCKED == intent.action) { if (Intent.ACTION_USER_UNLOCKED == intent.action) {
loadMediaResumptionControls() loadMediaResumptionControls()
@@ -152,7 +155,7 @@ class MediaResumeListener @Inject constructor(
resumeComponents.forEach { resumeComponents.forEach {
if (!blockedApps.contains(it.packageName)) { if (!blockedApps.contains(it.packageName)) {
val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it) val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
browser.findRecentMedia() browser.findRecentMedia()
} }
} }
@@ -193,14 +196,10 @@ class MediaResumeListener @Inject constructor(
private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
Log.d(TAG, "Testing if we can connect to $componentName") Log.d(TAG, "Testing if we can connect to $componentName")
mediaBrowser?.disconnect() mediaBrowser?.disconnect()
mediaBrowser = ResumeMediaBrowser(context, mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() { object : ResumeMediaBrowser.Callback() {
override fun onConnected() { override fun onConnected() {
Log.d(TAG, "yes we can resume with $componentName") Log.d(TAG, "Connected to $componentName")
mediaDataManager.setResumeAction(key, getResumeAction(componentName))
updateResumptionList(componentName)
mediaBrowser?.disconnect()
mediaBrowser = null
} }
override fun onError() { override fun onError() {
@@ -209,6 +208,19 @@ class MediaResumeListener @Inject constructor(
mediaBrowser?.disconnect() mediaBrowser?.disconnect()
mediaBrowser = null 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) componentName)
mediaBrowser?.testConnection() mediaBrowser?.testConnection()
@@ -245,7 +257,7 @@ class MediaResumeListener @Inject constructor(
private fun getResumeAction(componentName: ComponentName): Runnable { private fun getResumeAction(componentName: ComponentName): Runnable {
return Runnable { return Runnable {
mediaBrowser?.disconnect() mediaBrowser?.disconnect()
mediaBrowser = ResumeMediaBrowser(context, mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() { object : ResumeMediaBrowser.Callback() {
override fun onConnected() { override fun onConnected() {
if (mediaBrowser?.token == null) { if (mediaBrowser?.token == null) {

View File

@@ -30,6 +30,8 @@ import android.service.media.MediaBrowserService;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.List; import java.util.List;
/** /**
@@ -46,6 +48,7 @@ public class ResumeMediaBrowser {
private static final String TAG = "ResumeMediaBrowser"; private static final String TAG = "ResumeMediaBrowser";
private final Context mContext; private final Context mContext;
private final Callback mCallback; private final Callback mCallback;
private MediaBrowserFactory mBrowserFactory;
private MediaBrowser mMediaBrowser; private MediaBrowser mMediaBrowser;
private ComponentName mComponentName; private ComponentName mComponentName;
@@ -55,10 +58,12 @@ public class ResumeMediaBrowser {
* @param callback used to report media items found * @param callback used to report media items found
* @param componentName Component name of the MediaBrowserService this browser will connect to * @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; mContext = context;
mCallback = callback; mCallback = callback;
mComponentName = componentName; mComponentName = componentName;
mBrowserFactory = browserFactory;
} }
/** /**
@@ -74,7 +79,7 @@ public class ResumeMediaBrowser {
disconnect(); disconnect();
Bundle rootHints = new Bundle(); Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
mMediaBrowser = new MediaBrowser(mContext, mMediaBrowser = mBrowserFactory.create(
mComponentName, mComponentName,
mConnectionCallback, mConnectionCallback,
rootHints); rootHints);
@@ -88,17 +93,19 @@ public class ResumeMediaBrowser {
List<MediaBrowser.MediaItem> children) { List<MediaBrowser.MediaItem> children) {
if (children.size() == 0) { if (children.size() == 0) {
Log.d(TAG, "No children found for " + mComponentName); Log.d(TAG, "No children found for " + mComponentName);
return; mCallback.onError();
}
// 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 { } 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(); disconnect();
} }
@@ -131,7 +138,7 @@ public class ResumeMediaBrowser {
Log.d(TAG, "Service connected for " + mComponentName); Log.d(TAG, "Service connected for " + mComponentName);
if (mMediaBrowser != null && mMediaBrowser.isConnected()) { if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
String root = mMediaBrowser.getRoot(); String root = mMediaBrowser.getRoot();
if (!TextUtils.isEmpty(root)) { if (!TextUtils.isEmpty(root) && mMediaBrowser != null) {
mCallback.onConnected(); mCallback.onConnected();
mMediaBrowser.subscribe(root, mSubscriptionCallback); mMediaBrowser.subscribe(root, mSubscriptionCallback);
return; return;
@@ -182,7 +189,7 @@ public class ResumeMediaBrowser {
disconnect(); disconnect();
Bundle rootHints = new Bundle(); Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
mMediaBrowser = new MediaBrowser(mContext, mComponentName, mMediaBrowser = mBrowserFactory.create(mComponentName,
new MediaBrowser.ConnectionCallback() { new MediaBrowser.ConnectionCallback() {
@Override @Override
public void onConnected() { public void onConnected() {
@@ -192,7 +199,7 @@ public class ResumeMediaBrowser {
return; return;
} }
MediaSession.Token token = mMediaBrowser.getSessionToken(); MediaSession.Token token = mMediaBrowser.getSessionToken();
MediaController controller = new MediaController(mContext, token); MediaController controller = createMediaController(token);
controller.getTransportControls(); controller.getTransportControls();
controller.getTransportControls().prepare(); controller.getTransportControls().prepare();
controller.getTransportControls().play(); controller.getTransportControls().play();
@@ -212,6 +219,11 @@ public class ResumeMediaBrowser {
mMediaBrowser.connect(); mMediaBrowser.connect();
} }
@VisibleForTesting
protected MediaController createMediaController(MediaSession.Token token) {
return new MediaController(mContext, token);
}
/** /**
* Get the media session token * Get the media session token
* @return the token, or null if the MediaBrowser is null or disconnected * @return the token, or null if the MediaBrowser is null or disconnected
@@ -235,42 +247,19 @@ public class ResumeMediaBrowser {
/** /**
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser. * 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 * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is
* depending on whether it was successful. * 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. * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/ */
public void testConnection() { public void testConnection() {
disconnect(); 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(); Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
mMediaBrowser = new MediaBrowser(mContext, mMediaBrowser = mBrowserFactory.create(
mComponentName, mComponentName,
connectionCallback, mConnectionCallback,
rootHints); rootHints);
mMediaBrowser.connect(); mMediaBrowser.connect();
} }

View File

@@ -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);
}
}

View File

@@ -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))
}
}

View File

@@ -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
}
}
}