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

Commit 38c6f2bf authored by cecilia's avatar cecilia Committed by Cecilia Hong
Browse files

On Smartspace removal update, only dismiss media recommendation/player

when it's invisible to users.

1. Add the param "immediately" to #onMediaDataLoaded and
on the underlying Smartspace data's removal;
2. #onSmartspaceMediaDataRemoved should only set "immediately" to true
on the user's interaction (e.g. Swipe-to-dismiss);
3. Add #SmartspaceMediaData to preserve the information for a
recommendation's view.
    - This data class is to introduce the field - "isActive", which will be set to false when a) The underlying
Smartspace data is removed; b) The Smartspace rec card is dismissed by
the user.
    - This appraoch is used instead of marking the SmartspaceTarget
instance to be null: the instance needs to be kept around to block the
duplicate Smartspace updates.

Fixes: 187219674
Fixes: 185787575
Test: Local builds
Change-Id: I19c398fa617749a9e643db33865fd2c5f8f9bdc4
parent 9787f776
Loading
Loading
Loading
Loading
+48 −21
Original line number Diff line number Diff line
package com.android.systemui.media

import android.app.smartspace.SmartspaceTarget
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
@@ -184,7 +183,12 @@ class MediaCarouselController @Inject constructor(
        visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
                true /* persistent */)
        mediaManager.addListener(object : MediaDataManager.Listener {
            override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
            override fun onMediaDataLoaded(
                key: String,
                oldKey: String?,
                data: MediaData,
                immediately: Boolean
            ) {
                if (addOrUpdatePlayer(key, oldKey, data)) {
                    MediaPlayerData.getMediaPlayer(key, null)?.let {
                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
@@ -210,10 +214,11 @@ class MediaCarouselController @Inject constructor(

            override fun onSmartspaceMediaDataLoaded(
                key: String,
                data: SmartspaceTarget,
                data: SmartspaceMediaData,
                shouldPrioritize: Boolean
            ) {
                Log.d(TAG, "My Smartspace media update is here")
                if (data.isActive) {
                    addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
                    MediaPlayerData.getMediaPlayer(key, null)?.let {
                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
@@ -224,15 +229,22 @@ class MediaCarouselController @Inject constructor(
                    if (mediaCarouselScrollHandler.visibleToUser) {
                        logSmartspaceImpression()
                    }
                } else {
                    onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
                }
            }

            override fun onMediaDataRemoved(key: String) {
                removePlayer(key)
            }

            override fun onSmartspaceMediaDataRemoved(key: String) {
            override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
                Log.d(TAG, "My Smartspace media removal request is received")
                removePlayer(key)
                if (immediately || visualStabilityManager.isReorderingAllowed) {
                    onMediaDataRemoved(key)
                } else {
                    keysNeedRemoval.add(key)
                }
            }
        })
        mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
@@ -287,7 +299,7 @@ class MediaCarouselController @Inject constructor(
        // Automatically scroll to the active player if needed
        if (shouldScrollToActivePlayer) {
            shouldScrollToActivePlayer = false
            val activeMediaIndex = MediaPlayerData.getActiveMediaIndex()
            val activeMediaIndex = MediaPlayerData.activeMediaIndex()
            if (activeMediaIndex != -1) {
                mediaCarouselScrollHandler.scrollToActivePlayer(activeMediaIndex)
            }
@@ -333,7 +345,7 @@ class MediaCarouselController @Inject constructor(

    private fun addSmartspaceMediaRecommendations(
        key: String,
        data: SmartspaceTarget,
        data: SmartspaceMediaData,
        shouldPrioritize: Boolean
    ) {
        Log.d(TAG, "Updating smartspace target in carousel")
@@ -342,6 +354,11 @@ class MediaCarouselController @Inject constructor(
            return
        }

        val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
        existingSmartspaceMediaKey?.let {
            MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey)
        }

        var newRecs = mediaControlPanelFactory.get()
        newRecs.attachRecommendation(
            RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent))
@@ -349,7 +366,7 @@ class MediaCarouselController @Inject constructor(
        val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT)
        newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
        newRecs.bindRecommendation(data, bgColor)
        newRecs.bindRecommendation(data.copy(backgroundColor = bgColor))
        MediaPlayerData.addMediaRecommendation(key, newRecs, shouldPrioritize)
        updatePlayerToState(newRecs, noAnimation = true)
        reorderAllPlayers()
@@ -378,11 +395,11 @@ class MediaCarouselController @Inject constructor(

            if (dismissMediaData) {
                // Inform the media manager of a potentially late dismissal
                mediaManager.dismissMediaData(key, 0L /* delaye */)
                mediaManager.dismissMediaData(key, delay = 0L)
            }
            if (dismissRecommendation) {
                // Inform the media manager of a potentially late dismissal
                mediaManager.dismissSmartspaceRecommendation(0L /* delay */)
                mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
            }
        }
    }
@@ -392,7 +409,7 @@ class MediaCarouselController @Inject constructor(
        pageIndicator.tintList = ColorStateList.valueOf(getForegroundColor())

        MediaPlayerData.mediaData().forEach { (key, data) ->
            removePlayer(key, dismissMediaData = false)
            removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
            addOrUpdatePlayer(key = key, oldKey = null, data = data)
        }
    }
@@ -732,7 +749,7 @@ internal object MediaPlayerData {
    fun players() = mediaPlayers.values

    /** Returns the index of the first non-timeout media. */
    fun getActiveMediaIndex(): Int {
    fun activeMediaIndex(): Int {
        mediaPlayers.entries.forEachIndexed { index, e ->
            if (!e.key.isSsMediaRec && e.key.data.active) {
                return index
@@ -741,6 +758,16 @@ internal object MediaPlayerData {
        return -1
    }

    /** Returns the existing Smartspace target id. */
    fun smartspaceMediaKey(): String? {
        mediaData.entries.forEach { e ->
            if (e.value.isSsMediaRec) {
                return e.key
            }
        }
        return null
    }

    fun playerKeys() = mediaPlayers.keys

    @VisibleForTesting
+1 −3
Original line number Diff line number Diff line
@@ -560,12 +560,10 @@ class MediaCarouselScrollHandler(
    }

    fun scrollToActivePlayer(activePlayerIndex: Int) {
        var destIndex = activePlayerIndex
        destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
        val destIndex = Math.min(mediaContent.getChildCount() - 1, activePlayerIndex)
        val view = mediaContent.getChildAt(destIndex)
        // We need to post this to wait for the active player becomes visible.
        mainExecutor.executeDelayed({
            visibleMediaIndex = activePlayerIndex
            scrollView.smoothScrollTo(view.left, scrollView.scrollY)
        }, SCROLL_DELAY)
    }
+14 −46
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;

import android.app.PendingIntent;
import android.app.smartspace.SmartspaceAction;
import android.app.smartspace.SmartspaceTarget;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -34,7 +33,6 @@ import android.graphics.drawable.Icon;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.text.Layout;
import android.util.Log;
import android.view.View;
@@ -74,7 +72,6 @@ import kotlin.Unit;
public class MediaControlPanel {
    private static final String TAG = "MediaControlPanel";
    private static final float DISABLED_ALPHA = 0.38f;
    private static final String EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name";
    private static final String EXTRAS_SMARTSPACE_INTENT =
            "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT";
    private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
@@ -493,27 +490,30 @@ public class MediaControlPanel {
        };
    }

    /** Bind this recommendation view based on the data given. */
    public void bindRecommendation(@NonNull SmartspaceTarget target, @NonNull int backgroundColor) {
    /** Bind this recommendation view based on the given data. */
    public void bindRecommendation(@NonNull SmartspaceMediaData data) {
        if (mRecommendationViewHolder == null) {
            return;
        }

        mInstanceId = target.getSmartspaceTargetId().hashCode();
        mInstanceId = data.getTargetId().hashCode();
        mBackgroundColor = data.getBackgroundColor();
        mRecommendationViewHolder.getRecommendations()
                .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
        mBackgroundColor = backgroundColor;
                .setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));

        List<SmartspaceAction> mediaRecommendationList = target.getIconGrid();
        List<SmartspaceAction> mediaRecommendationList = data.getRecommendations();
        if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) {
            Log.w(TAG, "Empty media recommendations");
            return;
        }

        // Set up recommendation card's header.
        ApplicationInfo applicationInfo = getApplicationInfo(target);
        if (applicationInfo == null) {
            Log.w(TAG, "No valid application info is found for media recommendations");
        ApplicationInfo applicationInfo = null;
        try {
            applicationInfo = mContext.getPackageManager()
                    .getApplicationInfo(data.getPackageName(), 0 /* flags */);
        } catch (PackageManager.NameNotFoundException e) {
            Log.w(TAG, "Fail to get media recommendation's app info", e);
            return;
        }

@@ -531,7 +531,7 @@ public class MediaControlPanel {
        }
        // Set up media card's tap action if applicable.
        setSmartspaceRecItemOnClickListener(
                mRecommendationViewHolder.getRecommendations(), target.getBaseAction());
                mRecommendationViewHolder.getRecommendations(), data.getCardAction());

        List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems();
        List<Integer> mediaCoverItemsResIds = mRecommendationViewHolder.getMediaCoverItemsResIds();
@@ -574,7 +574,7 @@ public class MediaControlPanel {
                    /* isRecommendationCard */ true);
            closeGuts();
            mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
                    MediaViewController.GUTS_ANIMATION_DURATION + 100L);
                    data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L);
        });

        mController = null;
@@ -752,38 +752,6 @@ public class MediaControlPanel {
        return false;
    }

    /**
     * Returns the application info for the media recommendation's source app.
     *
     * @param target Smartspace target contains a list of media recommendations. Each item should
     *               contain the same source app's info.
     *
     * @return The source app's application info. This value can be null if no valid application
     * info can be obtained.
     */
    private ApplicationInfo getApplicationInfo(@NonNull SmartspaceTarget target) {
        List<SmartspaceAction> mediaRecommendationList = target.getIconGrid();
        if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) {
            return null;
        }

        for (SmartspaceAction recommendation: mediaRecommendationList) {
            Bundle extras = recommendation.getExtras();
            if (extras != null && extras.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME) != null) {
                // Get the logo from app's package name when applicable.
                String packageName = extras.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME);
                try {
                    return mContext.getPackageManager()
                            .getApplicationInfo(packageName, 0 /* flags */);
                } catch (PackageManager.NameNotFoundException e) {
                    Log.w(TAG, "Fail to get media recommendation's app info", e);
                }
            }
        }

        return null;
    }

    /**
     * Get the surface given the current end location for MediaViewController
     * @return surface used for Smartspace logging
+9 −5
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.media

import android.app.smartspace.SmartspaceTarget
import javax.inject.Inject

/**
@@ -28,7 +27,12 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
    private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()

    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
    override fun onMediaDataLoaded(
        key: String,
        oldKey: String?,
        data: MediaData,
        immediately: Boolean
    ) {
        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
            entries[key] = data to entries.remove(oldKey)?.second
            update(key, oldKey)
@@ -40,7 +44,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,

    override fun onSmartspaceMediaDataLoaded(
        key: String,
        data: SmartspaceTarget,
        data: SmartspaceMediaData,
        shouldPrioritize: Boolean
    ) {
        listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
@@ -50,8 +54,8 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
        remove(key)
    }

    override fun onSmartspaceMediaDataRemoved(key: String) {
        listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key) }
    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
        listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
    }

    override fun onMediaDeviceChanged(
+42 −27
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package com.android.systemui.media

import android.app.smartspace.SmartspaceAction
import android.app.smartspace.SmartspaceTarget
import android.os.SystemProperties
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
@@ -34,6 +32,7 @@ import kotlin.collections.LinkedHashMap

private const val TAG = "MediaDataFilter"
private const val DEBUG = true
private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"

/**
 * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
@@ -67,8 +66,7 @@ class MediaDataFilter @Inject constructor(
    private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
    // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
    private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
    var hasSmartspace: Boolean = false
        private set
    private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
    private var reactivatedKey: String? = null

    init {
@@ -81,7 +79,12 @@ class MediaDataFilter @Inject constructor(
        userTracker.startTracking()
    }

    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
    override fun onMediaDataLoaded(
        key: String,
        oldKey: String?,
        data: MediaData,
        immediately: Boolean
    ) {
        if (oldKey != null && oldKey != key) {
            allEntries.remove(oldKey)
        }
@@ -104,18 +107,32 @@ class MediaDataFilter @Inject constructor(

    override fun onSmartspaceMediaDataLoaded(
        key: String,
        data: SmartspaceTarget,
        data: SmartspaceMediaData,
        shouldPrioritize: Boolean
    ) {
        var shouldPrioritizeMutable = shouldPrioritize
        hasSmartspace = true
        if (!data.isActive) {
            Log.d(TAG, "Inactive recommendation data. Skip triggering.")
            return
        }

        // Override the pass-in value here, as the order of Smartspace card is only determined here.
        var shouldPrioritizeMutable = false
        smartspaceMediaData = data

        // Before forwarding the smartspace target, first check if we have recently inactive media
        val sorted = userEntries.toSortedMap(compareBy {
            userEntries.get(it)?.lastActive ?: -1
        })
        val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
        if (timeSinceActive < SMARTSPACE_MAX_AGE) {
        var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
        data.cardAction?.let {
            val smartspaceMaxAgeSeconds =
                it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
            if (smartspaceMaxAgeSeconds > 0) {
                smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
            }
        }
        if (timeSinceActive < smartspaceMaxAgeMillis) {
            val lastActiveKey = sorted.lastKey() // most recently active
            // Notify listeners to consider this media active
            Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
@@ -129,9 +146,8 @@ class MediaDataFilter @Inject constructor(
            shouldPrioritizeMutable = true
        }

        // Only proceed with the Smartspace update if the recommendation is not empty.
        if (isMediaRecommendationEmpty(data)) {
            Log.d(TAG, "Empty media recommendations. Skip showing the card")
        if (!data.isValid) {
            Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
            return
        }
        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
@@ -147,9 +163,7 @@ class MediaDataFilter @Inject constructor(
        }
    }

    override fun onSmartspaceMediaDataRemoved(key: String) {
        hasSmartspace = false

    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
        // First check if we had reactivated media instead of forwarding smartspace
        reactivatedKey?.let {
            val lastActiveKey = it
@@ -158,12 +172,17 @@ class MediaDataFilter @Inject constructor(
            // Notify listeners to update with actual active value
            userEntries.get(lastActiveKey)?.let { mediaData ->
                listeners.forEach {
                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData)
                    it.onMediaDataLoaded(
                            lastActiveKey, lastActiveKey, mediaData, immediately)
                }
            }
        }

        listeners.forEach { it.onSmartspaceMediaDataRemoved(key) }
        if (smartspaceMediaData.isActive) {
            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid)
        }
        listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
    }

    @VisibleForTesting
@@ -202,20 +221,22 @@ class MediaDataFilter @Inject constructor(
            // Force updates to listeners, needed for re-activated card
            mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
        }
        if (hasSmartspace) {
            mediaDataManager.dismissSmartspaceRecommendation(0L /* delay */)
        if (smartspaceMediaData.isActive) {
            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid)
        }
        mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, delay = 0L)
    }

    /**
     * Are there any media notifications active?
     */
    fun hasActiveMedia() = userEntries.any { it.value.active } || hasSmartspace
    fun hasActiveMedia() = userEntries.any { it.value.active } || smartspaceMediaData.isActive

    /**
     * Are there any media entries we should display?
     */
    fun hasAnyMedia() = userEntries.isNotEmpty() || hasSmartspace
    fun hasAnyMedia() = userEntries.isNotEmpty() || smartspaceMediaData.isActive

    /**
     * Add a listener for filtered [MediaData] changes
@@ -227,12 +248,6 @@ class MediaDataFilter @Inject constructor(
     */
    fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)

    /** Check if the Smartspace sends an empty update. */
    private fun isMediaRecommendationEmpty(data: SmartspaceTarget): Boolean {
        val mediaRecommendationList: List<SmartspaceAction> = data.getIconGrid()
        return mediaRecommendationList == null || mediaRecommendationList.isEmpty()
    }

    /**
     * Return the time since last active for the most-recent media.
     *
Loading