Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b7e77454 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Improve user handling when querying for resumable media

- Before trying to query recent media from a saved component, check
  whether the current user actually has that component installed
- Track user when creating the MediaBrowser, in case the user changes
  before the MBS returns a result

Test: atest MediaResumeListenerTest
Bug: 284297711
Change-Id: I838ff0e125acadabc8436a00dbff707cc4be6249
Merged-In: I838ff0e125acadabc8436a00dbff707cc4be6249
(cherry picked from commit e566a250)
parent cbed626a
Loading
Loading
Loading
Loading
+36 −10
Original line number Diff line number Diff line
@@ -97,9 +97,16 @@ class MediaResumeListener @Inject constructor(
                Log.e(TAG, "Error getting package information", e)
            }

            Log.d(TAG, "Adding resume controls $desc")
            mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token,
                appName.toString(), appIntent, component.packageName)
            Log.d(TAG, "Adding resume controls for ${browser.userId}: $desc")
            mediaDataManager.addResumptionControls(
                browser.userId,
                desc,
                resumeAction,
                token,
                appName.toString(),
                appIntent,
                component.packageName
            )
        }
    }

@@ -154,7 +161,11 @@ class MediaResumeListener @Inject constructor(
            }
            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) {
            // Save any missing times that we had to fill in
@@ -170,11 +181,21 @@ class MediaResumeListener @Inject constructor(
            return
        }

        val pm = context.packageManager
        val now = systemClock.currentTimeMillis()
        resumeComponents.forEach {
            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()
                } 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)
                val pm = context.packageManager
                val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
                val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
                val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)

                val inf = resumeInfo?.filter {
                    it.serviceInfo.packageName == data.packageName
@@ -242,13 +263,18 @@ class MediaResumeListener @Inject constructor(
                        browser: ResumeMediaBrowser
                    ) {
                        // 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))
                        updateResumptionList(componentName)
                        mediaBrowser = null
                    }
                },
                componentName)
                componentName,
                currentUserId
            )
        mediaBrowser?.testConnection()
    }

@@ -288,7 +314,7 @@ class MediaResumeListener @Inject constructor(
     */
    private fun getResumeAction(componentName: ComponentName): Runnable {
        return Runnable {
            mediaBrowser = mediaBrowserFactory.create(null, componentName)
            mediaBrowser = mediaBrowserFactory.create(null, componentName, currentUserId)
            mediaBrowser?.restart()
        }
    }
+19 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.media;

import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
@@ -50,6 +51,8 @@ public class ResumeMediaBrowser {
    private final Context mContext;
    @Nullable private final Callback mCallback;
    private MediaBrowserFactory mBrowserFactory;
    @UserIdInt private final int mUserId;

    private MediaBrowser mMediaBrowser;
    private ComponentName mComponentName;

@@ -58,13 +61,19 @@ public class ResumeMediaBrowser {
     * @param context the context
     * @param callback used to report media items found
     * @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,
            ComponentName componentName, MediaBrowserFactory browserFactory) {
    public ResumeMediaBrowser(
            Context context,
            @Nullable Callback callback,
            ComponentName componentName,
            MediaBrowserFactory browserFactory,
            @UserIdInt int userId) {
        mContext = context;
        mCallback = callback;
        mComponentName = componentName;
        mBrowserFactory = browserFactory;
        mUserId = userId;
    }

    /**
@@ -259,6 +268,14 @@ public class ResumeMediaBrowser {
        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
     * @return the token, or null if the MediaBrowser is null or disconnected
+5 −2
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.media;

import android.annotation.UserIdInt;
import android.content.ComponentName;
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 componentName component to browse
     * @param userId ID of the current user
     * @return
     */
    public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
            ComponentName componentName) {
        return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory);
            ComponentName componentName, @UserIdInt int userId) {
        return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory,
            userId);
    }
}
+84 −19
Original line number Diff line number Diff line
@@ -92,6 +92,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
    @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
    @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
    @Captor lateinit var componentCaptor: ArgumentCaptor<String>
    @Captor lateinit var userIdCaptor: ArgumentCaptor<Int>

    private lateinit var executor: FakeExecutor
    private lateinit var data: MediaData
@@ -112,7 +113,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
        Settings.Secure.putInt(context.contentResolver,
            Settings.Secure.MEDIA_CONTROLS_RESUME, 1)

        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), capture(userIdCaptor)))
                .thenReturn(resumeBrowser)

        // resume components are stored in sharedpreferences
@@ -123,6 +124,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
        whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
        whenever(mockContext.packageManager).thenReturn(context.packageManager)
        whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
        whenever(mockContext.userId).thenReturn(context.userId)

        executor = FakeExecutor(clock)
        resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
@@ -233,15 +235,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
    @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)
        setUpMbsWithValidResolveInfo()

        val description = MediaDescription.Builder().setTitle(TITLE).build()
        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
@@ -280,6 +274,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
    @Test
    fun testOnUserUnlock_loadsTracks() {
        // Set up mock service to successfully find valid media
        setUpMbsWithValidResolveInfo()
        val description = MediaDescription.Builder().setTitle(TITLE).build()
        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
        whenever(resumeBrowser.token).thenReturn(token)
@@ -308,15 +303,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
    @Test
    fun testGetResumeAction_restarts() {
        // 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)
        setUpMbsWithValidResolveInfo()

        val description = MediaDescription.Builder().setTitle(TITLE).build()
        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
@@ -362,6 +349,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
    @Test
    fun testLoadComponents_recentlyPlayed_adds() {
        // Set up browser to return successfully
        setUpMbsWithValidResolveInfo()
        val description = MediaDescription.Builder().setTitle(TITLE).build()
        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
        whenever(resumeBrowser.token).thenReturn(token)
@@ -445,4 +433,81 @@ class MediaResumeListenerTest : SysuiTestCase() {
                }
        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)
    }
}
 No newline at end of file
+11 −4
Original line number Diff line number Diff line
@@ -81,8 +81,14 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {

        whenever(mediaController.transportControls).thenReturn(transportControls)

        resumeBrowser = TestableResumeMediaBrowser(context, callback, component, browserFactory,
                mediaController)
        resumeBrowser = TestableResumeMediaBrowser(
            context,
            callback,
            component,
            browserFactory,
            mediaController,
            context.userId
        )
    }

    @Test
@@ -282,8 +288,9 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
        callback: Callback,
        componentName: ComponentName,
        browserFactory: MediaBrowserFactory,
        private val fakeController: MediaController
    ) : ResumeMediaBrowser(context, callback, componentName, browserFactory) {
        private val fakeController: MediaController,
        userId: Int
    ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, userId) {

        override fun createMediaController(token: MediaSession.Token): MediaController {
            return fakeController