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
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user