Merge "Add logging for media controls" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
9628692e6c
@@ -12,6 +12,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.internal.logging.InstanceId
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.classifier.FalsingCollector
|
||||
@@ -59,7 +60,7 @@ class MediaCarouselController @Inject constructor(
|
||||
falsingCollector: FalsingCollector,
|
||||
falsingManager: FalsingManager,
|
||||
dumpManager: DumpManager,
|
||||
private val mediaFlags: MediaFlags
|
||||
private val logger: MediaUiEventLogger
|
||||
) : Dumpable {
|
||||
/**
|
||||
* The current width of the carousel
|
||||
@@ -119,7 +120,9 @@ class MediaCarouselController @Inject constructor(
|
||||
private val mediaCarousel: MediaScrollView
|
||||
val mediaCarouselScrollHandler: MediaCarouselScrollHandler
|
||||
val mediaFrame: ViewGroup
|
||||
private lateinit var settingsButton: View
|
||||
@VisibleForTesting
|
||||
lateinit var settingsButton: View
|
||||
private set
|
||||
private val mediaContent: ViewGroup
|
||||
private val pageIndicator: PageIndicator
|
||||
private val visualStabilityCallback: OnReorderingAllowedListener
|
||||
@@ -183,7 +186,8 @@ class MediaCarouselController @Inject constructor(
|
||||
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
|
||||
mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
|
||||
executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation,
|
||||
this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression)
|
||||
this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression,
|
||||
logger)
|
||||
isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
inflateSettingsButton()
|
||||
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
|
||||
@@ -220,7 +224,7 @@ class MediaCarouselController @Inject constructor(
|
||||
MediaPlayerData.getMediaPlayer(key)?.let {
|
||||
/* ktlint-disable max-line-length */
|
||||
logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
|
||||
it.mInstanceId,
|
||||
it.mSmartspaceId,
|
||||
it.mUid,
|
||||
/* isRecommendationCard */ false,
|
||||
intArrayOf(
|
||||
@@ -239,12 +243,12 @@ class MediaCarouselController @Inject constructor(
|
||||
// resume card is ranked first
|
||||
MediaPlayerData.players().forEachIndexed { index, it ->
|
||||
if (it.recommendationViewHolder == null) {
|
||||
it.mInstanceId = SmallHash.hash(it.mUid +
|
||||
it.mSmartspaceId = SmallHash.hash(it.mUid +
|
||||
systemClock.currentTimeMillis().toInt())
|
||||
it.mIsImpressed = false
|
||||
/* ktlint-disable max-line-length */
|
||||
logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
|
||||
it.mInstanceId,
|
||||
it.mSmartspaceId,
|
||||
it.mUid,
|
||||
/* isRecommendationCard */ false,
|
||||
intArrayOf(
|
||||
@@ -291,12 +295,12 @@ class MediaCarouselController @Inject constructor(
|
||||
// recommendation card is valid and ranked first
|
||||
MediaPlayerData.players().forEachIndexed { index, it ->
|
||||
if (it.recommendationViewHolder == null) {
|
||||
it.mInstanceId = SmallHash.hash(it.mUid +
|
||||
it.mSmartspaceId = SmallHash.hash(it.mUid +
|
||||
systemClock.currentTimeMillis().toInt())
|
||||
it.mIsImpressed = false
|
||||
/* ktlint-disable max-line-length */
|
||||
logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
|
||||
it.mInstanceId,
|
||||
it.mSmartspaceId,
|
||||
it.mUid,
|
||||
/* isRecommendationCard */ false,
|
||||
intArrayOf(
|
||||
@@ -312,7 +316,7 @@ class MediaCarouselController @Inject constructor(
|
||||
MediaPlayerData.getMediaPlayer(key)?.let {
|
||||
/* ktlint-disable max-line-length */
|
||||
logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
|
||||
it.mInstanceId,
|
||||
it.mSmartspaceId,
|
||||
it.mUid,
|
||||
/* isRecommendationCard */ true,
|
||||
intArrayOf(
|
||||
@@ -369,6 +373,7 @@ class MediaCarouselController @Inject constructor(
|
||||
mediaFrame.addView(settingsButton)
|
||||
mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
|
||||
settingsButton.setOnClickListener {
|
||||
logger.logCarouselSettings()
|
||||
activityStarter.startActivity(settingsIntent, true /* dismissShade */)
|
||||
}
|
||||
}
|
||||
@@ -752,7 +757,7 @@ class MediaCarouselController @Inject constructor(
|
||||
return
|
||||
}
|
||||
logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
|
||||
mediaControlPanel.mInstanceId,
|
||||
mediaControlPanel.mSmartspaceId,
|
||||
mediaControlPanel.mUid,
|
||||
isRecommendationCard,
|
||||
intArrayOf(mediaControlPanel.surfaceForSmartspaceLogging))
|
||||
@@ -836,7 +841,7 @@ class MediaCarouselController @Inject constructor(
|
||||
index, it ->
|
||||
if (it.mIsImpressed) {
|
||||
logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
|
||||
it.mInstanceId,
|
||||
it.mSmartspaceId,
|
||||
it.mUid,
|
||||
it.recommendationViewHolder != null,
|
||||
intArrayOf(it.surfaceForSmartspaceLogging),
|
||||
@@ -846,6 +851,7 @@ class MediaCarouselController @Inject constructor(
|
||||
it.mIsImpressed = false
|
||||
}
|
||||
}
|
||||
logger.logSwipeDismiss()
|
||||
mediaManager.onSwipeToDismiss()
|
||||
}
|
||||
|
||||
@@ -881,7 +887,9 @@ internal object MediaPlayerData {
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
resumeAction = null,
|
||||
instanceId = InstanceId.fakeInstanceId(-1),
|
||||
appUid = -1)
|
||||
// Whether should prioritize Smartspace card.
|
||||
internal var shouldPrioritizeSs: Boolean = false
|
||||
private set
|
||||
|
||||
@@ -57,12 +57,13 @@ class MediaCarouselScrollHandler(
|
||||
private val scrollView: MediaScrollView,
|
||||
private val pageIndicator: PageIndicator,
|
||||
private val mainExecutor: DelayableExecutor,
|
||||
private val dismissCallback: () -> Unit,
|
||||
val dismissCallback: () -> Unit,
|
||||
private var translationChangedListener: () -> Unit,
|
||||
private val closeGuts: (immediate: Boolean) -> Unit,
|
||||
private val falsingCollector: FalsingCollector,
|
||||
private val falsingManager: FalsingManager,
|
||||
private val logSmartspaceImpression: (Boolean) -> Unit
|
||||
private val logSmartspaceImpression: (Boolean) -> Unit,
|
||||
private val logger: MediaUiEventLogger
|
||||
) {
|
||||
/**
|
||||
* Is the view in RTL
|
||||
@@ -476,6 +477,7 @@ class MediaCarouselScrollHandler(
|
||||
visibleMediaIndex = newIndex
|
||||
if (oldIndex != visibleMediaIndex && visibleToUser) {
|
||||
logSmartspaceImpression(qsExpanded)
|
||||
logger.logMediaCarouselPage(newIndex)
|
||||
}
|
||||
closeGuts(false)
|
||||
updatePlayerVisibilities()
|
||||
|
||||
@@ -53,6 +53,7 @@ import androidx.annotation.UiThread;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
|
||||
import com.android.internal.jank.InteractionJankMonitor;
|
||||
import com.android.internal.logging.InstanceId;
|
||||
import com.android.settingslib.widget.AdaptiveIcon;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
@@ -131,8 +132,6 @@ public class MediaControlPanel {
|
||||
private MediaController mController;
|
||||
private Lazy<MediaDataManager> mMediaDataManagerLazy;
|
||||
private int mBackgroundColor;
|
||||
// Instance id for logging purpose.
|
||||
protected int mInstanceId = -1;
|
||||
// Uid for the media app.
|
||||
protected int mUid = Process.INVALID_UID;
|
||||
private int mSmartspaceMediaItemsCount;
|
||||
@@ -140,9 +139,13 @@ public class MediaControlPanel {
|
||||
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
|
||||
private final FalsingManager mFalsingManager;
|
||||
|
||||
// Used for swipe-to-dismiss logging.
|
||||
// Used for logging.
|
||||
protected boolean mIsImpressed = false;
|
||||
private SystemClock mSystemClock;
|
||||
private MediaUiEventLogger mLogger;
|
||||
private InstanceId mInstanceId;
|
||||
protected int mSmartspaceId = -1;
|
||||
private String mPackageName;
|
||||
|
||||
/**
|
||||
* Initialize a new control panel
|
||||
@@ -157,7 +160,7 @@ public class MediaControlPanel {
|
||||
Lazy<MediaDataManager> lazyMediaDataManager,
|
||||
MediaOutputDialogFactory mediaOutputDialogFactory,
|
||||
MediaCarouselController mediaCarouselController,
|
||||
FalsingManager falsingManager, SystemClock systemClock) {
|
||||
FalsingManager falsingManager, SystemClock systemClock, MediaUiEventLogger logger) {
|
||||
mContext = context;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
mActivityStarter = activityStarter;
|
||||
@@ -169,8 +172,12 @@ public class MediaControlPanel {
|
||||
mMediaCarouselController = mediaCarouselController;
|
||||
mFalsingManager = falsingManager;
|
||||
mSystemClock = systemClock;
|
||||
mLogger = logger;
|
||||
|
||||
mSeekBarViewModel.setLogSmartspaceClick(() -> {
|
||||
mSeekBarViewModel.setLogSeek(() -> {
|
||||
if (mPackageName != null && mInstanceId != null) {
|
||||
mLogger.logSeek(mUid, mPackageName, mInstanceId);
|
||||
}
|
||||
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
|
||||
/* isRecommendationCard */ false);
|
||||
return Unit.INSTANCE;
|
||||
@@ -261,6 +268,7 @@ public class MediaControlPanel {
|
||||
});
|
||||
vh.getSettings().setOnClickListener(v -> {
|
||||
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
mLogger.logLongPressSettings(mUid, mPackageName, mInstanceId);
|
||||
mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
|
||||
}
|
||||
});
|
||||
@@ -301,16 +309,13 @@ public class MediaControlPanel {
|
||||
}
|
||||
mKey = key;
|
||||
MediaSession.Token token = data.getToken();
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
try {
|
||||
mUid = packageManager.getApplicationInfo(data.getPackageName(), 0 /* flags */).uid;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Unable to look up package name", e);
|
||||
}
|
||||
mPackageName = data.getPackageName();
|
||||
mUid = data.getAppUid();
|
||||
// Only assigns instance id if it's unassigned.
|
||||
if (mInstanceId == -1) {
|
||||
mInstanceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis());
|
||||
if (mSmartspaceId == -1) {
|
||||
mSmartspaceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis());
|
||||
}
|
||||
mInstanceId = data.getInstanceId();
|
||||
|
||||
mBackgroundColor = data.getBackgroundColor();
|
||||
if (mToken == null || !mToken.equals(token)) {
|
||||
@@ -401,6 +406,7 @@ public class MediaControlPanel {
|
||||
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return;
|
||||
}
|
||||
mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
|
||||
if (device.getIntent() != null) {
|
||||
if (device.getIntent().isActivity()) {
|
||||
mActivityStarter.startActivity(
|
||||
@@ -413,7 +419,7 @@ public class MediaControlPanel {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mMediaOutputDialogFactory.create(data.getPackageName(), true,
|
||||
mMediaOutputDialogFactory.create(mPackageName, true,
|
||||
mMediaViewHolder.getSeamlessButton());
|
||||
}
|
||||
});
|
||||
@@ -437,6 +443,7 @@ public class MediaControlPanel {
|
||||
|
||||
logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
|
||||
/* isRecommendationCard */ false);
|
||||
mLogger.logLongPressDismiss(mUid, mPackageName, mInstanceId);
|
||||
|
||||
if (mKey != null) {
|
||||
closeGuts();
|
||||
@@ -675,6 +682,7 @@ public class MediaControlPanel {
|
||||
button.setEnabled(true);
|
||||
button.setOnClickListener(v -> {
|
||||
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
|
||||
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
|
||||
/* isRecommendationCard */ false);
|
||||
action.run();
|
||||
@@ -794,7 +802,7 @@ public class MediaControlPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
mInstanceId = SmallHash.hash(data.getTargetId());
|
||||
mSmartspaceId = SmallHash.hash(data.getTargetId());
|
||||
mBackgroundColor = data.getBackgroundColor();
|
||||
TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations();
|
||||
recommendationCard.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
|
||||
@@ -976,6 +984,7 @@ public class MediaControlPanel {
|
||||
mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
|
||||
}
|
||||
mMediaViewController.openGuts();
|
||||
mLogger.logLongPressOpen(mUid, mPackageName, mInstanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1138,7 +1147,7 @@ public class MediaControlPanel {
|
||||
private void logSmartspaceCardReported(int eventId, boolean isRecommendationCard,
|
||||
int interactedSubcardRank, int interactedSubcardCardinality) {
|
||||
mMediaCarouselController.logSmartspaceCardReported(eventId,
|
||||
mInstanceId,
|
||||
mSmartspaceId,
|
||||
mUid,
|
||||
isRecommendationCard,
|
||||
new int[]{getSurfaceForSmartspaceLogging()},
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.app.PendingIntent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.Icon
|
||||
import android.media.session.MediaSession
|
||||
import com.android.internal.logging.InstanceId
|
||||
import com.android.systemui.R
|
||||
|
||||
/** State of a media view. */
|
||||
@@ -115,7 +116,17 @@ data class MediaData(
|
||||
/**
|
||||
* Timestamp when this player was last active.
|
||||
*/
|
||||
var lastActive: Long = 0L
|
||||
var lastActive: Long = 0L,
|
||||
|
||||
/**
|
||||
* Instance ID for logging purposes
|
||||
*/
|
||||
val instanceId: InstanceId,
|
||||
|
||||
/**
|
||||
* The UID of the app, used for logging
|
||||
*/
|
||||
val appUid: Int
|
||||
) {
|
||||
companion object {
|
||||
/** Media is playing on the local device */
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.drawable.Icon
|
||||
@@ -37,6 +38,7 @@ import android.media.session.MediaSession
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import android.provider.Settings
|
||||
import android.service.notification.StatusBarNotification
|
||||
@@ -44,6 +46,7 @@ import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.internal.logging.InstanceId
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
@@ -94,7 +97,9 @@ private val LOADING = MediaData(
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
resumeAction = null,
|
||||
instanceId = InstanceId.fakeInstanceId(-1),
|
||||
appUid = Process.INVALID_UID)
|
||||
|
||||
@VisibleForTesting
|
||||
internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData("INVALID", false, false,
|
||||
@@ -138,7 +143,8 @@ class MediaDataManager(
|
||||
private val useQsMediaPlayer: Boolean,
|
||||
private val systemClock: SystemClock,
|
||||
private val tunerService: TunerService,
|
||||
private val mediaFlags: MediaFlags
|
||||
private val mediaFlags: MediaFlags,
|
||||
private val logger: MediaUiEventLogger
|
||||
) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
|
||||
|
||||
companion object {
|
||||
@@ -202,12 +208,13 @@ class MediaDataManager(
|
||||
smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
|
||||
clock: SystemClock,
|
||||
tunerService: TunerService,
|
||||
mediaFlags: MediaFlags
|
||||
mediaFlags: MediaFlags,
|
||||
logger: MediaUiEventLogger
|
||||
) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
|
||||
broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
|
||||
mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
|
||||
activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context),
|
||||
Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags)
|
||||
Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags, logger)
|
||||
|
||||
private val appChangeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
@@ -298,17 +305,24 @@ class MediaDataManager(
|
||||
|
||||
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
||||
if (useQsMediaPlayer && isMediaNotification(sbn)) {
|
||||
var logEvent = false
|
||||
Assert.isMainThread()
|
||||
val oldKey = findExistingEntry(key, sbn.packageName)
|
||||
if (oldKey == null) {
|
||||
val temp = LOADING.copy(packageName = sbn.packageName)
|
||||
val instanceId = logger.getNewInstanceId()
|
||||
val temp = LOADING.copy(
|
||||
packageName = sbn.packageName,
|
||||
instanceId = instanceId
|
||||
)
|
||||
mediaEntries.put(key, temp)
|
||||
logEvent = true
|
||||
} else if (oldKey != key) {
|
||||
// Move to new key
|
||||
// Resume -> active conversion; move to new key
|
||||
val oldData = mediaEntries.remove(oldKey)!!
|
||||
logEvent = true
|
||||
mediaEntries.put(key, oldData)
|
||||
}
|
||||
loadMediaData(key, sbn, oldKey)
|
||||
loadMediaData(key, sbn, oldKey, logEvent)
|
||||
} else {
|
||||
onNotificationRemoved(key)
|
||||
}
|
||||
@@ -340,9 +354,23 @@ class MediaDataManager(
|
||||
) {
|
||||
// Resume controls don't have a notification key, so store by package name instead
|
||||
if (!mediaEntries.containsKey(packageName)) {
|
||||
val resumeData = LOADING.copy(packageName = packageName, resumeAction = action,
|
||||
hasCheckedForResume = true)
|
||||
val instanceId = logger.getNewInstanceId()
|
||||
val appUid = try {
|
||||
context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.w(TAG, "Could not get app UID for $packageName", e)
|
||||
Process.INVALID_UID
|
||||
}
|
||||
|
||||
val resumeData = LOADING.copy(
|
||||
packageName = packageName,
|
||||
resumeAction = action,
|
||||
hasCheckedForResume = true,
|
||||
instanceId = instanceId,
|
||||
appUid = appUid
|
||||
)
|
||||
mediaEntries.put(packageName, resumeData)
|
||||
logger.logResumeMediaAdded(appUid, packageName, instanceId)
|
||||
}
|
||||
backgroundExecutor.execute {
|
||||
loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent,
|
||||
@@ -368,10 +396,11 @@ class MediaDataManager(
|
||||
private fun loadMediaData(
|
||||
key: String,
|
||||
sbn: StatusBarNotification,
|
||||
oldKey: String?
|
||||
oldKey: String?,
|
||||
logEvent: Boolean = false
|
||||
) {
|
||||
backgroundExecutor.execute {
|
||||
loadMediaDataInBg(key, sbn, oldKey)
|
||||
loadMediaDataInBg(key, sbn, oldKey, logEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,6 +478,10 @@ class MediaDataManager(
|
||||
*/
|
||||
internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
|
||||
mediaEntries[key]?.let {
|
||||
if (timedOut && !forceUpdate) {
|
||||
// Only log this event when media expires on its own
|
||||
logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
|
||||
}
|
||||
if (it.active == !timedOut && !forceUpdate) {
|
||||
if (it.resumption) {
|
||||
if (DEBUG) Log.d(TAG, "timing out resume player $key")
|
||||
@@ -463,7 +496,9 @@ class MediaDataManager(
|
||||
}
|
||||
|
||||
private fun removeEntry(key: String) {
|
||||
mediaEntries.remove(key)
|
||||
mediaEntries.remove(key)?.let {
|
||||
logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
|
||||
}
|
||||
notifyMediaDataRemoved(key)
|
||||
}
|
||||
|
||||
@@ -537,6 +572,10 @@ class MediaDataManager(
|
||||
null
|
||||
}
|
||||
|
||||
val currentEntry = mediaEntries.get(packageName)
|
||||
val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
|
||||
val appUid = currentEntry?.appUid ?: Process.INVALID_UID
|
||||
|
||||
val mediaAction = getResumeMediaAction(resumeAction)
|
||||
val lastActive = systemClock.elapsedRealtime()
|
||||
foregroundExecutor.execute {
|
||||
@@ -545,14 +584,16 @@ class MediaDataManager(
|
||||
MediaButton(playOrPause = mediaAction), packageName, token, appIntent,
|
||||
device = null, active = false,
|
||||
resumeAction = resumeAction, resumption = true, notificationKey = packageName,
|
||||
hasCheckedForResume = true, lastActive = lastActive))
|
||||
hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId,
|
||||
appUid = appUid))
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMediaDataInBg(
|
||||
key: String,
|
||||
sbn: StatusBarNotification,
|
||||
oldKey: String?
|
||||
oldKey: String?,
|
||||
logEvent: Boolean = false
|
||||
) {
|
||||
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
|
||||
as MediaSession.Token?
|
||||
@@ -636,6 +677,22 @@ class MediaDataManager(
|
||||
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
|
||||
else MediaData.PLAYBACK_CAST_LOCAL
|
||||
val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
|
||||
|
||||
val currentEntry = mediaEntries.get(key)
|
||||
val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
|
||||
val appUid = try {
|
||||
context.packageManager.getApplicationInfo(sbn.packageName, 0)?.uid!!
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.w(TAG, "Could not get app UID for ${sbn.packageName}", e)
|
||||
Process.INVALID_UID
|
||||
}
|
||||
|
||||
if (logEvent) {
|
||||
logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
|
||||
} else if (playbackLocation != currentEntry?.playbackLocation) {
|
||||
logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
|
||||
}
|
||||
|
||||
val lastActive = systemClock.elapsedRealtime()
|
||||
foregroundExecutor.execute {
|
||||
val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
|
||||
@@ -647,7 +704,7 @@ class MediaDataManager(
|
||||
active, resumeAction = resumeAction, playbackLocation = playbackLocation,
|
||||
notificationKey = key, hasCheckedForResume = hasCheckedForResume,
|
||||
isPlaying = isPlaying, isClearable = sbn.isClearable(),
|
||||
lastActive = lastActive))
|
||||
lastActive = lastActive, instanceId = instanceId, appUid = appUid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,10 +1046,12 @@ class MediaDataManager(
|
||||
notifyMediaDataRemoved(key)
|
||||
notifyMediaDataLoaded(pkg, pkg, updated)
|
||||
}
|
||||
logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
|
||||
return
|
||||
}
|
||||
if (removed != null) {
|
||||
notifyMediaDataRemoved(key)
|
||||
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,6 +1068,7 @@ class MediaDataManager(
|
||||
filtered.forEach {
|
||||
mediaEntries.remove(it.key)
|
||||
notifyMediaDataRemoved(it.key)
|
||||
logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import com.android.internal.logging.InstanceId
|
||||
import com.android.internal.logging.InstanceIdSequence
|
||||
import com.android.internal.logging.UiEvent
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import java.lang.IllegalArgumentException
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val INSTANCE_ID_MAX = 1 shl 20
|
||||
|
||||
/**
|
||||
* A helper class to log events related to the media controls
|
||||
*/
|
||||
@SysUISingleton
|
||||
class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) {
|
||||
|
||||
private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX)
|
||||
|
||||
/**
|
||||
* Get a new instance ID for a new media control
|
||||
*/
|
||||
fun getNewInstanceId(): InstanceId {
|
||||
return instanceIdSequence.newInstanceId()
|
||||
}
|
||||
|
||||
fun logActiveMediaAdded(
|
||||
uid: Int,
|
||||
packageName: String,
|
||||
instanceId: InstanceId,
|
||||
playbackLocation: Int
|
||||
) {
|
||||
val event = when (playbackLocation) {
|
||||
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
|
||||
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
|
||||
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
|
||||
else -> throw IllegalArgumentException("Unknown playback location")
|
||||
}
|
||||
logger.logWithInstanceId(event, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logPlaybackLocationChange(
|
||||
uid: Int,
|
||||
packageName: String,
|
||||
instanceId: InstanceId,
|
||||
playbackLocation: Int
|
||||
) {
|
||||
val event = when (playbackLocation) {
|
||||
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
|
||||
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
|
||||
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
|
||||
else -> throw IllegalArgumentException("Unknown playback location")
|
||||
}
|
||||
logger.logWithInstanceId(event, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logResumeMediaAdded(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.RESUME_MEDIA_ADDED, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logActiveConvertedToResume(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.ACTIVE_TO_RESUME, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logMediaTimeout(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.MEDIA_TIMEOUT, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logMediaRemoved(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.MEDIA_REMOVED, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logMediaCarouselPage(position: Int) {
|
||||
// Since this operation is on the carousel, we don't include package information
|
||||
logger.logWithPosition(MediaUiEvent.CAROUSEL_PAGE, 0, null, position)
|
||||
}
|
||||
|
||||
fun logSwipeDismiss() {
|
||||
// Since this operation is on the carousel, we don't include package information
|
||||
logger.log(MediaUiEvent.DISMISS_SWIPE)
|
||||
}
|
||||
|
||||
fun logLongPressOpen(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.OPEN_LONG_PRESS, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logLongPressDismiss(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.DISMISS_LONG_PRESS, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName,
|
||||
instanceId)
|
||||
}
|
||||
|
||||
fun logCarouselSettings() {
|
||||
// Since this operation is on the carousel, we don't include package information
|
||||
logger.log(MediaUiEvent.OPEN_SETTINGS_CAROUSEL)
|
||||
}
|
||||
|
||||
fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
val event = when (buttonId) {
|
||||
R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
|
||||
R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
|
||||
R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
|
||||
else -> MediaUiEvent.TAP_ACTION_OTHER
|
||||
}
|
||||
|
||||
logger.logWithInstanceId(event, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logSeek(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.ACTION_SEEK, uid, packageName, instanceId)
|
||||
}
|
||||
|
||||
fun logOpenOutputSwitcher(uid: Int, packageName: String, instanceId: InstanceId) {
|
||||
logger.logWithInstanceId(MediaUiEvent.OPEN_OUTPUT_SWITCHER, uid, packageName, instanceId)
|
||||
}
|
||||
}
|
||||
|
||||
enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
|
||||
@UiEvent(doc = "A new media control was added for media playing locally on the device")
|
||||
LOCAL_MEDIA_ADDED(1029),
|
||||
|
||||
@UiEvent(doc = "A new media control was added for media cast from the device")
|
||||
CAST_MEDIA_ADDED(1030),
|
||||
|
||||
@UiEvent(doc = "A new media control was added for media playing remotely")
|
||||
REMOTE_MEDIA_ADDED(1031),
|
||||
|
||||
@UiEvent(doc = "The media for an existing control was transferred to local playback")
|
||||
TRANSFER_TO_LOCAL(1032),
|
||||
|
||||
@UiEvent(doc = "The media for an existing control was transferred to a cast device")
|
||||
TRANSFER_TO_CAST(1033),
|
||||
|
||||
@UiEvent(doc = "The media for an existing control was transferred to a remote device")
|
||||
TRANSFER_TO_REMOTE(1034),
|
||||
|
||||
@UiEvent(doc = "A new resumable media control was added")
|
||||
RESUME_MEDIA_ADDED(1013),
|
||||
|
||||
@UiEvent(doc = "An existing active media control was converted into resumable media")
|
||||
ACTIVE_TO_RESUME(1014),
|
||||
|
||||
@UiEvent(doc = "Media timed out")
|
||||
MEDIA_TIMEOUT(1015),
|
||||
|
||||
@UiEvent(doc = "A media control was removed from the carousel")
|
||||
MEDIA_REMOVED(1016),
|
||||
|
||||
@UiEvent(doc = "User swiped to another control within the media carousel")
|
||||
CAROUSEL_PAGE(1017),
|
||||
|
||||
@UiEvent(doc = "The user swiped away the media carousel")
|
||||
DISMISS_SWIPE(1018),
|
||||
|
||||
@UiEvent(doc = "The user opened the long press menu")
|
||||
OPEN_LONG_PRESS(1019),
|
||||
|
||||
@UiEvent(doc = "The user dismissed via long press menu")
|
||||
DISMISS_LONG_PRESS(1020),
|
||||
|
||||
@UiEvent(doc = "The user opened settings from long press menu")
|
||||
OPEN_SETTINGS_LONG_PRESS(1021),
|
||||
|
||||
@UiEvent(doc = "The user opened settings from the carousel")
|
||||
OPEN_SETTINGS_CAROUSEL(1022),
|
||||
|
||||
@UiEvent(doc = "The play/pause button was tapped")
|
||||
TAP_ACTION_PLAY_PAUSE(1023),
|
||||
|
||||
@UiEvent(doc = "The previous button was tapped")
|
||||
TAP_ACTION_PREV(1024),
|
||||
|
||||
@UiEvent(doc = "The next button was tapped")
|
||||
TAP_ACTION_NEXT(1025),
|
||||
|
||||
@UiEvent(doc = "A custom or generic action button was tapped")
|
||||
TAP_ACTION_OTHER(1026),
|
||||
|
||||
@UiEvent(doc = "The user seeked using the seekbar")
|
||||
ACTION_SEEK(1027),
|
||||
|
||||
@UiEvent(doc = "The user opened the output switcher from a media control")
|
||||
OPEN_OUTPUT_SWITCHER(1028);
|
||||
|
||||
override fun getId() = metricId
|
||||
}
|
||||
@@ -131,7 +131,7 @@ class SeekBarViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var logSmartspaceClick: () -> Unit
|
||||
lateinit var logSeek: () -> Unit
|
||||
|
||||
fun getEnabled() = _data.enabled
|
||||
|
||||
@@ -175,7 +175,7 @@ class SeekBarViewModel @Inject constructor(
|
||||
scrubbing = false
|
||||
checkPlaybackPosition()
|
||||
} else {
|
||||
logSmartspaceClick()
|
||||
logSeek()
|
||||
controller?.transportControls?.seekTo(position)
|
||||
// Invalidate the cached playbackState to avoid the thumb jumping back to the previous
|
||||
// position.
|
||||
|
||||
@@ -35,26 +35,11 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import javax.inject.Provider
|
||||
|
||||
private val DATA = MediaData(
|
||||
userId = -1,
|
||||
initialized = false,
|
||||
backgroundColor = 0,
|
||||
app = null,
|
||||
appIcon = null,
|
||||
artist = null,
|
||||
song = null,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = "INVALID",
|
||||
token = null,
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
private val DATA = MediaTestUtils.emptyMediaData
|
||||
|
||||
private val SMARTSPACE_KEY = "smartspace"
|
||||
|
||||
@@ -74,7 +59,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
|
||||
@Mock lateinit var falsingCollector: FalsingCollector
|
||||
@Mock lateinit var falsingManager: FalsingManager
|
||||
@Mock lateinit var dumpManager: DumpManager
|
||||
@Mock lateinit var mediaFlags: MediaFlags
|
||||
@Mock lateinit var logger: MediaUiEventLogger
|
||||
|
||||
private val clock = FakeSystemClock()
|
||||
private lateinit var mediaCarouselController: MediaCarouselController
|
||||
@@ -95,7 +80,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
|
||||
falsingCollector,
|
||||
falsingManager,
|
||||
dumpManager,
|
||||
mediaFlags
|
||||
logger
|
||||
)
|
||||
|
||||
MediaPlayerData.clear()
|
||||
@@ -189,4 +174,18 @@ class MediaCarouselControllerTest : SysuiTestCase() {
|
||||
val size = MediaPlayerData.playerKeys().size
|
||||
assertTrue(MediaPlayerData.playerKeys().elementAt(size - 1).isSsMediaRec)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSwipeDismiss_logged() {
|
||||
mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
|
||||
|
||||
verify(logger).logSwipeDismiss()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSettingsButton_logged() {
|
||||
mediaCarouselController.settingsButton.callOnClick()
|
||||
|
||||
verify(logger).logCarouselSettings()
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import androidx.constraintlayout.widget.Barrier
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.logging.InstanceId
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastSender
|
||||
@@ -49,6 +50,7 @@ import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.plugins.FalsingManager
|
||||
import com.android.systemui.util.animation.TransitionLayout
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.KotlinArgumentCaptor
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -59,6 +61,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.any
|
||||
@@ -69,7 +72,6 @@ import org.mockito.Mockito.verify
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
|
||||
private const val KEY = "TEST_KEY"
|
||||
private const val APP = "APP"
|
||||
private const val BG_COLOR = Color.RED
|
||||
private const val PACKAGE = "PKG"
|
||||
private const val ARTIST = "ARTIST"
|
||||
@@ -78,7 +80,6 @@ private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val USER_ID = 0
|
||||
private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME"
|
||||
|
||||
@SmallTest
|
||||
@@ -137,6 +138,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME)
|
||||
private lateinit var mediaData: MediaData
|
||||
private val clock = FakeSystemClock()
|
||||
@Mock private lateinit var logger: MediaUiEventLogger
|
||||
@Mock private lateinit var instanceId: InstanceId
|
||||
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
|
||||
@@ -148,7 +151,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
player = MediaControlPanel(context, bgExecutor, activityStarter, broadcastSender,
|
||||
mediaViewController, seekBarViewModel, Lazy { mediaDataManager },
|
||||
mediaOutputDialogFactory, mediaCarouselController, falsingManager, clock)
|
||||
mediaOutputDialogFactory, mediaCarouselController, falsingManager, clock, logger)
|
||||
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
|
||||
|
||||
// Set up mock views for the players
|
||||
@@ -212,23 +215,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
}
|
||||
session.setActive(true)
|
||||
|
||||
mediaData = MediaData(
|
||||
userId = USER_ID,
|
||||
initialized = true,
|
||||
mediaData = MediaTestUtils.emptyMediaData.copy(
|
||||
backgroundColor = BG_COLOR,
|
||||
app = APP,
|
||||
appIcon = null,
|
||||
artist = ARTIST,
|
||||
song = TITLE,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = PACKAGE,
|
||||
token = session.sessionToken,
|
||||
clickIntent = null,
|
||||
device = device,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
instanceId = instanceId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -534,6 +528,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun longClick_gutsClosed() {
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(mediaData, KEY)
|
||||
whenever(mediaViewController.isGutsVisible).thenReturn(false)
|
||||
|
||||
val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
|
||||
@@ -541,6 +536,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
captor.value.onLongClick(viewHolder.player)
|
||||
verify(mediaViewController).openGuts()
|
||||
verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -568,8 +564,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun settingsButtonClick() {
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(mediaData, KEY)
|
||||
|
||||
settings.callOnClick()
|
||||
verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
|
||||
val captor = ArgumentCaptor.forClass(Intent::class.java)
|
||||
verify(activityStarter).startActivity(captor.capture(), eq(true))
|
||||
@@ -586,6 +584,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
assertThat(dismiss.isEnabled).isEqualTo(true)
|
||||
dismiss.callOnClick()
|
||||
verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
|
||||
}
|
||||
|
||||
@@ -614,4 +613,151 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
|
||||
verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionPlayPauseClick_isLogged() {
|
||||
val semanticActions = MediaButton(
|
||||
playOrPause = MediaAction(null, Runnable {}, "play", null)
|
||||
)
|
||||
val data = mediaData.copy(semanticActions = semanticActions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.actionPlayPause.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.actionPlayPause), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionPrevClick_isLogged() {
|
||||
val semanticActions = MediaButton(
|
||||
prevOrCustom = MediaAction(null, Runnable {}, "previous", null)
|
||||
)
|
||||
val data = mediaData.copy(semanticActions = semanticActions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.actionPrev.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.actionPrev), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionNextClick_isLogged() {
|
||||
val semanticActions = MediaButton(
|
||||
nextOrCustom = MediaAction(null, Runnable {}, "next", null)
|
||||
)
|
||||
val data = mediaData.copy(semanticActions = semanticActions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.actionNext.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.actionNext), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionCustom0Click_isLogged() {
|
||||
val semanticActions = MediaButton(
|
||||
custom0 = MediaAction(null, Runnable {}, "custom 0", null)
|
||||
)
|
||||
val data = mediaData.copy(semanticActions = semanticActions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.action0.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.action0), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionCustom1Click_isLogged() {
|
||||
val semanticActions = MediaButton(
|
||||
custom1 = MediaAction(null, Runnable {}, "custom 1", null)
|
||||
)
|
||||
val data = mediaData.copy(semanticActions = semanticActions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.action1.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionCustom2Click_isLogged() {
|
||||
val actions = listOf(
|
||||
MediaAction(null, Runnable {}, "action 0", null),
|
||||
MediaAction(null, Runnable {}, "action 1", null),
|
||||
MediaAction(null, Runnable {}, "action 2", null),
|
||||
MediaAction(null, Runnable {}, "action 3", null),
|
||||
MediaAction(null, Runnable {}, "action 4", null)
|
||||
)
|
||||
val data = mediaData.copy(actions = actions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.action2.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.action2), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionCustom3Click_isLogged() {
|
||||
val actions = listOf(
|
||||
MediaAction(null, Runnable {}, "action 0", null),
|
||||
MediaAction(null, Runnable {}, "action 1", null),
|
||||
MediaAction(null, Runnable {}, "action 2", null),
|
||||
MediaAction(null, Runnable {}, "action 3", null),
|
||||
MediaAction(null, Runnable {}, "action 4", null)
|
||||
)
|
||||
val data = mediaData.copy(actions = actions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.action1.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun actionCustom4Click_isLogged() {
|
||||
val actions = listOf(
|
||||
MediaAction(null, Runnable {}, "action 0", null),
|
||||
MediaAction(null, Runnable {}, "action 1", null),
|
||||
MediaAction(null, Runnable {}, "action 2", null),
|
||||
MediaAction(null, Runnable {}, "action 3", null),
|
||||
MediaAction(null, Runnable {}, "action 4", null)
|
||||
)
|
||||
val data = mediaData.copy(actions = actions)
|
||||
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(data, KEY)
|
||||
|
||||
viewHolder.action1.callOnClick()
|
||||
verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openOutputSwitcher_isLogged() {
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(mediaData, KEY)
|
||||
|
||||
seamless.callOnClick()
|
||||
|
||||
verify(logger).logOpenOutputSwitcher(anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun logSeek() {
|
||||
player.attachPlayer(viewHolder)
|
||||
player.bindPlayer(mediaData, KEY)
|
||||
|
||||
val callback: () -> Unit = {}
|
||||
val captor = KotlinArgumentCaptor(callback::class.java)
|
||||
verify(seekBarViewModel).logSeek = captor.capture()
|
||||
captor.value.invoke()
|
||||
|
||||
verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.testing.TestableLooper;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.internal.logging.InstanceId;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -77,7 +78,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
mMediaData = new MediaData(
|
||||
USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
|
||||
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L);
|
||||
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
|
||||
InstanceId.fakeInstanceId(-1), -1);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.app.smartspace.SmartspaceAction
|
||||
import android.graphics.Color
|
||||
import androidx.test.filters.SmallTest
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
@@ -25,6 +24,8 @@ import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.broadcast.BroadcastSender
|
||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
@@ -33,7 +34,6 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
@@ -45,17 +45,9 @@ private const val KEY = "TEST_KEY"
|
||||
private const val KEY_ALT = "TEST_KEY_2"
|
||||
private const val USER_MAIN = 0
|
||||
private const val USER_GUEST = 10
|
||||
private const val APP = "APP"
|
||||
private const val BG_COLOR = Color.RED
|
||||
private const val PACKAGE = "PKG"
|
||||
private const val ARTIST = "ARTIST"
|
||||
private const val TITLE = "TITLE"
|
||||
private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
private fun <T> any(): T = Mockito.any()
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
@@ -68,8 +60,6 @@ class MediaDataFilterTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var broadcastSender: BroadcastSender
|
||||
@Mock
|
||||
private lateinit var mediaResumeListener: MediaResumeListener
|
||||
@Mock
|
||||
private lateinit var mediaDataManager: MediaDataManager
|
||||
@Mock
|
||||
private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
|
||||
@@ -83,7 +73,6 @@ class MediaDataFilterTest : SysuiTestCase() {
|
||||
private lateinit var mediaDataFilter: MediaDataFilter
|
||||
private lateinit var dataMain: MediaData
|
||||
private lateinit var dataGuest: MediaData
|
||||
private val device = MediaDeviceData(true, null, DEVICE_NAME)
|
||||
private val clock = FakeSystemClock()
|
||||
|
||||
@Before
|
||||
@@ -99,23 +88,9 @@ class MediaDataFilterTest : SysuiTestCase() {
|
||||
setUser(USER_MAIN)
|
||||
|
||||
// Set up test media data
|
||||
dataMain = MediaData(
|
||||
dataMain = MediaTestUtils.emptyMediaData.copy(
|
||||
userId = USER_MAIN,
|
||||
initialized = true,
|
||||
backgroundColor = BG_COLOR,
|
||||
app = APP,
|
||||
appIcon = null,
|
||||
artist = ARTIST,
|
||||
song = TITLE,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = PACKAGE,
|
||||
token = null,
|
||||
clickIntent = null,
|
||||
device = device,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
packageName = PACKAGE)
|
||||
dataGuest = dataMain.copy(userId = USER_GUEST)
|
||||
|
||||
`when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import androidx.media.utils.MediaConstants
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.InstanceIdSequenceFake
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
@@ -38,10 +39,10 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
@@ -51,7 +52,8 @@ import org.mockito.Mockito.`when` as whenever
|
||||
private const val KEY = "KEY"
|
||||
private const val KEY_2 = "KEY_2"
|
||||
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
|
||||
private const val PACKAGE_NAME = "com.android.systemui"
|
||||
private const val PACKAGE_NAME = "com.example.app"
|
||||
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
|
||||
private const val APP_NAME = "SystemUI"
|
||||
private const val SESSION_ARTIST = "artist"
|
||||
private const val SESSION_TITLE = "title"
|
||||
@@ -92,6 +94,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var mediaRecommendationItem: SmartspaceAction
|
||||
@Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
|
||||
@Mock private lateinit var mediaFlags: MediaFlags
|
||||
@Mock private lateinit var logger: MediaUiEventLogger
|
||||
lateinit var mediaDataManager: MediaDataManager
|
||||
lateinit var mediaNotification: StatusBarNotification
|
||||
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
|
||||
@@ -99,6 +102,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var tunerService: TunerService
|
||||
@Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
|
||||
|
||||
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
|
||||
|
||||
private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
|
||||
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
|
||||
|
||||
@@ -128,7 +133,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
useQsMediaPlayer = true,
|
||||
systemClock = clock,
|
||||
tunerService = tunerService,
|
||||
mediaFlags = mediaFlags
|
||||
mediaFlags = mediaFlags,
|
||||
logger = logger
|
||||
)
|
||||
verify(tunerService).addTunable(capture(tunableCaptor),
|
||||
eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
|
||||
@@ -169,6 +175,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf(mediaRecommendationItem))
|
||||
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
|
||||
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
|
||||
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -181,15 +188,13 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun testSetTimedOut_active_deactivatesMedia() {
|
||||
val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
|
||||
appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
|
||||
clickIntent = null, device = null, active = true, resumeAction = null)
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
assertThat(data.active).isTrue()
|
||||
|
||||
mediaDataManager.setTimedOut(KEY, timedOut = true)
|
||||
assertThat(data.active).isFalse()
|
||||
verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -201,9 +206,15 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
}
|
||||
mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
|
||||
APP_NAME, pendingIntent, PACKAGE_NAME)
|
||||
|
||||
backgroundExecutor.runAllReady()
|
||||
foregroundExecutor.runAllReady()
|
||||
verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor),
|
||||
eq(true), eq(0))
|
||||
|
||||
mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
|
||||
verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME),
|
||||
eq(mediaDataCaptor.value.instanceId))
|
||||
|
||||
// THEN it is removed and listeners are informed
|
||||
foregroundExecutor.advanceClockToLast()
|
||||
@@ -219,10 +230,9 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun testOnMetaDataLoaded_callsListener() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject(), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME),
|
||||
eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_LOCAL))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -241,7 +251,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testOnNotificationAdded_isRcn_markedRemote() {
|
||||
val rcn = SbnBuilder().run {
|
||||
setPkg("com.android.systemui") // System package
|
||||
setPkg(SYSTEM_PACKAGE_NAME)
|
||||
modifyNotification(context).also {
|
||||
it.setSmallIcon(android.R.drawable.ic_media_pause)
|
||||
it.setStyle(MediaStyle().apply {
|
||||
@@ -259,25 +269,24 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
eq(0))
|
||||
assertThat(mediaDataCaptor.value!!.playbackLocation).isEqualTo(
|
||||
MediaData.PLAYBACK_CAST_REMOTE)
|
||||
verify(logger).logActiveMediaAdded(anyInt(), eq(SYSTEM_PACKAGE_NAME),
|
||||
eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_callsListener() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
mediaDataManager.onNotificationRemoved(KEY)
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_withResumption() {
|
||||
// GIVEN that the manager has a notification with a resume action
|
||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
assertThat(data.resumption).isFalse()
|
||||
mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
|
||||
@@ -289,6 +298,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
eq(0))
|
||||
assertThat(mediaDataCaptor.value.resumption).isTrue()
|
||||
assertThat(mediaDataCaptor.value.isPlaying).isFalse()
|
||||
verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -333,15 +343,13 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
||||
whenever(playbackInfo.playbackType).thenReturn(
|
||||
MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
val dataRemoteWithResume = data.copy(resumeAction = Runnable {},
|
||||
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
|
||||
verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME),
|
||||
eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL))
|
||||
|
||||
// WHEN the notification is removed
|
||||
mediaDataManager.onNotificationRemoved(KEY)
|
||||
@@ -373,12 +381,34 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
assertThat(data.actions).hasSize(1)
|
||||
assertThat(data.semanticActions!!.playOrPause).isNotNull()
|
||||
assertThat(data.lastActive).isAtLeast(currentTime)
|
||||
verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResumptionDisabled_dismissesResumeControls() {
|
||||
// WHEN there are resume controls and resumption is switched off
|
||||
val desc = MediaDescription.Builder().run {
|
||||
setTitle(SESSION_TITLE)
|
||||
build()
|
||||
}
|
||||
mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
|
||||
APP_NAME, pendingIntent, PACKAGE_NAME)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor),
|
||||
eq(true), eq(0))
|
||||
val data = mediaDataCaptor.value
|
||||
mediaDataManager.setMediaResumptionEnabled(false)
|
||||
|
||||
// THEN the resume controls are dismissed
|
||||
verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
|
||||
verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDismissMedia_listenerCalled() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
val removed = mediaDataManager.dismissMediaData(KEY, 0L)
|
||||
assertThat(removed).isTrue()
|
||||
|
||||
@@ -386,6 +416,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
foregroundExecutor.runAllReady()
|
||||
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -513,11 +544,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testOnMediaDataChanged_updatesLastActiveTime() {
|
||||
val currentTime = clock.elapsedRealtime()
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime)
|
||||
}
|
||||
|
||||
@@ -543,12 +570,9 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() {
|
||||
// GIVEN that the manager has a notification with a resume action
|
||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
val instanceId = data.instanceId
|
||||
assertThat(data.resumption).isFalse()
|
||||
mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
|
||||
|
||||
@@ -563,6 +587,10 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
eq(0))
|
||||
assertThat(mediaDataCaptor.value.resumption).isTrue()
|
||||
assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
|
||||
|
||||
// Log as a conversion event, not as a new resume control
|
||||
verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
|
||||
verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -633,11 +661,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
}
|
||||
whenever(controller.playbackState).thenReturn(stateBuilder.build())
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
|
||||
assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
|
||||
val actions = mediaDataCaptor.value!!.semanticActions!!
|
||||
@@ -679,11 +703,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
}
|
||||
whenever(controller.playbackState).thenReturn(stateBuilder.build())
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
|
||||
assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
|
||||
val actions = mediaDataCaptor.value!!.semanticActions!!
|
||||
@@ -722,11 +742,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
whenever(controller.playbackState).thenReturn(stateBuilder.build())
|
||||
whenever(controller.extras).thenReturn(extras)
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
addNotificationAndLoad()
|
||||
|
||||
assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
|
||||
val actions = mediaDataCaptor.value!!.semanticActions!!
|
||||
@@ -744,4 +760,49 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
assertThat(actions.custom1).isNotNull()
|
||||
assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlaybackLocationChange_isLogged() {
|
||||
// Media control added for local playback
|
||||
addNotificationAndLoad()
|
||||
val instanceId = mediaDataCaptor.value.instanceId
|
||||
|
||||
// Location is updated to local cast
|
||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
||||
whenever(playbackInfo.playbackType).thenReturn(
|
||||
MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
|
||||
addNotificationAndLoad()
|
||||
verify(logger).logPlaybackLocationChange(anyInt(), eq(PACKAGE_NAME),
|
||||
eq(instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL))
|
||||
|
||||
// update to remote cast
|
||||
val rcn = SbnBuilder().run {
|
||||
setPkg(SYSTEM_PACKAGE_NAME) // System package
|
||||
modifyNotification(context).also {
|
||||
it.setSmallIcon(android.R.drawable.ic_media_pause)
|
||||
it.setStyle(MediaStyle().apply {
|
||||
setMediaSession(session.sessionToken)
|
||||
setRemotePlaybackInfo("Remote device", 0, null)
|
||||
})
|
||||
}
|
||||
build()
|
||||
}
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, rcn)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(logger).logPlaybackLocationChange(anyInt(), eq(SYSTEM_PACKAGE_NAME),
|
||||
eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to add a media notification and capture the resulting MediaData
|
||||
*/
|
||||
private fun addNotificationAndLoad() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
|
||||
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -44,7 +45,6 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
@@ -57,12 +57,8 @@ private const val KEY = "TEST_KEY"
|
||||
private const val KEY_OLD = "TEST_KEY_OLD"
|
||||
private const val PACKAGE = "PKG"
|
||||
private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
private const val REMOTE_DEVICE_NAME = "REMOTE_DEVICE_NAME"
|
||||
private const val USER_ID = 0
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@@ -115,24 +111,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
// Create a media sesssion and notification for testing.
|
||||
session = MediaSession(context, SESSION_KEY)
|
||||
|
||||
mediaData = MediaData(
|
||||
userId = USER_ID,
|
||||
initialized = true,
|
||||
backgroundColor = 0,
|
||||
app = PACKAGE,
|
||||
appIcon = null,
|
||||
artist = null,
|
||||
song = SESSION_TITLE,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
mediaData = MediaTestUtils.emptyMediaData.copy(
|
||||
packageName = PACKAGE,
|
||||
token = session.sessionToken,
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
|
||||
token = session.sessionToken)
|
||||
whenever(controllerFactory.create(session.sessionToken))
|
||||
.thenReturn(controller)
|
||||
}
|
||||
|
||||
@@ -163,27 +163,12 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
isPlaying: Boolean?,
|
||||
location: Int,
|
||||
resumption: Boolean
|
||||
) = MediaData(
|
||||
userId = 0,
|
||||
initialized = false,
|
||||
backgroundColor = 0,
|
||||
) = MediaTestUtils.emptyMediaData.copy(
|
||||
app = app,
|
||||
appIcon = null,
|
||||
artist = null,
|
||||
song = null,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = "package: $app",
|
||||
token = null,
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null,
|
||||
playbackLocation = location,
|
||||
resumption = resumption,
|
||||
notificationKey = "key: $app",
|
||||
hasCheckedForResume = false,
|
||||
isPlaying = isPlaying
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Color
|
||||
import android.media.MediaDescription
|
||||
import android.media.session.MediaSession
|
||||
import android.provider.Settings
|
||||
@@ -57,13 +56,9 @@ import org.mockito.MockitoAnnotations
|
||||
|
||||
private const val KEY = "TEST_KEY"
|
||||
private const val OLD_KEY = "RESUME_KEY"
|
||||
private const val APP = "APP"
|
||||
private const val BG_COLOR = Color.RED
|
||||
private const val PACKAGE_NAME = "PKG"
|
||||
private const val CLASS_NAME = "CLASS"
|
||||
private const val ARTIST = "ARTIST"
|
||||
private const val TITLE = "TITLE"
|
||||
private const val USER_ID = 0
|
||||
private const val MEDIA_PREFERENCES = "media_control_prefs"
|
||||
private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
|
||||
|
||||
@@ -130,24 +125,10 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
resumeListener.setManager(mediaDataManager)
|
||||
mediaDataManager.addListener(resumeListener)
|
||||
|
||||
data = MediaData(
|
||||
userId = USER_ID,
|
||||
initialized = true,
|
||||
backgroundColor = BG_COLOR,
|
||||
app = APP,
|
||||
appIcon = null,
|
||||
artist = ARTIST,
|
||||
data = MediaTestUtils.emptyMediaData.copy(
|
||||
song = TITLE,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = PACKAGE_NAME,
|
||||
token = token,
|
||||
clickIntent = null,
|
||||
device = device,
|
||||
active = true,
|
||||
notificationKey = KEY,
|
||||
resumeAction = null)
|
||||
token = token)
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.graphics.Color
|
||||
import android.media.session.MediaController
|
||||
import android.media.session.MediaController.PlaybackInfo
|
||||
import android.media.session.MediaSession
|
||||
@@ -39,7 +38,6 @@ import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
@@ -50,35 +48,12 @@ import org.mockito.Mockito.`when` as whenever
|
||||
private const val PACKAGE = "PKG"
|
||||
private const val KEY = "TEST_KEY"
|
||||
private const val NOTIF_KEY = "TEST_KEY"
|
||||
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val APP_NAME = "APP_NAME"
|
||||
private const val USER_ID = 0
|
||||
|
||||
private val info = MediaData(
|
||||
userId = USER_ID,
|
||||
initialized = true,
|
||||
backgroundColor = Color.DKGRAY,
|
||||
app = APP_NAME,
|
||||
appIcon = null,
|
||||
artist = SESSION_ARTIST,
|
||||
song = SESSION_TITLE,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
private val info = MediaTestUtils.emptyMediaData.copy(
|
||||
packageName = PACKAGE,
|
||||
token = null,
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null,
|
||||
resumption = false,
|
||||
notificationKey = NOTIF_KEY,
|
||||
hasCheckedForResume = false
|
||||
notificationKey = NOTIF_KEY
|
||||
)
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import com.android.internal.logging.InstanceId
|
||||
|
||||
class MediaTestUtils {
|
||||
companion object {
|
||||
val emptyMediaData = MediaData(
|
||||
userId = 0,
|
||||
initialized = true,
|
||||
backgroundColor = 0,
|
||||
app = null,
|
||||
appIcon = null,
|
||||
artist = null,
|
||||
song = null,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = "",
|
||||
token = null,
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null,
|
||||
instanceId = InstanceId.fakeInstanceId(-1),
|
||||
appUid = -1)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
@@ -50,9 +51,7 @@ private const val PACKAGE = "PKG"
|
||||
private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val USER_ID = 0
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
private fun <T> anyObject(): T {
|
||||
return Mockito.anyObject<T>()
|
||||
}
|
||||
@@ -96,23 +95,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
}
|
||||
session.setActive(true)
|
||||
|
||||
mediaData = MediaData(
|
||||
userId = USER_ID,
|
||||
initialized = true,
|
||||
backgroundColor = 0,
|
||||
app = PACKAGE,
|
||||
appIcon = null,
|
||||
artist = null,
|
||||
song = SESSION_TITLE,
|
||||
artwork = null,
|
||||
actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(),
|
||||
packageName = PACKAGE,
|
||||
token = session.sessionToken,
|
||||
clickIntent = null,
|
||||
device = null,
|
||||
active = true,
|
||||
resumeAction = null)
|
||||
mediaData = MediaTestUtils.emptyMediaData.copy(
|
||||
app = PACKAGE,
|
||||
packageName = PACKAGE,
|
||||
token = session.sessionToken
|
||||
)
|
||||
|
||||
resumeData = mediaData.copy(token = null, active = false, resumption = true)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
@@ -46,6 +47,7 @@ import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@SmallTest
|
||||
@@ -71,14 +73,14 @@ public class SeekBarViewModelTest : SysuiTestCase() {
|
||||
private val token1 = MediaSession.Token(1, null)
|
||||
private val token2 = MediaSession.Token(2, null)
|
||||
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fakeExecutor = FakeExecutor(FakeSystemClock())
|
||||
viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor))
|
||||
viewModel.logSmartspaceClick = { }
|
||||
mockController = mock(MediaController::class.java)
|
||||
viewModel.logSeek = { }
|
||||
whenever(mockController.sessionToken).thenReturn(token1)
|
||||
mockTransport = mock(MediaController.TransportControls::class.java)
|
||||
|
||||
// LiveData to run synchronously
|
||||
ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
|
||||
|
||||
Reference in New Issue
Block a user