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

Commit b0cee2ec authored by Jieru Shi's avatar Jieru Shi
Browse files

Add dismiss, impression, click logging for media control card, add dismiss...

Add dismiss, impression, click logging for media control card, add dismiss logging for media recommendation card

Bug: 181364757
Test: media control card statsd_drive: https://paste.googleplex.com/4558591743754240
media recommendation card statsd_drive: https://paste.googleplex.com/5698893212811264
Change-Id: Ie9d656fa9d519f75c4c4a2d77cf8b0355240b7a4
parent cacddc52
Loading
Loading
Loading
Loading
+66 −26
Original line number Diff line number Diff line
@@ -106,7 +106,7 @@ class MediaCarouselController @Inject constructor(
    private var carouselMeasureHeight: Int = 0
    private var desiredHostState: MediaHostState? = null
    private val mediaCarousel: MediaScrollView
    private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
    val mediaCarouselScrollHandler: MediaCarouselScrollHandler
    val mediaFrame: ViewGroup
    private lateinit var settingsButton: View
    private val mediaContent: ViewGroup
@@ -156,20 +156,13 @@ class MediaCarouselController @Inject constructor(
        }
    }

    var visibleToUser: Boolean = false
        set(value) {
            if (field != value) {
                field = value
            }
        }

    init {
        mediaFrame = inflateMediaCarousel()
        mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
        pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
        mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
                executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
                this::closeGuts, falsingCollector, falsingManager)
                executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation,
                this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression)
        isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
        inflateSettingsButton()
        mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
@@ -210,7 +203,7 @@ class MediaCarouselController @Inject constructor(
            override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
                Log.d(TAG, "My Smartspace media update is here")
                addSmartspaceMediaRecommendations(key, data)
                if (visibleToUser) {
                if (mediaCarouselScrollHandler.visibleToUser) {
                    logSmartspaceImpression()
                }
            }
@@ -580,24 +573,69 @@ class MediaCarouselController @Inject constructor(
     * Log the user impression for media card.
     */
    fun logSmartspaceImpression() {
        MediaPlayerData.players().forEach {
            // Log every impression of media recommendation card since it will only be shown
            // for 1 minute after each connection.
            if (it.recommendationViewHolder?.recommendations?.visibility == View.VISIBLE) {
        val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
        if (MediaPlayerData.players().size > visibleMediaIndex) {
            val mediaControlPanel = MediaPlayerData.players().elementAt(visibleMediaIndex)
            val isMediaActive =
                    MediaPlayerData.playerKeys().elementAt(visibleMediaIndex).data?.active
            val isRecommendationCard = mediaControlPanel.recommendationViewHolder != null
            if (!isRecommendationCard && !isMediaActive) {
                // Media control card time out or swiped away
                return
            }
            logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
                    mediaControlPanel.mInstanceId,
                    isRecommendationCard,
                    mediaControlPanel.surfaceForSmartspaceLogging)
        }
    }

    @JvmOverloads
    fun logSmartspaceCardReported(
        eventId: Int,
        instanceId: Int,
        isRecommendationCard: Boolean,
        surface: Int,
        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex
    ) {
        /* ktlint-disable max-line-length */
        SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
                        800, // SMARTSPACE_CARD_SEEN
                        it.getInstanceId(),
                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__HEADPHONE_MEDIA_RECOMMENDATIONS,
                        it.getSurfaceForSmartspaceLogging(),
                        /* rank */ 0,
                        /* cardinality */ 1)
                eventId,
                instanceId,
                if (isRecommendationCard)
                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__HEADPHONE_MEDIA_RECOMMENDATIONS
                else
                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__HEADPHONE_RESUME_MEDIA,
                surface,
                rank,
                mediaContent.getChildCount())
        /* ktlint-disable max-line-length */
    }

            // TODO(shijieru): add logging for media control card
    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,
                    true,
                    recommendation.get(0).surfaceForSmartspaceLogging,
            /* rank */-1)
        } else {
            val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
            if (MediaPlayerData.players().size > visibleMediaIndex) {
                val player = MediaPlayerData.players().elementAt(visibleMediaIndex)
                logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                        player.mInstanceId,
                false,
                        player.surfaceForSmartspaceLogging,
                /* rank */-1)
            }
        }
        mediaManager.onSwipeToDismiss()
    }
}

@VisibleForTesting
@@ -605,7 +643,7 @@ internal object MediaPlayerData {
    private val EMPTY = MediaData(-1, false, 0, null, null, null, null, null,
        emptyList(), emptyList(), "INVALID", null, null, null, false, null)

    private data class MediaSortKey(
    data class MediaSortKey(
        // Is Smartspace media recommendation. When the Smartspace media is present, it should
        // always be the first card in carousel.
        val isSsMediaRec: Boolean,
@@ -663,6 +701,8 @@ internal object MediaPlayerData {
        return -1
    }

    fun playerKeys() = mediaPlayers.keys

    @VisibleForTesting
    fun clear() {
        mediaData.clear()
+15 −3
Original line number Diff line number Diff line
@@ -28,13 +28,13 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
import com.android.settingslib.Utils
import com.android.systemui.Gefingerpoken
import com.android.systemui.qs.PageIndicator
import com.android.systemui.R
import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.plugins.FalsingManager
import com.android.wm.shell.animation.PhysicsAnimator
import com.android.systemui.qs.PageIndicator
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.wm.shell.animation.PhysicsAnimator

private const val FLING_SLOP = 1000000
private const val DISMISS_DELAY = 100L
@@ -61,7 +61,8 @@ class MediaCarouselScrollHandler(
    private var translationChangedListener: () -> Unit,
    private val closeGuts: () -> Unit,
    private val falsingCollector: FalsingCollector,
    private val falsingManager: FalsingManager
    private val falsingManager: FalsingManager,
    private val logSmartspaceImpression: () -> Unit
) {
    /**
     * Is the view in RTL
@@ -200,6 +201,13 @@ class MediaCarouselScrollHandler(
        }
    }

    var visibleToUser: Boolean = false
        set(value) {
            if (field != value) {
                field = value
            }
        }

    init {
        gestureDetector = GestureDetectorCompat(scrollView.context, gestureListener)
        scrollView.touchListener = touchListener
@@ -460,7 +468,11 @@ class MediaCarouselScrollHandler(
        scrollIntoCurrentMedia = scrollInAmount
        val nowScrolledIn = scrollIntoCurrentMedia != 0
        if (newIndex != visibleMediaIndex || wasScrolledIn != nowScrolledIn) {
            val oldIndex = visibleMediaIndex
            visibleMediaIndex = newIndex
            if (oldIndex != visibleMediaIndex && visibleToUser) {
                logSmartspaceImpression()
            }
            closeGuts()
            updatePlayerVisibilities()
        }
+24 −13
Original line number Diff line number Diff line
@@ -105,7 +105,8 @@ public class MediaControlPanel {
    private int mDevicePadding;
    private int mAlbumArtSize;
    // Instance id for logging purpose.
    private int mInstanceId;
    protected int mInstanceId = -1;
    private MediaCarouselController mMediaCarouselController;
    private final MediaOutputDialogFactory mMediaOutputDialogFactory;

    /**
@@ -119,7 +120,7 @@ public class MediaControlPanel {
            ActivityStarter activityStarter, MediaViewController mediaViewController,
            SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
            KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
            mediaOutputDialogFactory) {
            mediaOutputDialogFactory, MediaCarouselController mediaCarouselController) {
        mContext = context;
        mBackgroundExecutor = backgroundExecutor;
        mActivityStarter = activityStarter;
@@ -128,6 +129,7 @@ public class MediaControlPanel {
        mMediaDataManagerLazy = lazyMediaDataManager;
        mKeyguardDismissUtil = keyguardDismissUtil;
        mMediaOutputDialogFactory = mediaOutputDialogFactory;
        mMediaCarouselController = mediaCarouselController;
        loadDimens();
    }

@@ -251,6 +253,8 @@ public class MediaControlPanel {
        }
        mKey = key;
        MediaSession.Token token = data.getToken();
        mInstanceId = data.getPackageName().hashCode();

        mBackgroundColor = data.getBackgroundColor();
        if (mToken == null || !mToken.equals(token)) {
            mToken = token;
@@ -270,6 +274,9 @@ public class MediaControlPanel {
        if (clickIntent != null) {
            mPlayerViewHolder.getPlayer().setOnClickListener(v -> {
                if (mMediaViewController.isGutsVisible()) return;

                logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
                        false);
                mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
                        buildLaunchAnimatorController(mPlayerViewHolder.getPlayer()));
            });
@@ -376,6 +383,8 @@ public class MediaControlPanel {
            } else {
                button.setEnabled(true);
                button.setOnClickListener(v -> {
                    logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
                            false);
                    action.run();
                });
            }
@@ -408,6 +417,9 @@ public class MediaControlPanel {
        mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
        mPlayerViewHolder.getDismiss().setEnabled(isDismissible);
        mPlayerViewHolder.getDismiss().setOnClickListener(v -> {
            logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                    false);

            if (mKey != null) {
                closeGuts();
                mKeyguardDismissUtil.executeWhenUnlocked(() -> {
@@ -550,6 +562,8 @@ public class MediaControlPanel {

        // Set up long press to show guts setting panel.
        mRecommendationViewHolder.getDismiss().setOnClickListener(v -> {
            logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                    true);
            closeGuts();
            mKeyguardDismissUtil.executeWhenUnlocked(() -> {
                mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
@@ -652,15 +666,9 @@ public class MediaControlPanel {
        }

        view.setOnClickListener(v -> {
            // When media recommendation card is shown, there could be only one card.
            SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
                    760, // SMARTSPACE_CARD_CLICK
                    mInstanceId,
                    SysUiStatsLog
                            .SMART_SPACE_CARD_REPORTED__CARD_TYPE__HEADPHONE_MEDIA_RECOMMENDATIONS,
                    getSurfaceForSmartspaceLogging(),
                    /* rank */ 0,
                    /* cardinality */ 1);
            // When media recommendation card is shown, it will always be the top card.
            logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
                    true);

            if (shouldSmartspaceRecItemOpenInForeground(action)) {
                // Request to unlock the device if the activity needs to be opened in foreground.
@@ -718,7 +726,10 @@ public class MediaControlPanel {
        return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE;
    }

    protected int getInstanceId() {
        return mInstanceId;
    private void logSmartspaceCardReported(int eventId, boolean isRecommendationCard) {
        mMediaCarouselController.logSmartspaceCardReported(eventId,
                mInstanceId,
                isRecommendationCard,
                getSurfaceForSmartspaceLogging());
    }
}
+6 −7
Original line number Diff line number Diff line
@@ -171,16 +171,15 @@ class MediaHierarchyManager @Inject constructor(
            if (field != value) {
                field = value
            }
            // Pull down shade from lock screen (exclude the case when shade is brought out by
            // tapping twice on lock screen)
            if (value && isLockScreenShadeVisibleToUser()) {
            // qs is expanded on LS shade and HS shade
            if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
                mediaCarouselController.logSmartspaceImpression()
            }
            // Release shade and back to lock screen
            if (isLockScreenVisibleToUser()) {
                mediaCarouselController.logSmartspaceImpression()
            }
            mediaCarouselController.visibleToUser = isVisibleToUser()
            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
        }

    /**
@@ -257,7 +256,7 @@ class MediaHierarchyManager @Inject constructor(
                if (newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()) {
                    mediaCarouselController.logSmartspaceImpression()
                }
                mediaCarouselController.visibleToUser = isVisibleToUser()
                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
            }

            override fun onDozeAmountChanged(linear: Float, eased: Float) {
@@ -275,7 +274,7 @@ class MediaHierarchyManager @Inject constructor(
                    updateDesiredLocation()
                    qsExpanded = false
                }
                mediaCarouselController.visibleToUser = isVisibleToUser()
                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
            }

            override fun onExpandedChanged(isExpanded: Boolean) {
@@ -287,7 +286,7 @@ class MediaHierarchyManager @Inject constructor(
                if (isLockScreenVisibleToUser()) {
                    mediaCarouselController.logSmartspaceImpression()
                }
                mediaCarouselController.visibleToUser = isVisibleToUser()
                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
            }
        })

+2 −1
Original line number Diff line number Diff line
@@ -93,6 +93,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
    @Mock private lateinit var expandedSet: ConstraintSet
    @Mock private lateinit var collapsedSet: ConstraintSet
    @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
    @Mock private lateinit var mediaCarouselController: MediaCarouselController
    private lateinit var appIcon: ImageView
    private lateinit var albumView: ImageView
    private lateinit var titleText: TextView
@@ -129,7 +130,7 @@ public class MediaControlPanelTest : SysuiTestCase() {

        player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
                seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
                mediaOutputDialogFactory)
                mediaOutputDialogFactory, mediaCarouselController)
        whenever(seekBarViewModel.progress).thenReturn(seekBarData)

        // Mock out a view holder for the player to attach to.