Add logging for media recommendation events

Adds events for
- Media recommendation added or removed
- Media control re-activated due to a recommendation
- Tap on media recommendation card or item
- Media recommendation long press menu open and button taps

Bug: 216488338
Test: manual
Test: atest com.android.systemui.media
Change-Id: Ifdfa430a11e6f15fa0f969d78723b3ef11131ae2
This commit is contained in:
Beth Thibodeau
2022-04-06 14:04:10 -04:00
parent e0aee9342b
commit f000c437a8
10 changed files with 367 additions and 60 deletions

View File

@@ -504,6 +504,11 @@ class MediaCarouselController @Inject constructor(
dismissMediaData: Boolean = true, dismissMediaData: Boolean = true,
dismissRecommendation: Boolean = true dismissRecommendation: Boolean = true
) { ) {
if (key == MediaPlayerData.smartspaceMediaKey()) {
MediaPlayerData.smartspaceMediaData?.let {
logger.logRecommendationRemoved(it.packageName, it.instanceId)
}
}
val removed = MediaPlayerData.removeMediaPlayer(key) val removed = MediaPlayerData.removeMediaPlayer(key)
removed?.apply { removed?.apply {
mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaCarouselScrollHandler.onPrePlayerRemoved(removed)

View File

@@ -331,6 +331,7 @@ public class MediaControlPanel {
}); });
mRecommendationViewHolder.getSettings().setOnClickListener(v -> { mRecommendationViewHolder.getSettings().setOnClickListener(v -> {
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
mLogger.logLongPressSettings(mUid, mPackageName, mInstanceId);
mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */); mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
} }
}); });
@@ -927,6 +928,8 @@ public class MediaControlPanel {
mSmartspaceId = SmallHash.hash(data.getTargetId()); mSmartspaceId = SmallHash.hash(data.getTargetId());
mBackgroundColor = data.getBackgroundColor(); mBackgroundColor = data.getBackgroundColor();
mPackageName = data.getPackageName();
mInstanceId = data.getInstanceId();
TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations(); TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations();
recommendationCard.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); recommendationCard.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
@@ -1055,6 +1058,7 @@ public class MediaControlPanel {
mRecommendationViewHolder.getDismiss().setOnClickListener(v -> { mRecommendationViewHolder.getDismiss().setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
mLogger.logLongPressDismiss(mUid, mPackageName, mInstanceId);
logSmartspaceCardReported( logSmartspaceCardReported(
761 // SMARTSPACE_CARD_DISMISS 761 // SMARTSPACE_CARD_DISMISS
); );
@@ -1191,6 +1195,11 @@ public class MediaControlPanel {
view.setOnClickListener(v -> { view.setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
if (interactedSubcardRank == -1) {
mLogger.logRecommendationCardTap(mPackageName, mInstanceId);
} else {
mLogger.logRecommendationItemTap(mPackageName, mInstanceId, interactedSubcardRank);
}
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT, logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
interactedSubcardRank, interactedSubcardRank,
getSmartspaceSubCardCardinality()); getSmartspaceSubCardCardinality());

View File

@@ -60,7 +60,8 @@ class MediaDataFilter @Inject constructor(
private val broadcastSender: BroadcastSender, private val broadcastSender: BroadcastSender,
private val lockscreenUserManager: NotificationLockscreenUserManager, private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor, @Main private val executor: Executor,
private val systemClock: SystemClock private val systemClock: SystemClock,
private val logger: MediaUiEventLogger
) : MediaDataManager.Listener { ) : MediaDataManager.Listener {
private val userTracker: CurrentUserTracker private val userTracker: CurrentUserTracker
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
@@ -151,6 +152,8 @@ class MediaDataFilter @Inject constructor(
Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
reactivatedKey = lastActiveKey reactivatedKey = lastActiveKey
val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName,
mediaData.instanceId)
listeners.forEach { listeners.forEach {
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData,
receivedSmartspaceCardLatency = receivedSmartspaceCardLatency =
@@ -167,6 +170,8 @@ class MediaDataFilter @Inject constructor(
Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
return return
} }
logger.logRecommendationAdded(smartspaceMediaData.packageName,
smartspaceMediaData.instanceId)
listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
} }
@@ -197,7 +202,9 @@ class MediaDataFilter @Inject constructor(
if (smartspaceMediaData.isActive) { if (smartspaceMediaData.isActive) {
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid) targetId = smartspaceMediaData.targetId,
isValid = smartspaceMediaData.isValid,
instanceId = smartspaceMediaData.instanceId)
} }
listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
} }
@@ -252,9 +259,12 @@ class MediaDataFilter @Inject constructor(
broadcastSender.sendBroadcast(dismissIntent) broadcastSender.sendBroadcast(dismissIntent)
} }
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid) targetId = smartspaceMediaData.targetId,
isValid = smartspaceMediaData.isValid,
instanceId = smartspaceMediaData.instanceId)
mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId,
delay = 0L)
} }
mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, delay = 0L)
} }
/** /**

View File

@@ -103,8 +103,17 @@ private val LOADING = MediaData(
appUid = Process.INVALID_UID) appUid = Process.INVALID_UID)
@VisibleForTesting @VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData("INVALID", false, false, internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData(
"INVALID", null, emptyList(), null, 0, 0) targetId = "INVALID",
isActive = false,
isValid = false,
packageName = "INVALID",
cardAction = null,
recommendations = emptyList(),
dismissIntent = null,
backgroundColor = 0,
headphoneConnectionTimeMillis = 0,
instanceId = InstanceId.fakeInstanceId(-1))
fun isMediaNotification(sbn: StatusBarNotification): Boolean { fun isMediaNotification(sbn: StatusBarNotification): Boolean {
return sbn.notification.isMediaNotification() return sbn.notification.isMediaNotification()
@@ -528,14 +537,16 @@ class MediaDataManager(
* connection session. * connection session.
*/ */
fun dismissSmartspaceRecommendation(key: String, delay: Long) { fun dismissSmartspaceRecommendation(key: String, delay: Long) {
if (smartspaceMediaData.targetId != key) { if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid) {
// If this doesn't match, or we've already invalidated the data, no action needed
return return
} }
if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
if (smartspaceMediaData.isActive) { if (smartspaceMediaData.isActive) {
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId) targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId)
} }
foregroundExecutor.executeDelayed( foregroundExecutor.executeDelayed(
{ notifySmartspaceMediaDataRemoved( { notifySmartspaceMediaDataRemoved(
@@ -1010,7 +1021,8 @@ class MediaDataManager(
Log.d(TAG, "Set Smartspace media to be inactive for the data update") Log.d(TAG, "Set Smartspace media to be inactive for the data update")
} }
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId) targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId)
notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
} }
1 -> { 1 -> {
@@ -1189,15 +1201,24 @@ class MediaDataManager(
.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
} }
packageName(target)?.let { packageName(target)?.let {
return SmartspaceMediaData(target.smartspaceTargetId, isActive, true, it, return SmartspaceMediaData(
target.baseAction, target.iconGrid, targetId = target.smartspaceTargetId,
dismissIntent, 0, target.creationTimeMillis) isActive = isActive,
isValid = true,
packageName = it,
cardAction = target.baseAction,
recommendations = target.iconGrid,
dismissIntent = dismissIntent,
backgroundColor = 0,
headphoneConnectionTimeMillis = target.creationTimeMillis,
instanceId = logger.getNewInstanceId())
} }
return EMPTY_SMARTSPACE_MEDIA_DATA return EMPTY_SMARTSPACE_MEDIA_DATA
.copy(targetId = target.smartspaceTargetId, .copy(targetId = target.smartspaceTargetId,
isActive = isActive, isActive = isActive,
dismissIntent = dismissIntent, dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis) headphoneConnectionTimeMillis = target.creationTimeMillis,
instanceId = logger.getNewInstanceId())
} }
private fun packageName(target: SmartspaceTarget): String? { private fun packageName(target: SmartspaceTarget): String? {

View File

@@ -151,6 +151,31 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
} }
logger.log(event) logger.log(event)
} }
fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName,
instanceId)
}
fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName,
instanceId)
}
fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName,
instanceId)
}
fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0,
packageName, instanceId, position)
}
fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName,
instanceId)
}
} }
enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
@@ -233,7 +258,22 @@ enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039), MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
@UiEvent(doc = "The media carousel moved to the dream state") @UiEvent(doc = "The media carousel moved to the dream state")
MEDIA_CAROUSEL_LOCATION_DREAM(1040); MEDIA_CAROUSEL_LOCATION_DREAM(1040),
@UiEvent(doc = "A media recommendation card was added to the media carousel")
MEDIA_RECOMMENDATION_ADDED(1041),
@UiEvent(doc = "A media recommendation card was removed from the media carousel")
MEDIA_RECOMMENDATION_REMOVED(1042),
@UiEvent(doc = "An existing media control was made active as a recommendation")
MEDIA_RECOMMENDATION_ACTIVATED(1043),
@UiEvent(doc = "User tapped on an item in a media recommendation card")
MEDIA_RECOMMENDATION_ITEM_TAP(1044),
@UiEvent(doc = "User tapped on a media recommendation card")
MEDIA_RECOMMENDATION_CARD_TAP(1045);
override fun getId() = metricId override fun getId() = metricId
} }

View File

@@ -18,6 +18,7 @@ package com.android.systemui.media
import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceAction
import android.content.Intent import android.content.Intent
import com.android.internal.logging.InstanceId
/** State of a Smartspace media recommendations view. */ /** State of a Smartspace media recommendations view. */
data class SmartspaceMediaData( data class SmartspaceMediaData(
@@ -56,5 +57,9 @@ data class SmartspaceMediaData(
/** /**
* The timestamp in milliseconds that headphone is connected. * The timestamp in milliseconds that headphone is connected.
*/ */
val headphoneConnectionTimeMillis: Long val headphoneConnectionTimeMillis: Long,
/**
* Instance ID for [MediaUiEventLogger]
*/
val instanceId: InstanceId
) )

View File

@@ -19,6 +19,7 @@ package com.android.systemui.media
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
@@ -28,6 +29,7 @@ import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue import junit.framework.Assert.assertTrue
@@ -225,4 +227,19 @@ class MediaCarouselControllerTest : SysuiTestCase() {
animate = false) animate = false)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
} }
@Test
fun testRecommendationRemoved_logged() {
val packageName = "smartspace package"
val instanceId = InstanceId.fakeInstanceId(123)
val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
packageName = packageName,
instanceId = instanceId
)
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
mediaCarouselController.removePlayer(SMARTSPACE_KEY)
verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
}
} }

View File

@@ -17,16 +17,22 @@
package com.android.systemui.media package com.android.systemui.media
import android.app.PendingIntent import android.app.PendingIntent
import android.app.smartspace.SmartspaceAction
import android.content.Context
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Animatable2 import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.Icon
import android.graphics.drawable.RippleDrawable import android.graphics.drawable.RippleDrawable
import android.media.MediaMetadata import android.media.MediaMetadata
import android.media.session.MediaSession import android.media.session.MediaSession
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
@@ -67,6 +73,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.any import org.mockito.Mockito.any
import org.mockito.Mockito.anyString
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
@@ -107,6 +114,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var collapsedSet: ConstraintSet @Mock private lateinit var collapsedSet: ConstraintSet
@Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
@Mock private lateinit var mediaCarouselController: MediaCarouselController @Mock private lateinit var mediaCarouselController: MediaCarouselController
@Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
@Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingManager: FalsingManager
@Mock private lateinit var transitionParent: ViewGroup @Mock private lateinit var transitionParent: ViewGroup
private lateinit var appIcon: ImageView private lateinit var appIcon: ImageView
@@ -145,6 +153,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
private val clock = FakeSystemClock() private val clock = FakeSystemClock()
@Mock private lateinit var logger: MediaUiEventLogger @Mock private lateinit var logger: MediaUiEventLogger
@Mock private lateinit var instanceId: InstanceId @Mock private lateinit var instanceId: InstanceId
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var applicationInfo: ApplicationInfo
@Mock private lateinit var recommendationViewHolder: RecommendationViewHolder
@Mock private lateinit var smartspaceAction: SmartspaceAction
private lateinit var smartspaceData: SmartspaceMediaData
@Mock private lateinit var coverContainer: ViewGroup
private lateinit var coverItem: ImageView
@JvmField @Rule val mockito = MockitoJUnit.rule() @JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -155,6 +171,16 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(mediaViewController.expandedLayout).thenReturn(expandedSet) whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
// Set up package manager mocks
val icon = context.getDrawable(R.drawable.ic_android)
whenever(packageManager.getApplicationIcon(anyString())).thenReturn(icon)
whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
.thenReturn(icon)
whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
.thenReturn(applicationInfo)
whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
context.setMockPackageManager(packageManager)
player = MediaControlPanel( player = MediaControlPanel(
context, context,
bgExecutor, bgExecutor,
@@ -170,7 +196,60 @@ public class MediaControlPanelTest : SysuiTestCase() {
clock, clock,
logger logger
) )
initMediaViewHolderMocks()
// Create media session
val metadataBuilder = MediaMetadata.Builder().apply {
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
val playbackBuilder = PlaybackState.Builder().apply {
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
setActions(PlaybackState.ACTION_PLAY)
}
session = MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
session.setActive(true)
mediaData = MediaTestUtils.emptyMediaData.copy(
backgroundColor = BG_COLOR,
artist = ARTIST,
song = TITLE,
packageName = PACKAGE,
token = session.sessionToken,
device = device,
instanceId = instanceId)
// Set up recommendation view
initRecommendationViewHolderMocks()
// Set valid recommendation data
val extras = Bundle()
val intent = Intent().apply {
putExtras(extras)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
whenever(smartspaceAction.intent).thenReturn(intent)
whenever(smartspaceAction.extras).thenReturn(extras)
smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
packageName = PACKAGE,
instanceId = instanceId,
recommendations = listOf(smartspaceAction),
cardAction = smartspaceAction
)
}
/**
* Initialize elements in media view holder
*/
private fun initMediaViewHolderMocks() {
whenever(seekBarViewModel.progress).thenReturn(seekBarData) whenever(seekBarViewModel.progress).thenReturn(seekBarData)
whenever(mediaCarouselController.mediaCarouselScrollHandler)
.thenReturn(mediaCarouselScrollHandler)
whenever(mediaCarouselScrollHandler.qsExpanded).thenReturn(false)
// Set up mock views for the players // Set up mock views for the players
appIcon = ImageView(context) appIcon = ImageView(context)
@@ -218,37 +297,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
action4.id) action4.id)
} }
initMediaViewHolderMocks()
// Create media session
val metadataBuilder = MediaMetadata.Builder().apply {
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
val playbackBuilder = PlaybackState.Builder().apply {
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
setActions(PlaybackState.ACTION_PLAY)
}
session = MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
session.setActive(true)
mediaData = MediaTestUtils.emptyMediaData.copy(
backgroundColor = BG_COLOR,
artist = ARTIST,
song = TITLE,
packageName = PACKAGE,
token = session.sessionToken,
device = device,
instanceId = instanceId)
}
/**
* Initialize elements in media view holder
*/
private fun initMediaViewHolderMocks() {
whenever(viewHolder.player).thenReturn(view) whenever(viewHolder.player).thenReturn(view)
whenever(viewHolder.appIcon).thenReturn(appIcon) whenever(viewHolder.appIcon).thenReturn(appIcon)
whenever(viewHolder.albumView).thenReturn(albumView) whenever(viewHolder.albumView).thenReturn(albumView)
@@ -297,6 +345,38 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
} }
/**
* Initialize elements for the recommendation view holder
*/
private fun initRecommendationViewHolderMocks() {
whenever(recommendationViewHolder.recommendations).thenReturn(view)
whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon)
whenever(recommendationViewHolder.cardText).thenReturn(titleText)
// Add a recommendation item
coverItem = ImageView(context).also { it.setId(R.id.media_cover1) }
whenever(coverContainer.id).thenReturn(R.id.media_cover1_container)
whenever(recommendationViewHolder.mediaCoverItems).thenReturn(listOf(coverItem))
whenever(recommendationViewHolder.mediaCoverContainers).thenReturn(listOf(coverContainer))
whenever(recommendationViewHolder.mediaCoverItemsResIds)
.thenReturn(listOf(R.id.media_cover1))
whenever(recommendationViewHolder.mediaCoverContainersResIds)
.thenReturn(listOf(R.id.media_cover1_container))
// Long press menu
whenever(recommendationViewHolder.settings).thenReturn(settings)
whenever(recommendationViewHolder.cancel).thenReturn(cancel)
whenever(recommendationViewHolder.dismiss).thenReturn(dismiss)
val actionIcon = Icon.createWithResource(context, R.drawable.ic_android)
whenever(smartspaceAction.icon).thenReturn(actionIcon)
// Needed for card and item action click
val mockContext = mock(Context::class.java)
whenever(view.context).thenReturn(mockContext)
whenever(coverContainer.context).thenReturn(mockContext)
}
@After @After
fun tearDown() { fun tearDown() {
session.release() session.release()
@@ -881,6 +961,67 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId)) verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
} }
@Test
fun recommendation_gutsClosed_longPressOpens() {
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(smartspaceData)
whenever(mediaViewController.isGutsVisible).thenReturn(false)
val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
verify(recommendationViewHolder.recommendations).setOnLongClickListener(captor.capture())
captor.value.onLongClick(recommendationViewHolder.recommendations)
verify(mediaViewController).openGuts()
verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
}
@Test
fun recommendation_settingsButtonClick_isLogged() {
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(smartspaceData)
settings.callOnClick()
verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
val captor = ArgumentCaptor.forClass(Intent::class.java)
verify(activityStarter).startActivity(captor.capture(), eq(true))
assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
}
@Test
fun recommendation_dismissButton_isLogged() {
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(smartspaceData)
dismiss.callOnClick()
verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
}
@Test
fun recommendation_tapOnCard_isLogged() {
val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(smartspaceData)
verify(recommendationViewHolder.recommendations).setOnClickListener(captor.capture())
captor.value.onClick(recommendationViewHolder.recommendations)
verify(logger).logRecommendationCardTap(eq(PACKAGE), eq(instanceId))
}
@Test
fun recommendation_tapOnItem_isLogged() {
val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(smartspaceData)
verify(coverContainer).setOnClickListener(captor.capture())
captor.value.onClick(recommendationViewHolder.recommendations)
verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0))
}
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) } withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
} }

View File

@@ -20,6 +20,7 @@ import android.app.smartspace.SmartspaceAction
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.broadcast.BroadcastSender
@@ -46,7 +47,11 @@ private const val KEY_ALT = "TEST_KEY_2"
private const val USER_MAIN = 0 private const val USER_MAIN = 0
private const val USER_GUEST = 10 private const val USER_GUEST = 10
private const val PACKAGE = "PKG" private const val PACKAGE = "PKG"
private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
private const val APP_UID = 99
private const val SMARTSPACE_KEY = "SMARTSPACE_KEY" private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -69,6 +74,8 @@ class MediaDataFilterTest : SysuiTestCase() {
private lateinit var smartspaceData: SmartspaceMediaData private lateinit var smartspaceData: SmartspaceMediaData
@Mock @Mock
private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock
private lateinit var logger: MediaUiEventLogger
private lateinit var mediaDataFilter: MediaDataFilter private lateinit var mediaDataFilter: MediaDataFilter
private lateinit var dataMain: MediaData private lateinit var dataMain: MediaData
@@ -79,8 +86,14 @@ class MediaDataFilterTest : SysuiTestCase() {
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
MediaPlayerData.clear() MediaPlayerData.clear()
mediaDataFilter = MediaDataFilter(context, broadcastDispatcher, broadcastSender, mediaDataFilter = MediaDataFilter(
lockscreenUserManager, executor, clock) context,
broadcastDispatcher,
broadcastSender,
lockscreenUserManager,
executor,
clock,
logger)
mediaDataFilter.mediaDataManager = mediaDataManager mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener) mediaDataFilter.addListener(listener)
@@ -90,16 +103,19 @@ class MediaDataFilterTest : SysuiTestCase() {
// Set up test media data // Set up test media data
dataMain = MediaTestUtils.emptyMediaData.copy( dataMain = MediaTestUtils.emptyMediaData.copy(
userId = USER_MAIN, userId = USER_MAIN,
packageName = PACKAGE) packageName = PACKAGE,
instanceId = INSTANCE_ID,
appUid = APP_UID)
dataGuest = dataMain.copy(userId = USER_GUEST) dataGuest = dataMain.copy(userId = USER_GUEST)
`when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
`when`(smartspaceData.isActive).thenReturn(true) `when`(smartspaceData.isActive).thenReturn(true)
`when`(smartspaceData.isValid).thenReturn(true) `when`(smartspaceData.isValid).thenReturn(true)
`when`(smartspaceData.packageName).thenReturn(PACKAGE) `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
`when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem)) `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
`when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn( `when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn(
clock.currentTimeMillis() - 100) clock.currentTimeMillis() - 100)
`when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
} }
private fun setUser(id: Int) { private fun setUser(id: Int) {
@@ -241,6 +257,8 @@ class MediaDataFilterTest : SysuiTestCase() {
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
verify(logger, never()).logRecommendationActivated(any(), any(), any())
} }
@Test @Test
@@ -254,6 +272,8 @@ class MediaDataFilterTest : SysuiTestCase() {
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger, never()).logRecommendationAdded(any(), any())
verify(logger, never()).logRecommendationActivated(any(), any(), any())
} }
@Test @Test
@@ -267,6 +287,8 @@ class MediaDataFilterTest : SysuiTestCase() {
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
verify(logger, never()).logRecommendationActivated(any(), any(), any())
} }
@Test @Test
@@ -281,6 +303,8 @@ class MediaDataFilterTest : SysuiTestCase() {
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger, never()).logRecommendationAdded(any(), any())
verify(logger, never()).logRecommendationActivated(any(), any(), any())
} }
@Test @Test
@@ -302,6 +326,8 @@ class MediaDataFilterTest : SysuiTestCase() {
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger, never()).logRecommendationAdded(any(), any())
verify(logger, never()).logRecommendationActivated(any(), any(), any())
} }
@Test @Test
@@ -324,6 +350,8 @@ class MediaDataFilterTest : SysuiTestCase() {
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
// Smartspace update shouldn't be propagated for the empty rec list. // Smartspace update shouldn't be propagated for the empty rec list.
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
verify(logger, never()).logRecommendationAdded(any(), any())
verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
} }
@Test @Test
@@ -345,6 +373,8 @@ class MediaDataFilterTest : SysuiTestCase() {
// Smartspace update should also be propagated but not prioritized. // Smartspace update should also be propagated but not prioritized.
verify(listener) verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
} }
@Test @Test

View File

@@ -18,6 +18,7 @@ import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
@@ -46,6 +47,7 @@ import org.mockito.Mockito
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
@@ -451,11 +453,22 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test @Test
fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() { fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
verify(logger).getNewInstanceId()
val instanceId = instanceIdSequence.lastInstanceId
verify(listener).onSmartspaceMediaDataLoaded( verify(listener).onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE), eq(KEY_MEDIA_SMARTSPACE),
eq(SmartspaceMediaData(KEY_MEDIA_SMARTSPACE, true /* isActive */, true /*isValid */, eq(SmartspaceMediaData(
PACKAGE_NAME, mediaSmartspaceBaseAction, listOf(mediaRecommendationItem), targetId = KEY_MEDIA_SMARTSPACE,
DISMISS_INTENT, 0, 1234L)), isActive = true,
isValid = true,
packageName = PACKAGE_NAME,
cardAction = mediaSmartspaceBaseAction,
recommendations = listOf(mediaRecommendationItem),
dismissIntent = DISMISS_INTENT,
backgroundColor = 0,
headphoneConnectionTimeMillis = 1234L,
instanceId = InstanceId.fakeInstanceId(instanceId))),
eq(false)) eq(false))
} }
@@ -463,12 +476,18 @@ class MediaDataManagerTest : SysuiTestCase() {
fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() { fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
verify(logger).getNewInstanceId()
val instanceId = instanceIdSequence.lastInstanceId
verify(listener).onSmartspaceMediaDataLoaded( verify(listener).onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE), eq(KEY_MEDIA_SMARTSPACE),
eq(EMPTY_SMARTSPACE_MEDIA_DATA eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy(
.copy(targetId = KEY_MEDIA_SMARTSPACE, isActive = true, targetId = KEY_MEDIA_SMARTSPACE,
isValid = false, dismissIntent = DISMISS_INTENT, isActive = true,
headphoneConnectionTimeMillis = 1234L)), isValid = false,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = 1234L,
instanceId = InstanceId.fakeInstanceId(instanceId))),
eq(false)) eq(false))
} }
@@ -483,18 +502,25 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
verify(logger).getNewInstanceId()
val instanceId = instanceIdSequence.lastInstanceId
verify(listener).onSmartspaceMediaDataLoaded( verify(listener).onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE), eq(KEY_MEDIA_SMARTSPACE),
eq(EMPTY_SMARTSPACE_MEDIA_DATA eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy(
.copy(targetId = KEY_MEDIA_SMARTSPACE, isActive = true, targetId = KEY_MEDIA_SMARTSPACE,
isValid = false, dismissIntent = null, headphoneConnectionTimeMillis = 1234L)), isActive = true,
isValid = false,
dismissIntent = null,
headphoneConnectionTimeMillis = 1234L,
instanceId = InstanceId.fakeInstanceId(instanceId))),
eq(false)) eq(false))
} }
@Test @Test
fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() { fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
smartspaceMediaDataProvider.onTargetsAvailable(listOf()) smartspaceMediaDataProvider.onTargetsAvailable(listOf())
verify(logger, never()).getNewInstanceId()
verify(listener, never()) verify(listener, never())
.onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
} }
@@ -502,11 +528,14 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test @Test
fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() { fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
verify(logger).getNewInstanceId()
smartspaceMediaDataProvider.onTargetsAvailable(listOf()) smartspaceMediaDataProvider.onTargetsAvailable(listOf())
foregroundExecutor.advanceClockToLast() foregroundExecutor.advanceClockToLast()
foregroundExecutor.runAllReady() foregroundExecutor.runAllReady()
verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
verifyNoMoreInteractions(logger)
} }
@Test @Test