Loading packages/SystemUI/src/com/android/systemui/flags/Flags.kt +3 −0 Original line number Diff line number Diff line Loading @@ -364,6 +364,9 @@ object Flags { // TODO(b/267007629): Tracking Bug val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress") // TODO(b/267166152) : Tracking Bug val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations") // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") Loading packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt +11 −2 Original line number Diff line number Diff line Loading @@ -41,10 +41,12 @@ data class SmartspaceMediaData( val recommendations: List<SmartspaceAction>, /** Intent for the user's initiated dismissal. */ val dismissIntent: Intent?, /** The timestamp in milliseconds that headphone is connected. */ /** The timestamp in milliseconds that the card was generated */ val headphoneConnectionTimeMillis: Long, /** Instance ID for [MediaUiEventLogger] */ val instanceId: InstanceId val instanceId: InstanceId, /** The timestamp in milliseconds indicating when the card should be removed */ val expiryTimeMs: Long, ) { /** * Indicates if all the data is valid. Loading Loading @@ -86,5 +88,12 @@ data class SmartspaceMediaData( } } /** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */ const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE" /** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */ const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION" /** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */ const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER" const val NUM_REQUIRED_RECOMMENDATIONS = 3 private val TAG = SmartspaceMediaData::class.simpleName!! packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt +32 −15 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager Loading Loading @@ -66,7 +67,8 @@ constructor( private val lockscreenUserManager: NotificationLockscreenUserManager, @Main private val executor: Executor, private val systemClock: SystemClock, private val logger: MediaUiEventLogger private val logger: MediaUiEventLogger, private val mediaFlags: MediaFlags, ) : MediaDataManager.Listener { private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() internal val listeners: Set<MediaDataManager.Listener> Loading Loading @@ -121,7 +123,9 @@ constructor( data: SmartspaceMediaData, shouldPrioritize: Boolean ) { if (!data.isActive) { // With persistent recommendation card, we could get a background update while inactive // Otherwise, consider it an invalid update if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) { Log.d(TAG, "Inactive recommendation data. Skip triggering.") return } Loading @@ -141,7 +145,7 @@ constructor( } } val shouldReactivate = !hasActiveMedia() && hasAnyMedia() val shouldReactivate = !hasActiveMedia() && hasAnyMedia() && data.isActive if (timeSinceActive < smartspaceMaxAgeMillis) { // It could happen there are existing active media resume cards, then we don't need to Loading Loading @@ -169,7 +173,7 @@ constructor( ) } } } else { } else if (data.isActive) { // Mark to prioritize Smartspace card if no recent media. shouldPrioritizeMutable = true } Loading Loading @@ -252,7 +256,7 @@ constructor( if (dismissIntent == null) { Log.w( TAG, "Cannot create dismiss action click action: " + "extras missing dismiss_intent." "Cannot create dismiss action click action: extras missing dismiss_intent." ) } else if ( dismissIntent.getComponent() != null && Loading @@ -264,17 +268,23 @@ constructor( } else { broadcastSender.sendBroadcast(dismissIntent) } if (mediaFlags.isPersistentSsCardEnabled()) { smartspaceMediaData = smartspaceMediaData.copy(isActive = false) mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId) } else { smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( targetId = smartspaceMediaData.targetId, instanceId = smartspaceMediaData.instanceId instanceId = smartspaceMediaData.instanceId, ) mediaDataManager.dismissSmartspaceRecommendation( smartspaceMediaData.targetId, delay = 0L delay = 0L, ) } } } /** Are there any active media entries, including the recommendation? */ fun hasActiveMediaOrRecommendation() = Loading @@ -283,8 +293,15 @@ constructor( (smartspaceMediaData.isValid() || reactivatedKey != null)) /** Are there any media entries we should display? */ fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) fun hasAnyMediaOrRecommendation(): Boolean { val hasSmartspace = if (mediaFlags.isPersistentSsCardEnabled()) { smartspaceMediaData.isValid() } else { smartspaceMediaData.isActive && smartspaceMediaData.isValid() } return userEntries.isNotEmpty() || hasSmartspace } /** Are there any media notifications active (excluding the recommendation)? */ fun hasActiveMedia() = userEntries.any { it.value.active } Loading packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +64 −19 Original line number Diff line number Diff line Loading @@ -49,7 +49,6 @@ import android.support.v4.media.MediaMetadataCompat 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 Loading @@ -63,6 +62,8 @@ import com.android.systemui.media.controls.models.player.MediaButton import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider import com.android.systemui.media.controls.resume.MediaResumeListener Loading Loading @@ -119,7 +120,6 @@ private val LOADING = appUid = Process.INVALID_UID ) @VisibleForTesting internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData( targetId = "INVALID", Loading @@ -129,7 +129,8 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA = recommendations = emptyList(), dismissIntent = null, headphoneConnectionTimeMillis = 0, instanceId = InstanceId.fakeInstanceId(-1) instanceId = InstanceId.fakeInstanceId(-1), expiryTimeMs = 0, ) fun isMediaNotification(sbn: StatusBarNotification): Boolean { Loading Loading @@ -548,6 +549,11 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") onMediaDataLoaded(key, key, it) } if (key == smartspaceMediaData.targetId) { if (DEBUG) Log.d(TAG, "smartspace card expired") dismissSmartspaceRecommendation(key, delay = 0L) } } /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ Loading Loading @@ -605,8 +611,8 @@ class MediaDataManager( } /** * Called whenever the recommendation has been expired, or swiped from QQS. This will make the * recommendation view to not be shown anymore during this headphone connection session. * Called whenever the recommendation has been expired or removed by the user. This will remove * the recommendation card entirely from the carousel. */ fun dismissSmartspaceRecommendation(key: String, delay: Long) { if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { Loading @@ -628,6 +634,23 @@ class MediaDataManager( ) } /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ fun setRecommendationInactive(key: String) { if (!mediaFlags.isPersistentSsCardEnabled()) { Log.e(TAG, "Only persistent recommendation can be inactive!") return } if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { // If this doesn't match, or we've already invalidated the data, no action needed return } smartspaceMediaData = smartspaceMediaData.copy(isActive = false) notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } private fun loadMediaDataInBgForResumption( userId: Int, desc: MediaDescription, Loading Loading @@ -1265,12 +1288,25 @@ class MediaDataManager( if (DEBUG) { Log.d(TAG, "Set Smartspace media to be inactive for the data update") } if (mediaFlags.isPersistentSsCardEnabled()) { // Smartspace uses this signal to hide the card (e.g. when it expires or user // disconnects headphones), so treat as setting inactive when flag is on smartspaceMediaData = smartspaceMediaData.copy(isActive = false) notifySmartspaceMediaDataLoaded( smartspaceMediaData.targetId, smartspaceMediaData, ) } else { smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( targetId = smartspaceMediaData.targetId, instanceId = smartspaceMediaData.instanceId instanceId = smartspaceMediaData.instanceId, ) notifySmartspaceMediaDataRemoved( smartspaceMediaData.targetId, immediately = false, ) notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) } } 1 -> { val newMediaTarget = mediaTargets.get(0) Loading @@ -1279,7 +1315,7 @@ class MediaDataManager( return } if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true) smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } else -> { Loading @@ -1288,7 +1324,7 @@ class MediaDataManager( Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") notifySmartspaceMediaDataRemoved( smartspaceMediaData.targetId, false /* immediately */ immediately = false, ) smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA } Loading Loading @@ -1494,21 +1530,28 @@ class MediaDataManager( } /** * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status. * Converts the pass-in SmartspaceTarget to SmartspaceMediaData * * @return An empty SmartspaceMediaData with the valid target Id is returned if the * SmartspaceTarget's data is invalid. */ private fun toSmartspaceMediaData( target: SmartspaceTarget, isActive: Boolean ): SmartspaceMediaData { private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { var dismissIntent: Intent? = null if (target.baseAction != null && target.baseAction.extras != null) { dismissIntent = target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? } val isActive = when { !mediaFlags.isPersistentSsCardEnabled() -> true target.baseAction == null -> true else -> target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) != EXTRA_VALUE_TRIGGER_PERIODIC } packageName(target)?.let { return SmartspaceMediaData( targetId = target.smartspaceTargetId, Loading @@ -1518,7 +1561,8 @@ class MediaDataManager( recommendations = target.iconGrid, dismissIntent = dismissIntent, headphoneConnectionTimeMillis = target.creationTimeMillis, instanceId = logger.getNewInstanceId() instanceId = logger.getNewInstanceId(), expiryTimeMs = target.expiryTimeMillis, ) } return EMPTY_SMARTSPACE_MEDIA_DATA.copy( Loading @@ -1526,7 +1570,8 @@ class MediaDataManager( isActive = isActive, dismissIntent = dismissIntent, headphoneConnectionTimeMillis = target.creationTimeMillis, instanceId = logger.getNewInstanceId() instanceId = logger.getNewInstanceId(), expiryTimeMs = target.expiryTimeMillis, ) } Loading packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt +88 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,9 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.SysuiStatusBarStateController Loading @@ -49,10 +51,12 @@ constructor( @Main private val mainExecutor: DelayableExecutor, private val logger: MediaTimeoutLogger, statusBarStateController: SysuiStatusBarStateController, private val systemClock: SystemClock private val systemClock: SystemClock, private val mediaFlags: MediaFlags, ) : MediaDataManager.Listener { private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf() private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf() /** * Callback representing that a media object is now expired: Loading Loading @@ -93,6 +97,16 @@ constructor( listener.doTimeout() } } recommendationListeners.forEach { (key, listener) -> if ( listener.cancellation != null && listener.expiration <= systemClock.currentTimeMillis() ) { logger.logTimeoutCancelled(key, "Timed out while dozing") listener.doTimeout() } } } } } Loading Loading @@ -155,6 +169,30 @@ constructor( mediaListeners.remove(key)?.destroy() } override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, shouldPrioritize: Boolean ) { if (!mediaFlags.isPersistentSsCardEnabled()) return // First check if we already have a listener recommendationListeners.get(key)?.let { if (!it.destroyed) { it.recommendationData = data return } } // Otherwise, create a new one recommendationListeners[key] = RecommendationListener(key, data) } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { if (!mediaFlags.isPersistentSsCardEnabled()) return recommendationListeners.remove(key)?.destroy() } fun isTimedOut(key: String): Boolean { return mediaListeners[key]?.timedOut ?: false } Loading Loading @@ -335,4 +373,53 @@ constructor( } return true } /** Listens to changes in recommendation card data and schedules a timeout for its expiration */ private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) { private var timedOut = false var destroyed = false var expiration = Long.MAX_VALUE private set var cancellation: Runnable? = null private set var recommendationData: SmartspaceMediaData = data set(value) { destroyed = false field = value processUpdate() } init { recommendationData = data } fun destroy() { cancellation?.run() cancellation = null destroyed = true } private fun processUpdate() { if (recommendationData.expiryTimeMs != expiration) { // The expiry time changed - cancel and reschedule val timeout = recommendationData.expiryTimeMs - recommendationData.headphoneConnectionTimeMillis logger.logRecommendationTimeoutScheduled(key, timeout) cancellation?.run() cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout) expiration = recommendationData.expiryTimeMs } } fun doTimeout() { cancellation?.run() cancellation = null logger.logTimeout(key) timedOut = true expiration = Long.MAX_VALUE timeoutCallback(key, timedOut) } } } Loading
packages/SystemUI/src/com/android/systemui/flags/Flags.kt +3 −0 Original line number Diff line number Diff line Loading @@ -364,6 +364,9 @@ object Flags { // TODO(b/267007629): Tracking Bug val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress") // TODO(b/267166152) : Tracking Bug val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations") // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") Loading
packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt +11 −2 Original line number Diff line number Diff line Loading @@ -41,10 +41,12 @@ data class SmartspaceMediaData( val recommendations: List<SmartspaceAction>, /** Intent for the user's initiated dismissal. */ val dismissIntent: Intent?, /** The timestamp in milliseconds that headphone is connected. */ /** The timestamp in milliseconds that the card was generated */ val headphoneConnectionTimeMillis: Long, /** Instance ID for [MediaUiEventLogger] */ val instanceId: InstanceId val instanceId: InstanceId, /** The timestamp in milliseconds indicating when the card should be removed */ val expiryTimeMs: Long, ) { /** * Indicates if all the data is valid. Loading Loading @@ -86,5 +88,12 @@ data class SmartspaceMediaData( } } /** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */ const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE" /** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */ const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION" /** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */ const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER" const val NUM_REQUIRED_RECOMMENDATIONS = 3 private val TAG = SmartspaceMediaData::class.simpleName!!
packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt +32 −15 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager Loading Loading @@ -66,7 +67,8 @@ constructor( private val lockscreenUserManager: NotificationLockscreenUserManager, @Main private val executor: Executor, private val systemClock: SystemClock, private val logger: MediaUiEventLogger private val logger: MediaUiEventLogger, private val mediaFlags: MediaFlags, ) : MediaDataManager.Listener { private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() internal val listeners: Set<MediaDataManager.Listener> Loading Loading @@ -121,7 +123,9 @@ constructor( data: SmartspaceMediaData, shouldPrioritize: Boolean ) { if (!data.isActive) { // With persistent recommendation card, we could get a background update while inactive // Otherwise, consider it an invalid update if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) { Log.d(TAG, "Inactive recommendation data. Skip triggering.") return } Loading @@ -141,7 +145,7 @@ constructor( } } val shouldReactivate = !hasActiveMedia() && hasAnyMedia() val shouldReactivate = !hasActiveMedia() && hasAnyMedia() && data.isActive if (timeSinceActive < smartspaceMaxAgeMillis) { // It could happen there are existing active media resume cards, then we don't need to Loading Loading @@ -169,7 +173,7 @@ constructor( ) } } } else { } else if (data.isActive) { // Mark to prioritize Smartspace card if no recent media. shouldPrioritizeMutable = true } Loading Loading @@ -252,7 +256,7 @@ constructor( if (dismissIntent == null) { Log.w( TAG, "Cannot create dismiss action click action: " + "extras missing dismiss_intent." "Cannot create dismiss action click action: extras missing dismiss_intent." ) } else if ( dismissIntent.getComponent() != null && Loading @@ -264,17 +268,23 @@ constructor( } else { broadcastSender.sendBroadcast(dismissIntent) } if (mediaFlags.isPersistentSsCardEnabled()) { smartspaceMediaData = smartspaceMediaData.copy(isActive = false) mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId) } else { smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( targetId = smartspaceMediaData.targetId, instanceId = smartspaceMediaData.instanceId instanceId = smartspaceMediaData.instanceId, ) mediaDataManager.dismissSmartspaceRecommendation( smartspaceMediaData.targetId, delay = 0L delay = 0L, ) } } } /** Are there any active media entries, including the recommendation? */ fun hasActiveMediaOrRecommendation() = Loading @@ -283,8 +293,15 @@ constructor( (smartspaceMediaData.isValid() || reactivatedKey != null)) /** Are there any media entries we should display? */ fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) fun hasAnyMediaOrRecommendation(): Boolean { val hasSmartspace = if (mediaFlags.isPersistentSsCardEnabled()) { smartspaceMediaData.isValid() } else { smartspaceMediaData.isActive && smartspaceMediaData.isValid() } return userEntries.isNotEmpty() || hasSmartspace } /** Are there any media notifications active (excluding the recommendation)? */ fun hasActiveMedia() = userEntries.any { it.value.active } Loading
packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +64 −19 Original line number Diff line number Diff line Loading @@ -49,7 +49,6 @@ import android.support.v4.media.MediaMetadataCompat 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 Loading @@ -63,6 +62,8 @@ import com.android.systemui.media.controls.models.player.MediaButton import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider import com.android.systemui.media.controls.resume.MediaResumeListener Loading Loading @@ -119,7 +120,6 @@ private val LOADING = appUid = Process.INVALID_UID ) @VisibleForTesting internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData( targetId = "INVALID", Loading @@ -129,7 +129,8 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA = recommendations = emptyList(), dismissIntent = null, headphoneConnectionTimeMillis = 0, instanceId = InstanceId.fakeInstanceId(-1) instanceId = InstanceId.fakeInstanceId(-1), expiryTimeMs = 0, ) fun isMediaNotification(sbn: StatusBarNotification): Boolean { Loading Loading @@ -548,6 +549,11 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") onMediaDataLoaded(key, key, it) } if (key == smartspaceMediaData.targetId) { if (DEBUG) Log.d(TAG, "smartspace card expired") dismissSmartspaceRecommendation(key, delay = 0L) } } /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ Loading Loading @@ -605,8 +611,8 @@ class MediaDataManager( } /** * Called whenever the recommendation has been expired, or swiped from QQS. This will make the * recommendation view to not be shown anymore during this headphone connection session. * Called whenever the recommendation has been expired or removed by the user. This will remove * the recommendation card entirely from the carousel. */ fun dismissSmartspaceRecommendation(key: String, delay: Long) { if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { Loading @@ -628,6 +634,23 @@ class MediaDataManager( ) } /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ fun setRecommendationInactive(key: String) { if (!mediaFlags.isPersistentSsCardEnabled()) { Log.e(TAG, "Only persistent recommendation can be inactive!") return } if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { // If this doesn't match, or we've already invalidated the data, no action needed return } smartspaceMediaData = smartspaceMediaData.copy(isActive = false) notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } private fun loadMediaDataInBgForResumption( userId: Int, desc: MediaDescription, Loading Loading @@ -1265,12 +1288,25 @@ class MediaDataManager( if (DEBUG) { Log.d(TAG, "Set Smartspace media to be inactive for the data update") } if (mediaFlags.isPersistentSsCardEnabled()) { // Smartspace uses this signal to hide the card (e.g. when it expires or user // disconnects headphones), so treat as setting inactive when flag is on smartspaceMediaData = smartspaceMediaData.copy(isActive = false) notifySmartspaceMediaDataLoaded( smartspaceMediaData.targetId, smartspaceMediaData, ) } else { smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( targetId = smartspaceMediaData.targetId, instanceId = smartspaceMediaData.instanceId instanceId = smartspaceMediaData.instanceId, ) notifySmartspaceMediaDataRemoved( smartspaceMediaData.targetId, immediately = false, ) notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) } } 1 -> { val newMediaTarget = mediaTargets.get(0) Loading @@ -1279,7 +1315,7 @@ class MediaDataManager( return } if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true) smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } else -> { Loading @@ -1288,7 +1324,7 @@ class MediaDataManager( Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") notifySmartspaceMediaDataRemoved( smartspaceMediaData.targetId, false /* immediately */ immediately = false, ) smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA } Loading Loading @@ -1494,21 +1530,28 @@ class MediaDataManager( } /** * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status. * Converts the pass-in SmartspaceTarget to SmartspaceMediaData * * @return An empty SmartspaceMediaData with the valid target Id is returned if the * SmartspaceTarget's data is invalid. */ private fun toSmartspaceMediaData( target: SmartspaceTarget, isActive: Boolean ): SmartspaceMediaData { private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { var dismissIntent: Intent? = null if (target.baseAction != null && target.baseAction.extras != null) { dismissIntent = target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? } val isActive = when { !mediaFlags.isPersistentSsCardEnabled() -> true target.baseAction == null -> true else -> target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) != EXTRA_VALUE_TRIGGER_PERIODIC } packageName(target)?.let { return SmartspaceMediaData( targetId = target.smartspaceTargetId, Loading @@ -1518,7 +1561,8 @@ class MediaDataManager( recommendations = target.iconGrid, dismissIntent = dismissIntent, headphoneConnectionTimeMillis = target.creationTimeMillis, instanceId = logger.getNewInstanceId() instanceId = logger.getNewInstanceId(), expiryTimeMs = target.expiryTimeMillis, ) } return EMPTY_SMARTSPACE_MEDIA_DATA.copy( Loading @@ -1526,7 +1570,8 @@ class MediaDataManager( isActive = isActive, dismissIntent = dismissIntent, headphoneConnectionTimeMillis = target.creationTimeMillis, instanceId = logger.getNewInstanceId() instanceId = logger.getNewInstanceId(), expiryTimeMs = target.expiryTimeMillis, ) } Loading
packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt +88 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,9 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.SysuiStatusBarStateController Loading @@ -49,10 +51,12 @@ constructor( @Main private val mainExecutor: DelayableExecutor, private val logger: MediaTimeoutLogger, statusBarStateController: SysuiStatusBarStateController, private val systemClock: SystemClock private val systemClock: SystemClock, private val mediaFlags: MediaFlags, ) : MediaDataManager.Listener { private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf() private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf() /** * Callback representing that a media object is now expired: Loading Loading @@ -93,6 +97,16 @@ constructor( listener.doTimeout() } } recommendationListeners.forEach { (key, listener) -> if ( listener.cancellation != null && listener.expiration <= systemClock.currentTimeMillis() ) { logger.logTimeoutCancelled(key, "Timed out while dozing") listener.doTimeout() } } } } } Loading Loading @@ -155,6 +169,30 @@ constructor( mediaListeners.remove(key)?.destroy() } override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, shouldPrioritize: Boolean ) { if (!mediaFlags.isPersistentSsCardEnabled()) return // First check if we already have a listener recommendationListeners.get(key)?.let { if (!it.destroyed) { it.recommendationData = data return } } // Otherwise, create a new one recommendationListeners[key] = RecommendationListener(key, data) } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { if (!mediaFlags.isPersistentSsCardEnabled()) return recommendationListeners.remove(key)?.destroy() } fun isTimedOut(key: String): Boolean { return mediaListeners[key]?.timedOut ?: false } Loading Loading @@ -335,4 +373,53 @@ constructor( } return true } /** Listens to changes in recommendation card data and schedules a timeout for its expiration */ private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) { private var timedOut = false var destroyed = false var expiration = Long.MAX_VALUE private set var cancellation: Runnable? = null private set var recommendationData: SmartspaceMediaData = data set(value) { destroyed = false field = value processUpdate() } init { recommendationData = data } fun destroy() { cancellation?.run() cancellation = null destroyed = true } private fun processUpdate() { if (recommendationData.expiryTimeMs != expiration) { // The expiry time changed - cancel and reschedule val timeout = recommendationData.expiryTimeMs - recommendationData.headphoneConnectionTimeMillis logger.logRecommendationTimeoutScheduled(key, timeout) cancellation?.run() cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout) expiration = recommendationData.expiryTimeMs } } fun doTimeout() { cancellation?.run() cancellation = null logger.logTimeout(key) timedOut = true expiration = Long.MAX_VALUE timeoutCallback(key, timedOut) } } }