Merge "Improve user handling when querying for resumable media" into sc-v2-dev

This commit is contained in:
Beth Thibodeau
2023-07-09 02:12:00 +00:00
committed by Android (Google) Code Review
5 changed files with 155 additions and 37 deletions

View File

@@ -97,9 +97,16 @@ class MediaResumeListener @Inject constructor(
Log.e(TAG, "Error getting package information", e) Log.e(TAG, "Error getting package information", e)
} }
Log.d(TAG, "Adding resume controls $desc") Log.d(TAG, "Adding resume controls for ${browser.userId}: $desc")
mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token, mediaDataManager.addResumptionControls(
appName.toString(), appIntent, component.packageName) browser.userId,
desc,
resumeAction,
token,
appName.toString(),
appIntent,
component.packageName
)
} }
} }
@@ -154,7 +161,11 @@ class MediaResumeListener @Inject constructor(
} }
resumeComponents.add(component to lastPlayed) resumeComponents.add(component to lastPlayed)
} }
Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") Log.d(
TAG,
"loaded resume components for $currentUserId: " +
"${resumeComponents.toArray().contentToString()}"
)
if (needsUpdate) { if (needsUpdate) {
// Save any missing times that we had to fill in // Save any missing times that we had to fill in
@@ -170,11 +181,21 @@ class MediaResumeListener @Inject constructor(
return return
} }
val pm = context.packageManager
val now = systemClock.currentTimeMillis() val now = systemClock.currentTimeMillis()
resumeComponents.forEach { resumeComponents.forEach {
if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) { if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first) // Verify that the service exists for this user
browser.findRecentMedia() val intent = Intent(MediaBrowserService.SERVICE_INTERFACE)
intent.component = it.first
val inf = pm.resolveServiceAsUser(intent, 0, currentUserId)
if (inf != null) {
val browser =
mediaBrowserFactory.create(mediaBrowserCallback, it.first, currentUserId)
browser.findRecentMedia()
} else {
Log.d(TAG, "User $currentUserId does not have component ${it.first}")
}
} }
} }
} }
@@ -199,7 +220,7 @@ class MediaResumeListener @Inject constructor(
Log.d(TAG, "Checking for service component for " + data.packageName) Log.d(TAG, "Checking for service component for " + data.packageName)
val pm = context.packageManager val pm = context.packageManager
val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
val resumeInfo = pm.queryIntentServices(serviceIntent, 0) val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)
val inf = resumeInfo?.filter { val inf = resumeInfo?.filter {
it.serviceInfo.packageName == data.packageName it.serviceInfo.packageName == data.packageName
@@ -242,13 +263,18 @@ class MediaResumeListener @Inject constructor(
browser: ResumeMediaBrowser browser: ResumeMediaBrowser
) { ) {
// Since this is a test, just save the component for later // Since this is a test, just save the component for later
Log.d(TAG, "Can get resumable media from $componentName") Log.d(
TAG,
"Can get resumable media for ${browser.userId} from $componentName"
)
mediaDataManager.setResumeAction(key, getResumeAction(componentName)) mediaDataManager.setResumeAction(key, getResumeAction(componentName))
updateResumptionList(componentName) updateResumptionList(componentName)
mediaBrowser = null mediaBrowser = null
} }
}, },
componentName) componentName,
currentUserId
)
mediaBrowser?.testConnection() mediaBrowser?.testConnection()
} }
@@ -288,7 +314,7 @@ class MediaResumeListener @Inject constructor(
*/ */
private fun getResumeAction(componentName: ComponentName): Runnable { private fun getResumeAction(componentName: ComponentName): Runnable {
return Runnable { return Runnable {
mediaBrowser = mediaBrowserFactory.create(null, componentName) mediaBrowser = mediaBrowserFactory.create(null, componentName, currentUserId)
mediaBrowser?.restart() mediaBrowser?.restart()
} }
} }

View File

@@ -17,6 +17,7 @@
package com.android.systemui.media; package com.android.systemui.media;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -50,6 +51,8 @@ public class ResumeMediaBrowser {
private final Context mContext; private final Context mContext;
@Nullable private final Callback mCallback; @Nullable private final Callback mCallback;
private MediaBrowserFactory mBrowserFactory; private MediaBrowserFactory mBrowserFactory;
@UserIdInt private final int mUserId;
private MediaBrowser mMediaBrowser; private MediaBrowser mMediaBrowser;
private ComponentName mComponentName; private ComponentName mComponentName;
@@ -58,13 +61,19 @@ public class ResumeMediaBrowser {
* @param context the context * @param context the context
* @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
* @param userId ID of the current user
*/ */
public ResumeMediaBrowser(Context context, @Nullable Callback callback, public ResumeMediaBrowser(
ComponentName componentName, MediaBrowserFactory browserFactory) { Context context,
@Nullable Callback callback,
ComponentName componentName,
MediaBrowserFactory browserFactory,
@UserIdInt int userId) {
mContext = context; mContext = context;
mCallback = callback; mCallback = callback;
mComponentName = componentName; mComponentName = componentName;
mBrowserFactory = browserFactory; mBrowserFactory = browserFactory;
mUserId = userId;
} }
/** /**
@@ -259,6 +268,14 @@ public class ResumeMediaBrowser {
return new MediaController(mContext, token); return new MediaController(mContext, token);
} }
/**
* Get the ID of the user associated with this broswer
* @return the user ID
*/
public @UserIdInt int getUserId() {
return mUserId;
}
/** /**
* 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

View File

@@ -16,6 +16,7 @@
package com.android.systemui.media; package com.android.systemui.media;
import android.annotation.UserIdInt;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -39,10 +40,12 @@ public class ResumeMediaBrowserFactory {
* *
* @param callback will be called on connection or error, and addTrack when media item found * @param callback will be called on connection or error, and addTrack when media item found
* @param componentName component to browse * @param componentName component to browse
* @param userId ID of the current user
* @return * @return
*/ */
public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback, public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
ComponentName componentName) { ComponentName componentName, @UserIdInt int userId) {
return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory); return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory,
userId);
} }
} }

View File

@@ -92,6 +92,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
@Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback> @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
@Captor lateinit var actionCaptor: ArgumentCaptor<Runnable> @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
@Captor lateinit var componentCaptor: ArgumentCaptor<String> @Captor lateinit var componentCaptor: ArgumentCaptor<String>
@Captor lateinit var userIdCaptor: ArgumentCaptor<Int>
private lateinit var executor: FakeExecutor private lateinit var executor: FakeExecutor
private lateinit var data: MediaData private lateinit var data: MediaData
@@ -112,7 +113,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
Settings.Secure.putInt(context.contentResolver, Settings.Secure.putInt(context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME, 1) Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
whenever(resumeBrowserFactory.create(capture(callbackCaptor), any())) whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), capture(userIdCaptor)))
.thenReturn(resumeBrowser) .thenReturn(resumeBrowser)
// resume components are stored in sharedpreferences // resume components are stored in sharedpreferences
@@ -123,6 +124,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor) whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
whenever(mockContext.packageManager).thenReturn(context.packageManager) whenever(mockContext.packageManager).thenReturn(context.packageManager)
whenever(mockContext.contentResolver).thenReturn(context.contentResolver) whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
whenever(mockContext.userId).thenReturn(context.userId)
executor = FakeExecutor(clock) executor = FakeExecutor(clock)
resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
@@ -233,15 +235,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
@Test @Test
fun testOnLoad_checksForResume_hasService() { fun testOnLoad_checksForResume_hasService() {
// Set up mocks to successfully find a MBS that returns valid media // Set up mocks to successfully find a MBS that returns valid media
val pm = mock(PackageManager::class.java) setUpMbsWithValidResolveInfo()
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 description = MediaDescription.Builder().setTitle(TITLE).build()
val component = ComponentName(PACKAGE_NAME, CLASS_NAME) val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
@@ -280,6 +274,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
@Test @Test
fun testOnUserUnlock_loadsTracks() { fun testOnUserUnlock_loadsTracks() {
// Set up mock service to successfully find valid media // Set up mock service to successfully find valid media
setUpMbsWithValidResolveInfo()
val description = MediaDescription.Builder().setTitle(TITLE).build() val description = MediaDescription.Builder().setTitle(TITLE).build()
val component = ComponentName(PACKAGE_NAME, CLASS_NAME) val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
whenever(resumeBrowser.token).thenReturn(token) whenever(resumeBrowser.token).thenReturn(token)
@@ -308,15 +303,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
@Test @Test
fun testGetResumeAction_restarts() { fun testGetResumeAction_restarts() {
// Set up mocks to successfully find a MBS that returns valid media // Set up mocks to successfully find a MBS that returns valid media
val pm = mock(PackageManager::class.java) setUpMbsWithValidResolveInfo()
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 description = MediaDescription.Builder().setTitle(TITLE).build()
val component = ComponentName(PACKAGE_NAME, CLASS_NAME) val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
@@ -362,6 +349,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
@Test @Test
fun testLoadComponents_recentlyPlayed_adds() { fun testLoadComponents_recentlyPlayed_adds() {
// Set up browser to return successfully // Set up browser to return successfully
setUpMbsWithValidResolveInfo()
val description = MediaDescription.Builder().setTitle(TITLE).build() val description = MediaDescription.Builder().setTitle(TITLE).build()
val component = ComponentName(PACKAGE_NAME, CLASS_NAME) val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
whenever(resumeBrowser.token).thenReturn(token) whenever(resumeBrowser.token).thenReturn(token)
@@ -445,4 +433,81 @@ class MediaResumeListenerTest : SysuiTestCase() {
} }
verify(sharedPrefsEditor, times(1)).apply() verify(sharedPrefsEditor, times(1)).apply()
} }
@Test
fun testUserUnlocked_userChangeWhileQuerying() {
val firstUserId = context.userId
val secondUserId = firstUserId + 1
val description = MediaDescription.Builder().setTitle(TITLE).build()
val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
setUpMbsWithValidResolveInfo()
whenever(resumeBrowser.token).thenReturn(token)
whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
val unlockIntent =
Intent(Intent.ACTION_USER_UNLOCKED).apply {
putExtra(Intent.EXTRA_USER_HANDLE, firstUserId)
}
// When the first user unlocks and we query their recent media
resumeListener.userChangeReceiver.onReceive(context, unlockIntent)
whenever(resumeBrowser.userId).thenReturn(userIdCaptor.value)
verify(resumeBrowser, times(3)).findRecentMedia()
// And the user changes before the MBS response is received
val changeIntent =
Intent(Intent.ACTION_USER_SWITCHED).apply {
putExtra(Intent.EXTRA_USER_HANDLE, secondUserId)
}
resumeListener.userChangeReceiver.onReceive(context, changeIntent)
callbackCaptor.value.addTrack(description, component, resumeBrowser)
// Then the loaded media is correctly associated with the first user
verify(mediaDataManager)
.addResumptionControls(
eq(firstUserId),
eq(description),
any(),
eq(token),
eq(PACKAGE_NAME),
eq(pendingIntent),
eq(PACKAGE_NAME)
)
}
@Test
fun testUserUnlocked_noComponent_doesNotQuery() {
// Set up a valid MBS, but user does not have the service available
setUpMbsWithValidResolveInfo()
val pm = mock(PackageManager::class.java)
whenever(mockContext.packageManager).thenReturn(pm)
whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(null)
val unlockIntent =
Intent(Intent.ACTION_USER_UNLOCKED).apply {
putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
}
// When the user is unlocked, but does not have the component installed
resumeListener.userChangeReceiver.onReceive(context, unlockIntent)
// Then we never attempt to connect to it
verify(resumeBrowser, never()).findRecentMedia()
}
/** Sets up mocks to successfully find a MBS that returns valid media. */
private fun setUpMbsWithValidResolveInfo() {
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.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(resumeInfo)
whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(resolveInfo)
whenever(pm.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
}
} }

View File

@@ -81,8 +81,14 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
whenever(mediaController.transportControls).thenReturn(transportControls) whenever(mediaController.transportControls).thenReturn(transportControls)
resumeBrowser = TestableResumeMediaBrowser(context, callback, component, browserFactory, resumeBrowser = TestableResumeMediaBrowser(
mediaController) context,
callback,
component,
browserFactory,
mediaController,
context.userId
)
} }
@Test @Test
@@ -282,8 +288,9 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
callback: Callback, callback: Callback,
componentName: ComponentName, componentName: ComponentName,
browserFactory: MediaBrowserFactory, browserFactory: MediaBrowserFactory,
private val fakeController: MediaController private val fakeController: MediaController,
) : ResumeMediaBrowser(context, callback, componentName, browserFactory) { userId: Int
) : ResumeMediaBrowser(context, callback, componentName, browserFactory, userId) {
override fun createMediaController(token: MediaSession.Token): MediaController { override fun createMediaController(token: MediaSession.Token): MediaController {
return fakeController return fakeController