Merge "Improve user handling when querying for resumable media" into sc-qpr1-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
3554a4a7ed
@@ -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
|
||||||
|
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()
|
browser.findRecentMedia()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "User $currentUserId does not have component ${it.first}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +219,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
|
||||||
@@ -241,13 +262,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +313,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -223,15 +225,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)
|
||||||
@@ -270,6 +264,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)
|
||||||
@@ -298,15 +293,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)
|
||||||
@@ -352,6 +339,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)
|
||||||
@@ -435,4 +423,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user