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

Commit 4b79500f authored by Jieru Shi's avatar Jieru Shi
Browse files

Fix Smartspace media logging bugs

1. Log media card received event for both LS and SHADE
2. Log dismiss event for all impressed media card
3. Fix latency logging for media card
4. Fix instance id for media resume card

Bug: 202552118
Test: manual, https://paste.googleplex.com/4995063781261312
Change-Id: Ie47db39a5aaae79a3a6c60b109877e4caebcf9ea
parent c546f6e0
Loading
Loading
Loading
Loading
+116 −71
Original line number Diff line number Diff line
@@ -37,7 +37,7 @@ import javax.inject.Provider

private const val TAG = "MediaCarouselController"
private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
private const val DEBUG = false
private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)

/**
 * Class that is responsible for keeping the view carousel up to date.
@@ -209,41 +209,56 @@ class MediaCarouselController @Inject constructor(
                oldKey: String?,
                data: MediaData,
                immediately: Boolean,
                isSsReactivated: Boolean
                receivedSmartspaceCardLatency: Int
            ) {
                if (addOrUpdatePlayer(key, oldKey, data)) {
                    // Log card received if a new resumable media card is added
                    MediaPlayerData.getMediaPlayer(key)?.let {
                        /* ktlint-disable max-line-length */
                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
                                it.mInstanceId,
                                it.mUid,
                                /* isRecommendationCard */ false,
                                it.surfaceForSmartspaceLogging,
                                intArrayOf(
                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN),
                                rank = MediaPlayerData.getMediaPlayerIndex(key))
                        /* ktlint-disable max-line-length */
                    }
                    if (mediaCarouselScrollHandler.visibleToUser &&
                            mediaCarouselScrollHandler.visibleMediaIndex
                            == MediaPlayerData.getMediaPlayerIndex(key)) {
                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
                    }
                if (isSsReactivated) {
                    // If resumable media is reactivated by headphone connection, update instance
                    // id for each card and log a receive event.
                } else if (receivedSmartspaceCardLatency != 0) {
                    // Log resume card received if resumable media card is reactivated and
                    // resume card is ranked first
                    MediaPlayerData.players().forEachIndexed { index, it ->
                        if (it.recommendationViewHolder == null) {
                            it.mInstanceId = SmallHash.hash(it.mUid +
                                    systemClock.currentTimeMillis().toInt())
                            it.mIsImpressed = false
                            /* ktlint-disable max-line-length */
                            logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
                                    it.mInstanceId,
                                    it.mUid,
                                    /* isRecommendationCard */ false,
                                    it.surfaceForSmartspaceLogging,
                                    rank = index)
                        }
                                    intArrayOf(
                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN),
                                    rank = index,
                                    receivedLatencyMillis = receivedSmartspaceCardLatency)
                            /* ktlint-disable max-line-length */
                        }
                    }
                    // If media container area already visible to the user, log impression for
                    // reactivated card.
                    if (mediaCarouselScrollHandler.visibleToUser &&
                        isSsReactivated && !mediaCarouselScrollHandler.qsExpanded) {
                    // It could happen that reactived media player isn't visible to user because
                    // of it is a resumption card.
                            !mediaCarouselScrollHandler.qsExpanded) {
                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
                    }
                }

                val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
                if (canRemove && !Utils.useMediaResumption(context)) {
                    // This view isn't playing, let's remove this! This happens e.g when
@@ -262,28 +277,51 @@ class MediaCarouselController @Inject constructor(
            override fun onSmartspaceMediaDataLoaded(
                key: String,
                data: SmartspaceMediaData,
                shouldPrioritize: Boolean
                shouldPrioritize: Boolean,
                isSsReactivated: Boolean
            ) {
                if (DEBUG) Log.d(TAG, "Loading Smartspace media update")
                if (data.isActive) {
                    addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
                    MediaPlayerData.getMediaPlayer(key)?.let {
                    if (isSsReactivated && shouldPrioritize) {
                        // Log resume card received if resumable media card is reactivated and
                        // recommendation card is valid and ranked first
                        MediaPlayerData.players().forEachIndexed { index, it ->
                            if (it.recommendationViewHolder == null) {
                                it.mInstanceId = SmallHash.hash(it.mUid +
                                        systemClock.currentTimeMillis().toInt())
                                it.mIsImpressed = false
                                /* ktlint-disable max-line-length */
                                logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
                                        it.mInstanceId,
                                        it.mUid,
                                /* isRecommendationCard */ true,
                                it.surfaceForSmartspaceLogging,
                                rank = MediaPlayerData.getMediaPlayerIndex(key))

                        if (mediaCarouselScrollHandler.visibleToUser &&
                                mediaCarouselScrollHandler.visibleMediaIndex ==
                                MediaPlayerData.getMediaPlayerIndex(key)) {
                            logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
                                        /* isRecommendationCard */ false,
                                        intArrayOf(
                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN),
                                        rank = index,
                                        receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
                                /* ktlint-disable max-line-length */
                            }
                        }
                    }
                    addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
                    MediaPlayerData.getMediaPlayer(key)?.let {
                        /* ktlint-disable max-line-length */
                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
                                it.mInstanceId,
                                it.mUid,
                                /* isRecommendationCard */ true,
                                    it.surfaceForSmartspaceLogging)
                                intArrayOf(
                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN),
                                rank = MediaPlayerData.getMediaPlayerIndex(key),
                                receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
                        /* ktlint-disable max-line-length */
                    }
                    if (mediaCarouselScrollHandler.visibleToUser &&
                            mediaCarouselScrollHandler.visibleMediaIndex
                            == MediaPlayerData.getMediaPlayerIndex(key)) {
                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
                    }
                } else {
                    onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
@@ -718,7 +756,8 @@ class MediaCarouselController @Inject constructor(
                    mediaControlPanel.mInstanceId,
                    mediaControlPanel.mUid,
                    isRecommendationCard,
                    mediaControlPanel.surfaceForSmartspaceLogging)
                    intArrayOf(mediaControlPanel.surfaceForSmartspaceLogging))
            mediaControlPanel.mIsImpressed = true
        }
    }

@@ -731,12 +770,15 @@ class MediaCarouselController @Inject constructor(
     * instanceId
     * @param uid uid for the application that media comes from
     * @param isRecommendationCard whether the card is media recommendation
     * @param surface which display surface the media card is on (e.g. lockscreen, shade)
     * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
     * the event happened
     * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
     * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
     * @param interactedSubcardCardinality how many media items were shown to the user when there
     * is user interaction
     * @param rank the rank for media card in the media carousel, starting from 0
     * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
     * between headphone connection to sysUI displays media recommendation card
     *
     */
    fun logSmartspaceCardReported(
@@ -744,10 +786,11 @@ class MediaCarouselController @Inject constructor(
        instanceId: Int,
        uid: Int,
        isRecommendationCard: Boolean,
        surface: Int,
        surfaces: IntArray,
        interactedSubcardRank: Int = 0,
        interactedSubcardCardinality: Int = 0,
        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex
        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
        receivedLatencyMillis: Int = 0
    ) {
        // Only log media resume card when Smartspace data is available
        if (!isRecommendationCard &&
@@ -756,6 +799,8 @@ class MediaCarouselController @Inject constructor(
            return
        }

        val cardinality = mediaContent.getChildCount()
        surfaces.forEach { surface ->
            /* ktlint-disable max-line-length */
            SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
                    eventId,
@@ -765,7 +810,7 @@ class MediaCarouselController @Inject constructor(
                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
                    surface,
                    rank,
                mediaContent.getChildCount(),
                    cardinality,
                    if (isRecommendationCard)
                        15 // MEDIA_RECOMMENDATION
                    else
@@ -773,33 +818,33 @@ class MediaCarouselController @Inject constructor(
                    uid,
                    interactedSubcardRank,
                    interactedSubcardCardinality,
                0 // received_latency_millis
                    receivedLatencyMillis
            )
            /* ktlint-disable max-line-length */
            if (DEBUG) {
                Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" +
                        " surface: $surface rank: $rank cardinality: $cardinality " +
                        "isRecommendationCard: $isRecommendationCard uid: $uid " +
                        "interactedSubcardRank: $interactedSubcardRank " +
                        "interactedSubcardCardinality: $interactedSubcardCardinality " +
                        "received_latency_millis: $receivedLatencyMillis")
            }
        }
    }

    private fun onSwipeToDismiss() {
        val recommendation = MediaPlayerData.players().filter {
            it.recommendationViewHolder != null
        }
        // Use -1 as rank value to indicate user swipe to dismiss the card
        if (!recommendation.isEmpty()) {
            logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                    recommendation.get(0).mInstanceId,
                    recommendation.get(0).mUid,
                    true,
                    recommendation.get(0).surfaceForSmartspaceLogging,
                    rank = -1)
        } else {
            val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
            if (MediaPlayerData.players().size > visibleMediaIndex) {
                val player = MediaPlayerData.players().elementAt(visibleMediaIndex)
        MediaPlayerData.players().forEachIndexed {
            index, it ->
            if (it.mIsImpressed) {
                logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                        player.mInstanceId,
                        player.mUid,
                        false,
                        player.surfaceForSmartspaceLogging,
                        it.mInstanceId,
                        it.mUid,
                        it.recommendationViewHolder != null,
                        intArrayOf(it.surfaceForSmartspaceLogging),
                        // Use -1 as rank value to indicate user swipe to dismiss the card
                        rank = -1)
                // Reset card impressed state when swipe to dismissed
                it.mIsImpressed = false
            }
        }
        mediaManager.onSwipeToDismiss()
+12 −3
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.time.SystemClock;

import java.net.URISyntaxException;
import java.util.List;
@@ -121,6 +122,9 @@ public class MediaControlPanel {
    private MediaCarouselController mMediaCarouselController;
    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
    private final FalsingManager mFalsingManager;
    // Used for swipe-to-dismiss logging.
    protected boolean mIsImpressed = false;
    private SystemClock mSystemClock;

    /**
     * Initialize a new control panel
@@ -134,7 +138,7 @@ public class MediaControlPanel {
            SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
            KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
            mediaOutputDialogFactory, MediaCarouselController mediaCarouselController,
            FalsingManager falsingManager) {
            FalsingManager falsingManager, SystemClock systemClock) {
        mContext = context;
        mBackgroundExecutor = backgroundExecutor;
        mActivityStarter = activityStarter;
@@ -145,6 +149,8 @@ public class MediaControlPanel {
        mMediaOutputDialogFactory = mediaOutputDialogFactory;
        mMediaCarouselController = mediaCarouselController;
        mFalsingManager = falsingManager;
        mSystemClock = systemClock;

        loadDimens();

        mSeekBarViewModel.setLogSmartspaceClick(() -> {
@@ -291,7 +297,10 @@ public class MediaControlPanel {
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Unable to look up package name", e);
        }
        mInstanceId = SmallHash.hash(mUid);
        // Only assigns instance id if it's unassigned.
        if (mInstanceId == -1) {
            mInstanceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis());
        }

        mBackgroundColor = data.getBackgroundColor();
        if (mToken == null || !mToken.equals(token)) {
@@ -885,7 +894,7 @@ public class MediaControlPanel {
                mInstanceId,
                mUid,
                isRecommendationCard,
                getSurfaceForSmartspaceLogging(),
                new int[]{getSurfaceForSmartspaceLogging()},
                interactedSubcardRank,
                interactedSubcardCardinality);
    }
+3 −2
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
        oldKey: String?,
        data: MediaData,
        immediately: Boolean,
        isSsReactivated: Boolean
        receivedSmartspaceCardLatency: Int
    ) {
        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
            entries[key] = data to entries.remove(oldKey)?.second
@@ -46,7 +46,8 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
    override fun onSmartspaceMediaDataLoaded(
        key: String,
        data: SmartspaceMediaData,
        shouldPrioritize: Boolean
        shouldPrioritize: Boolean,
        isSsReactivated: Boolean
    ) {
        listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
    }
+24 −18
Original line number Diff line number Diff line
@@ -56,7 +56,6 @@ internal val SMARTSPACE_MAX_AGE = SystemProperties
class MediaDataFilter @Inject constructor(
    private val context: Context,
    private val broadcastDispatcher: BroadcastDispatcher,
    private val mediaResumeListener: MediaResumeListener,
    private val lockscreenUserManager: NotificationLockscreenUserManager,
    @Main private val executor: Executor,
    private val systemClock: SystemClock
@@ -88,7 +87,7 @@ class MediaDataFilter @Inject constructor(
        oldKey: String?,
        data: MediaData,
        immediately: Boolean,
        isSsReactivated: Boolean
        receivedSmartspaceCardLatency: Int
    ) {
        if (oldKey != null && oldKey != key) {
            allEntries.remove(oldKey)
@@ -106,14 +105,15 @@ class MediaDataFilter @Inject constructor(

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

    override fun onSmartspaceMediaDataLoaded(
        key: String,
        data: SmartspaceMediaData,
        shouldPrioritize: Boolean
        shouldPrioritize: Boolean,
        isSsReactivated: Boolean
    ) {
        if (!data.isActive) {
            Log.d(TAG, "Inactive recommendation data. Skip triggering.")
@@ -123,8 +123,6 @@ class MediaDataFilter @Inject constructor(
        // Override the pass-in value here, as the order of Smartspace card is only determined here.
        var shouldPrioritizeMutable = false
        smartspaceMediaData = data
        // Override the pass-in value here, as the Smartspace reactivation could only happen here.
        var isSsReactivated = false

        // Before forwarding the smartspace target, first check if we have recently inactive media
        val sorted = userEntries.toSortedMap(compareBy {
@@ -139,18 +137,25 @@ class MediaDataFilter @Inject constructor(
                smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
            }
        }

        val activeMedia = userEntries.filter { (key, value) -> value.active }
        var isSsReactivatedMutable = activeMedia.isEmpty() && userEntries.isNotEmpty()

        if (timeSinceActive < smartspaceMaxAgeMillis) {
            // It could happen there are existing active media resume cards, then we don't need to
            // reactivate.
            if (isSsReactivatedMutable) {
                val lastActiveKey = sorted.lastKey() // most recently active
                // Notify listeners to consider this media active
                Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
                reactivatedKey = lastActiveKey
            if (MediaPlayerData.firstActiveMediaIndex() == -1) {
                isSsReactivated = true
            }
                val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
                listeners.forEach {
                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData,
                        isSsReactivated = isSsReactivated)
                            receivedSmartspaceCardLatency =
                            (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
                                    .toInt())
                }
            }
        } else {
            // Mark to prioritize Smartspace card if no recent media.
@@ -161,7 +166,8 @@ class MediaDataFilter @Inject constructor(
            Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
            return
        }
        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable,
                isSsReactivatedMutable) }
    }

    override fun onMediaDataRemoved(key: String) {
+14 −8
Original line number Diff line number Diff line
@@ -84,7 +84,7 @@ private val LOADING = MediaData(-1, false, 0, null, null, null, null, null,
        emptyList(), emptyList(), "INVALID", null, null, null, true, null)
@VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData("INVALID", false, false,
    "INVALID", null, emptyList(), null, 0)
    "INVALID", null, emptyList(), null, 0, 0)

fun isMediaNotification(sbn: StatusBarNotification): Boolean {
    return sbn.notification.isMediaNotification()
@@ -852,15 +852,16 @@ class MediaDataManager(
         * until the next refresh-round before UI becomes visible. True by default to take in place
         * immediately.
         *
         * @param isSsReactivated indicates transition from a state with no active media players to
         * a state with active media players upon receiving Smartspace media data.
         * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI
         * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace
         * signal.
         */
        fun onMediaDataLoaded(
            key: String,
            oldKey: String?,
            data: MediaData,
            immediately: Boolean = true,
            isSsReactivated: Boolean = false
            receivedSmartspaceCardLatency: Int = 0
        ) {}

        /**
@@ -869,11 +870,15 @@ class MediaDataManager(
         * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true,
         * it will be prioritized as the first card. Otherwise, it will show up as the last card as
         * default.
         *
         * @param isSsReactivated indicates resume media card is reactivated by Smartspace
         * recommendation signal
         */
        fun onSmartspaceMediaDataLoaded(
            key: String,
            data: SmartspaceMediaData,
            shouldPrioritize: Boolean = false
            shouldPrioritize: Boolean = false,
            isSsReactivated: Boolean = false
        ) {}

        /** Called whenever a previously existing Media notification was removed. */
@@ -909,12 +914,13 @@ class MediaDataManager(
        packageName(target)?.let {
            return SmartspaceMediaData(target.smartspaceTargetId, isActive, true, it,
                target.baseAction, target.iconGrid,
                dismissIntent, 0)
                dismissIntent, 0, target.creationTimeMillis)
        }
        return EMPTY_SMARTSPACE_MEDIA_DATA
            .copy(targetId = target.smartspaceTargetId,
                    isActive = isActive,
                dismissIntent = dismissIntent)
                    dismissIntent = dismissIntent,
                    headphoneConnectionTimeMillis = target.creationTimeMillis)
    }

    private fun packageName(target: SmartspaceTarget): String? {
Loading