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

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

Filter media controls by user

Adds a new step MediaDataFilter in the pipeline, after MediaDataCombineLatest,
which will only pass on media control loaded / removed events if they are for
the current user

If the user is switched, removes all existing controls for the old user and
then adds back any old controls for the new user

Fixes: 159958740
Test: atest com.android.systemui.media
Test: manual, switch users and observe controls updated correctly
Change-Id: Ia07180432df1ce031c42a6225d52160f83ef677e
parent bba5d59a
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -39,9 +39,8 @@ class MediaCarouselController @Inject constructor(
    private val mediaHostStatesManager: MediaHostStatesManager,
    private val activityStarter: ActivityStarter,
    @Main executor: DelayableExecutor,
    mediaManager: MediaDataCombineLatest,
    mediaManager: MediaDataFilter,
    configurationController: ConfigurationController,
    mediaDataManager: MediaDataManager,
    falsingManager: FalsingManager
) {
    /**
@@ -148,7 +147,7 @@ class MediaCarouselController @Inject constructor(
        mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
        pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
        mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
                executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
                executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
                falsingManager)
        isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
        inflateSettingsButton()
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.media.session.MediaSession

/** State of a media view. */
data class MediaData(
    val userId: Int,
    val initialized: Boolean = false,
    val backgroundColor: Int,
    /**
+11 −0
Original line number Diff line number Diff line
@@ -57,6 +57,17 @@ class MediaDataCombineLatest @Inject constructor(
        })
    }

    /**
     * Get a map of all non-null data entries
     */
    fun getData(): Map<String, MediaData> {
        return entries.filter {
            (key, pair) -> pair.first != null
        }.mapValues {
            (key, pair) -> pair.first!!
        }
    }

    /**
     * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData].
     */
+153 −0
Original line number Diff line number Diff line
/*
 * 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.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.CurrentUserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton

private const val TAG = "MediaDataFilter"

/**
 * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
 * switches (removing entries for the previous user, adding back entries for the current user)
 *
 * This is added downstream of [MediaDataManager] since we may still need to handle callbacks from
 * background users (e.g. timeouts) that UI classes should ignore.
 * Instead, UI classes should listen to this so they can stay in sync with the current user.
 */
@Singleton
class MediaDataFilter @Inject constructor(
    private val dataSource: MediaDataCombineLatest,
    private val broadcastDispatcher: BroadcastDispatcher,
    private val mediaResumeListener: MediaResumeListener,
    private val mediaDataManager: MediaDataManager,
    private val lockscreenUserManager: NotificationLockscreenUserManager,
    @Main private val executor: Executor
) : MediaDataManager.Listener {
    private val userTracker: CurrentUserTracker
    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()

    // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager
    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()

    init {
        userTracker = object : CurrentUserTracker(broadcastDispatcher) {
            override fun onUserSwitched(newUserId: Int) {
                // Post this so we can be sure lockscreenUserManager already got the broadcast
                executor.execute { handleUserSwitched(newUserId) }
            }
        }
        userTracker.startTracking()
        dataSource.addListener(this)
    }

    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
        if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
            return
        }

        if (oldKey != null) {
            mediaEntries.remove(oldKey)
        }
        mediaEntries.put(key, data)

        // Notify listeners
        val listenersCopy = listeners.toSet()
        listenersCopy.forEach {
            it.onMediaDataLoaded(key, oldKey, data)
        }
    }

    override fun onMediaDataRemoved(key: String) {
        mediaEntries.remove(key)?.let {
            // Only notify listeners if something actually changed
            val listenersCopy = listeners.toSet()
            listenersCopy.forEach {
                it.onMediaDataRemoved(key)
            }
        }
    }

    @VisibleForTesting
    internal fun handleUserSwitched(id: Int) {
        // If the user changes, remove all current MediaData objects and inform listeners
        val listenersCopy = listeners.toSet()
        val keyCopy = mediaEntries.keys.toMutableList()
        // Clear the list first, to make sure callbacks from listeners if we have any entries
        // are up to date
        mediaEntries.clear()
        keyCopy.forEach {
            Log.d(TAG, "Removing $it after user change")
            listenersCopy.forEach { listener ->
                listener.onMediaDataRemoved(it)
            }
        }

        dataSource.getData().forEach { (key, data) ->
            if (lockscreenUserManager.isCurrentProfile(data.userId)) {
                Log.d(TAG, "Re-adding $key after user change")
                mediaEntries.put(key, data)
                listenersCopy.forEach { listener ->
                    listener.onMediaDataLoaded(key, null, data)
                }
            }
        }
    }

    /**
     * Invoked when the user has dismissed the media carousel
     */
    fun onSwipeToDismiss() {
        val mediaKeys = mediaEntries.keys.toSet()
        mediaKeys.forEach {
            mediaDataManager.setTimedOut(it, timedOut = true)
        }
    }

    /**
     * Are there any media notifications active?
     */
    fun hasActiveMedia() = mediaEntries.any { it.value.active }

    /**
     * Are there any media entries we should display?
     * If resumption is enabled, this will include inactive players
     * If resumption is disabled, we only want to show active players
     */
    fun hasAnyMedia() = if (mediaResumeListener.isResumptionEnabled()) {
        mediaEntries.isNotEmpty()
    } else {
        hasActiveMedia()
    }

    /**
     * Add a listener for filtered [MediaData] changes
     */
    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)

    /**
     * Remove a listener that was registered with addListener
     */
    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
}
 No newline at end of file
+13 −58
Original line number Diff line number Diff line
@@ -67,7 +67,7 @@ private const val DEFAULT_LUMINOSITY = 0.25f
private const val LUMINOSITY_THRESHOLD = 0.05f
private const val SATURATION_MULTIPLIER = 0.8f

private val LOADING = MediaData(false, 0, null, null, null, null, null,
private val LOADING = MediaData(-1, false, 0, null, null, null, null, null,
        emptyList(), emptyList(), "INVALID", null, null, null, true, null)

fun isMediaNotification(sbn: StatusBarNotification): Boolean {
@@ -116,15 +116,6 @@ class MediaDataManager(
            broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
            Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))

    private val userChangeReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (Intent.ACTION_USER_SWITCHED == intent.action) {
                // Remove all controls, regardless of state
                clearData()
            }
        }
    }

    private val appChangeReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
@@ -152,9 +143,6 @@ class MediaDataManager(
        mediaResumeListener.setManager(this)
        addListener(mediaResumeListener)

        val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED)
        broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL)

        val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
        broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)

@@ -169,7 +157,6 @@ class MediaDataManager(

    fun destroy() {
        context.unregisterReceiver(appChangeReceiver)
        broadcastDispatcher.unregisterReceiver(userChangeReceiver)
    }

    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
@@ -190,20 +177,6 @@ class MediaDataManager(
        }
    }

    private fun clearData() {
        // Called on user change. Remove all current MediaData objects and inform listeners
        val listenersCopy = listeners.toSet()
        val keyCopy = mediaEntries.keys.toMutableList()
        // Clear the list first, to make sure callbacks from listeners if we have any entries
        // are up to date
        mediaEntries.clear()
        keyCopy.forEach {
            listenersCopy.forEach { listener ->
                listener.onMediaDataRemoved(it)
            }
        }
    }

    private fun removeAllForPackage(packageName: String) {
        Assert.isMainThread()
        val listenersCopy = listeners.toSet()
@@ -224,6 +197,7 @@ class MediaDataManager(
    }

    fun addResumptionControls(
        userId: Int,
        desc: MediaDescription,
        action: Runnable,
        token: MediaSession.Token,
@@ -238,7 +212,8 @@ class MediaDataManager(
            mediaEntries.put(packageName, resumeData)
        }
        backgroundExecutor.execute {
            loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName)
            loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent,
                packageName)
        }
    }

@@ -282,7 +257,7 @@ class MediaDataManager(
     * This will make the player not active anymore, hiding it from QQS and Keyguard.
     * @see MediaData.active
     */
    private fun setTimedOut(token: String, timedOut: Boolean) {
    internal fun setTimedOut(token: String, timedOut: Boolean) {
        mediaEntries[token]?.let {
            if (it.active == !timedOut) {
                return
@@ -293,6 +268,7 @@ class MediaDataManager(
    }

    private fun loadMediaDataInBgForResumption(
        userId: Int,
        desc: MediaDescription,
        resumeAction: Runnable,
        token: MediaSession.Token,
@@ -307,7 +283,7 @@ class MediaDataManager(
            return
        }

        Log.d(TAG, "adding track from browser: $desc")
        Log.d(TAG, "adding track for $userId from browser: $desc")

        // Album art
        var artworkBitmap = desc.iconBitmap
@@ -323,7 +299,7 @@ class MediaDataManager(

        val mediaAction = getResumeMediaAction(resumeAction)
        foregroundExecutor.execute {
            onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName,
            onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName,
                    null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
                    packageName, token, appIntent, device = null, active = false,
                    resumeAction = resumeAction, resumption = true, notificationKey = packageName,
@@ -439,10 +415,11 @@ class MediaDataManager(
            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
            val active = mediaEntries[key]?.active ?: true
            onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
                    song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
                    notif.contentIntent, null, active, resumeAction = resumeAction,
                    notificationKey = key, hasCheckedForResume = hasCheckedForResume))
            onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
                    smallIconDrawable, artist, song, artWorkIcon, actionIcons,
                    actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
                    active, resumeAction = resumeAction, notificationKey = key,
                    hasCheckedForResume = hasCheckedForResume))
        }
    }

@@ -564,18 +541,6 @@ class MediaDataManager(
        }
    }

    /**
     * Are there any media notifications active?
     */
    fun hasActiveMedia() = mediaEntries.any { it.value.active }

    /**
     * Are there any media entries we should display?
     * If resumption is enabled, this will include inactive players
     * If resumption is disabled, we only want to show active players
     */
    fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()

    fun setMediaResumptionEnabled(isEnabled: Boolean) {
        if (useMediaResumption == isEnabled) {
            return
@@ -596,16 +561,6 @@ class MediaDataManager(
        }
    }

    /**
     * Invoked when the user has dismissed the media carousel
     */
    fun onSwipeToDismiss() {
        val mediaKeys = mediaEntries.keys.toSet()
        mediaKeys.forEach {
            setTimedOut(it, timedOut = true)
        }
    }

    interface Listener {

        /**
Loading