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

Commit 84f5a0e7 authored by Lucas Dupin's avatar Lucas Dupin
Browse files

Hook up media player resumption to UI

Fixes: 157926365
Fixes: 158691042
Test: atest MediaControlPanelTest
Test: atest MediaDataCombineLatestTest
Test: atest MediaDeviceManagerTest
Test: atest MediaTimeoutListenerTest
Test: with media playing:
      - got from keyguard to QS
      - got from keyguard to QQS
      - got from QQS to QS
      repeat after media timeout
Change-Id: I3f6ccd69b06c328ce528959b868eb194fbf4ca9e
parent e4c1cb51
Loading
Loading
Loading
Loading
+2 −29
Original line number Diff line number Diff line
@@ -96,13 +96,13 @@ public class MediaControlPanel {
     */
    @Inject
    public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
            ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager,
            ActivityStarter activityStarter, MediaViewController mediaViewController,
            SeekBarViewModel seekBarViewModel) {
        mContext = context;
        mBackgroundExecutor = backgroundExecutor;
        mActivityStarter = activityStarter;
        mSeekBarViewModel = seekBarViewModel;
        mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
        mMediaViewController = mediaViewController;
        loadDimens();
    }

@@ -362,14 +362,6 @@ public class MediaControlPanel {
        return artwork;
    }

    /**
     * Return the token for the current media session
     * @return the token
     */
    public MediaSession.Token getMediaSessionToken() {
        return mToken;
    }

    /**
     * Get the current media controller
     * @return the controller
@@ -378,25 +370,6 @@ public class MediaControlPanel {
        return mController;
    }

    /**
     * Get the name of the package associated with the current media controller
     * @return the package name, or null if no controller
     */
    public String getMediaPlayerPackage() {
        if (mController == null) {
            return null;
        }
        return mController.getPackageName();
    }

    /**
     * Check whether this player has an attached media session.
     * @return whether there is a controller with a current media session.
     */
    public boolean hasMediaSession() {
        return mController != null && mController.getPlaybackState() != null;
    }

    /**
     * Check whether the media controlled by this player is currently playing
     * @return whether it is playing, or false if no controller information
+47 −1
Original line number Diff line number Diff line
@@ -25,19 +25,65 @@ import android.media.session.MediaSession
data class MediaData(
    val initialized: Boolean = false,
    val backgroundColor: Int,
    /**
     * App name that will be displayed on the player.
     */
    val app: String?,
    /**
     * Icon shown on player, close to app name.
     */
    val appIcon: Drawable?,
    /**
     * Artist name.
     */
    val artist: CharSequence?,
    /**
     * Song name.
     */
    val song: CharSequence?,
    /**
     * Album artwork.
     */
    val artwork: Icon?,
    /**
     * List of actions that can be performed on the player: prev, next, play, pause, etc.
     */
    val actions: List<MediaAction>,
    /**
     * Same as above, but shown on smaller versions of the player, like in QQS or keyguard.
     */
    val actionsToShowInCompact: List<Int>,
    /**
     * Package name of the app that's posting the media.
     */
    val packageName: String,
    /**
     * Unique media session identifier.
     */
    val token: MediaSession.Token?,
    /**
     * Action to perform when the player is tapped.
     * This is unrelated to {@link #actions}.
     */
    val clickIntent: PendingIntent?,
    /**
     * Where the media is playing: phone, headphones, ear buds, remote session.
     */
    val device: MediaDeviceData?,
    /**
     * When active, a player will be displayed on keyguard and quick-quick settings.
     * This is unrelated to the stream being playing or not, a player will not be active if
     * timed out, or in resumption mode.
     */
    var active: Boolean,
    /**
     * Action that should be performed to restart a non active session.
     */
    var resumeAction: Runnable?,
    val notificationKey: String = "INVALID",
    /**
     * Notification key for cancelling a media player after a timeout (when not using resumption.)
     */
    val notificationKey: String? = null,
    var hasCheckedForResume: Boolean = false
)

+26 −25
Original line number Diff line number Diff line
@@ -67,7 +67,7 @@ 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,
        emptyList(), emptyList(), "INVALID", null, null, null, null)
        emptyList(), emptyList(), "INVALID", null, null, null, true, null)

fun isMediaNotification(sbn: StatusBarNotification): Boolean {
    if (!sbn.notification.hasMediaSession()) {
@@ -88,12 +88,12 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean {
class MediaDataManager @Inject constructor(
    private val context: Context,
    private val mediaControllerFactory: MediaControllerFactory,
    private val mediaTimeoutListener: MediaTimeoutListener,
    private val notificationEntryManager: NotificationEntryManager,
    private val mediaResumeListener: MediaResumeListener,
    @Background private val backgroundExecutor: Executor,
    @Main private val foregroundExecutor: Executor,
    private val broadcastDispatcher: BroadcastDispatcher
    broadcastDispatcher: BroadcastDispatcher,
    mediaTimeoutListener: MediaTimeoutListener,
    mediaResumeListener: MediaResumeListener
) {

    private val listeners: MutableSet<Listener> = mutableSetOf()
@@ -131,7 +131,6 @@ class MediaDataManager @Inject constructor(
        mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
            setTimedOut(token, timedOut) }
        addListener(mediaTimeoutListener)

        if (useMediaResumption) {
            mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
                resumeAction: Runnable, token: MediaSession.Token, appName: String,
@@ -215,7 +214,7 @@ class MediaDataManager @Inject constructor(
            mediaEntries.put(packageName, resumeData)
        }
        backgroundExecutor.execute {
            loadMediaDataInBg(desc, action, token, appName, appIntent, packageName)
            loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName)
        }
    }

@@ -255,16 +254,21 @@ class MediaDataManager @Inject constructor(
    fun removeListener(listener: Listener) = listeners.remove(listener)

    private fun setTimedOut(token: String, timedOut: Boolean) {
        if (!timedOut) {
        mediaEntries[token]?.let {
            if (Utils.useMediaResumption(context)) {
                if (it.active == !timedOut) {
                    return
                }
        mediaEntries[token]?.let {
                it.active = !timedOut
                onMediaDataLoaded(token, token, it)
            } else {
                notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */,
                        UNDEFINED_DISMISS_REASON)
            }
        }
    }

    private fun loadMediaDataInBg(
    private fun loadMediaDataInBgForResumption(
        desc: MediaDescription,
        resumeAction: Runnable,
        token: MediaSession.Token,
@@ -272,11 +276,6 @@ class MediaDataManager @Inject constructor(
        appIntent: PendingIntent,
        packageName: String
    ) {
        if (resumeAction == null) {
            Log.e(TAG, "Resume action cannot be null")
            return
        }

        if (TextUtils.isEmpty(desc.title)) {
            Log.e(TAG, "Description incomplete")
            return
@@ -299,7 +298,8 @@ class MediaDataManager @Inject constructor(
        foregroundExecutor.execute {
            onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName,
                    null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
                packageName, token, appIntent, null, resumeAction, packageName))
                    packageName, token, appIntent, device = null, active = false,
                    resumeAction = resumeAction))
        }
    }

@@ -430,7 +430,8 @@ class MediaDataManager @Inject constructor(
        foregroundExecutor.execute {
            onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
                    song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
                    notif.contentIntent, null, resumeAction, key))
                    notif.contentIntent, null, active = true, resumeAction = resumeAction,
                    notificationKey = key))
        }
    }

@@ -528,13 +529,13 @@ class MediaDataManager @Inject constructor(
    /**
     * Are there any media notifications active?
     */
    fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) })
    fun hasActiveMedia() = mediaEntries.any { it.value.active }

    fun isActive(data: MediaData): Boolean {
        if (data.token == null) {
    fun isActive(token: MediaSession.Token?): Boolean {
        if (token == null) {
            return false
        }
        val controller = mediaControllerFactory.create(data.token)
        val controller = mediaControllerFactory.create(token)
        val state = controller?.playbackState?.state
        return state != null && NotificationMediaManager.isActiveState(state)
    }
@@ -542,7 +543,7 @@ class MediaDataManager @Inject constructor(
    /**
     * Are there any media entries, including resume controls?
     */
    fun hasAnyMedia() = mediaEntries.isNotEmpty()
    fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()

    interface Listener {

+60 −6
Original line number Diff line number Diff line
package com.android.systemui.media

import android.graphics.PointF
import android.graphics.Rect
import android.view.View
import android.view.View.OnAttachStateChangeListener
@@ -20,8 +21,6 @@ class MediaHost @Inject constructor(
    var location: Int = -1
        private set
    var visibleChangedListener: ((Boolean) -> Unit)? = null
    var visible: Boolean = false
        private set

    private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)

@@ -109,16 +108,17 @@ class MediaHost @Inject constructor(
    }

    private fun updateViewVisibility() {
        if (showsOnlyActiveMedia) {
            visible = mediaDataManager.hasActiveMedia()
        visible = if (showsOnlyActiveMedia) {
            mediaDataManager.hasActiveMedia()
        } else {
            visible = mediaDataManager.hasAnyMedia()
            mediaDataManager.hasAnyMedia()
        }
        hostView.visibility = if (visible) View.VISIBLE else View.GONE
        visibleChangedListener?.invoke(visible)
    }

    class MediaHostStateHolder @Inject constructor() : MediaHostState {
        private var gonePivot: PointF = PointF()

        override var measurementInput: MeasurementInput? = null
            set(value) {
@@ -144,6 +144,25 @@ class MediaHost @Inject constructor(
                }
            }

        override var visible: Boolean = true
            set(value) {
                if (field == value) {
                    return
                }
                field = value
                changedListener?.invoke()
            }

        override fun getPivotX(): Float = gonePivot.x
        override fun getPivotY(): Float = gonePivot.y
        override fun setGonePivot(x: Float, y: Float) {
            if (gonePivot.equals(x, y)) {
                return
            }
            gonePivot.set(x, y)
            changedListener?.invoke()
        }

        /**
         * A listener for all changes. This won't be copied over when invoking [copy]
         */
@@ -157,6 +176,8 @@ class MediaHost @Inject constructor(
            mediaHostState.expansion = expansion
            mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
            mediaHostState.measurementInput = measurementInput?.copy()
            mediaHostState.visible = visible
            mediaHostState.gonePivot.set(gonePivot)
            return mediaHostState
        }

@@ -173,6 +194,12 @@ class MediaHost @Inject constructor(
            if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
                return false
            }
            if (visible != other.visible) {
                return false
            }
            if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
                return false
            }
            return true
        }

@@ -180,6 +207,8 @@ class MediaHost @Inject constructor(
            var result = measurementInput?.hashCode() ?: 0
            result = 31 * result + expansion.hashCode()
            result = 31 * result + showsOnlyActiveMedia.hashCode()
            result = 31 * result + if (visible) 1 else 2
            result = 31 * result + gonePivot.hashCode()
            return result
        }
    }
@@ -194,7 +223,8 @@ interface MediaHostState {
    var measurementInput: MeasurementInput?

    /**
     * The expansion of the player, 0 for fully collapsed, 1 for fully expanded
     * The expansion of the player, 0 for fully collapsed (up to 3 actions), 1 for fully expanded
     * (up to 5 actions.)
     */
    var expansion: Float

@@ -203,6 +233,30 @@ interface MediaHostState {
     */
    var showsOnlyActiveMedia: Boolean

    /**
     * If the view should be VISIBLE or GONE.
     */
    var visible: Boolean

    /**
     * Sets the pivot point when clipping the height or width.
     * Clipping happens when animating visibility when we're visible in QS but not on QQS,
     * for example.
     */
    fun setGonePivot(x: Float, y: Float)

    /**
     * x position of pivot, from 0 to 1
     * @see [setGonePivot]
     */
    fun getPivotX(): Float

    /**
     * y position of pivot, from 0 to 1
     * @see [setGonePivot]
     */
    fun getPivotY(): Float

    /**
     * Get a copy of this view state, deepcopying all appropriate members
     */
+7 −2
Original line number Diff line number Diff line
@@ -43,6 +43,11 @@ class MediaTimeoutListener @Inject constructor(

    private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()

    /**
     * Callback representing that a media object is now expired:
     * @param token Media session unique identifier
     * @param pauseTimeuot True when expired for {@code PAUSED_MEDIA_TIMEOUT}
     */
    lateinit var timeoutCallback: (String, Boolean) -> Unit

    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
@@ -112,11 +117,11 @@ class MediaTimeoutListener @Inject constructor(
            }
        }

        private fun expireMediaTimeout(mediaNotificationKey: String, reason: String) {
        private fun expireMediaTimeout(mediaKey: String, reason: String) {
            cancellation?.apply {
                if (DEBUG) {
                    Log.v(TAG,
                            "media timeout cancelled for  $mediaNotificationKey, reason: $reason")
                            "media timeout cancelled for  $mediaKey, reason: $reason")
                }
                run()
            }
Loading