From b0cee2eccb6e9881311887475bd59d0ac38c6785 Mon Sep 17 00:00:00 2001 From: Jieru Shi Date: Mon, 3 May 2021 17:59:41 -0700 Subject: [PATCH] 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 --- .../systemui/media/MediaCarouselController.kt | 94 +++++++++++++------ .../media/MediaCarouselScrollHandler.kt | 18 +++- .../systemui/media/MediaControlPanel.java | 37 +++++--- .../systemui/media/MediaHierarchyManager.kt | 13 ++- .../systemui/media/MediaControlPanelTest.kt | 3 +- 5 files changed, 114 insertions(+), 51 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 7a7198fc92339..644876c6b1afe 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -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) { - /* 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) - /* ktlint-disable max-line-length */ + 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 } - - // TODO(shijieru): add logging for media control card + 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, + 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 */ + } + + 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() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index 63580041c899b..e5a62711a8d04 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -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() } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 647e8bfa704a3..fe3463f853f02 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -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 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()); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index a80a410d32dbe..3c28f6e1651a1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -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() } }) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index 800daa1e96aa0..dd3a19228b4ea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -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.