diff options
| author | 2020-09-24 19:03:56 +0000 | |
|---|---|---|
| committer | 2020-09-24 19:03:56 +0000 | |
| commit | 9746948a58ad389ed07146cb785eae6bb53fbc3f (patch) | |
| tree | 534945eeb7b0a47a89dfec7e62708cfc7c150259 | |
| parent | bfd3ac4d58f430e291489d142af9056fd4a2d278 (diff) | |
| parent | 63ade433671b1197f69aa1b50eb45fada3fd6167 (diff) | |
Merge "Use same steps to test MBS connection as when actually connecting" into rvc-qpr-dev
6 files changed, 697 insertions, 54 deletions
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 000000000000..aca033e99623 --- /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 1ac3034ea7c1..936db8735ad8 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.os.UserHandle 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.qualifiers.Background import com.android.systemui.tuner.TunerService @@ -47,7 +48,8 @@ class MediaResumeListener @Inject constructor( 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) @@ -58,7 +60,8 @@ class MediaResumeListener @Inject constructor( 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() @@ -142,7 +145,7 @@ class MediaResumeListener @Inject constructor( } resumeComponents.forEach { - val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it) + val browser = mediaBrowserFactory.create(mediaBrowserCallback, it) browser.findRecentMedia() } } @@ -181,14 +184,10 @@ class MediaResumeListener @Inject constructor( 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() { @@ -197,6 +196,19 @@ class MediaResumeListener @Inject constructor( 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() @@ -233,7 +245,7 @@ class MediaResumeListener @Inject constructor( 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 68b6785849aa..a4d44367be73 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.service.media.MediaBrowserService; import android.text.TextUtils; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.util.List; /** @@ -46,6 +48,7 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { * @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 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { 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 @@ public class ResumeMediaBrowser { /** * 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 000000000000..2261aa5ac265 --- /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 000000000000..5d81de6bce00 --- /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 000000000000..d26229edf71a --- /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 |