1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.media.controls.domain.pipeline
18 
19 import android.annotation.SuppressLint
20 import android.app.ActivityOptions
21 import android.app.BroadcastOptions
22 import android.app.Notification
23 import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
24 import android.app.PendingIntent
25 import android.app.StatusBarManager
26 import android.app.UriGrantsManager
27 import android.app.smartspace.SmartspaceAction
28 import android.app.smartspace.SmartspaceConfig
29 import android.app.smartspace.SmartspaceManager
30 import android.app.smartspace.SmartspaceSession
31 import android.app.smartspace.SmartspaceTarget
32 import android.content.BroadcastReceiver
33 import android.content.ContentProvider
34 import android.content.ContentResolver
35 import android.content.Context
36 import android.content.Intent
37 import android.content.IntentFilter
38 import android.content.pm.ApplicationInfo
39 import android.content.pm.PackageManager
40 import android.graphics.Bitmap
41 import android.graphics.ImageDecoder
42 import android.graphics.drawable.Animatable
43 import android.graphics.drawable.Icon
44 import android.media.MediaDescription
45 import android.media.MediaMetadata
46 import android.media.session.MediaController
47 import android.media.session.MediaSession
48 import android.media.session.PlaybackState
49 import android.net.Uri
50 import android.os.Parcelable
51 import android.os.Process
52 import android.os.UserHandle
53 import android.provider.Settings
54 import android.service.notification.StatusBarNotification
55 import android.support.v4.media.MediaMetadataCompat
56 import android.text.TextUtils
57 import android.util.Log
58 import android.util.Pair as APair
59 import androidx.media.utils.MediaConstants
60 import com.android.app.tracing.traceSection
61 import com.android.internal.annotations.Keep
62 import com.android.internal.logging.InstanceId
63 import com.android.keyguard.KeyguardUpdateMonitor
64 import com.android.systemui.Dumpable
65 import com.android.systemui.broadcast.BroadcastDispatcher
66 import com.android.systemui.dagger.SysUISingleton
67 import com.android.systemui.dagger.qualifiers.Background
68 import com.android.systemui.dagger.qualifiers.Main
69 import com.android.systemui.dump.DumpManager
70 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
71 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
72 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
73 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
74 import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
75 import com.android.systemui.media.controls.shared.model.MediaAction
76 import com.android.systemui.media.controls.shared.model.MediaButton
77 import com.android.systemui.media.controls.shared.model.MediaData
78 import com.android.systemui.media.controls.shared.model.MediaDeviceData
79 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
80 import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
81 import com.android.systemui.media.controls.ui.view.MediaViewHolder
82 import com.android.systemui.media.controls.util.MediaControllerFactory
83 import com.android.systemui.media.controls.util.MediaDataUtils
84 import com.android.systemui.media.controls.util.MediaFlags
85 import com.android.systemui.media.controls.util.MediaUiEventLogger
86 import com.android.systemui.plugins.ActivityStarter
87 import com.android.systemui.plugins.BcSmartspaceDataPlugin
88 import com.android.systemui.res.R
89 import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
90 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
91 import com.android.systemui.statusbar.notification.row.HybridGroupManager
92 import com.android.systemui.tuner.TunerService
93 import com.android.systemui.util.Assert
94 import com.android.systemui.util.Utils
95 import com.android.systemui.util.concurrency.DelayableExecutor
96 import com.android.systemui.util.concurrency.ThreadFactory
97 import com.android.systemui.util.time.SystemClock
98 import java.io.IOException
99 import java.io.PrintWriter
100 import java.util.concurrent.Executor
101 import javax.inject.Inject
102 
103 // URI fields to try loading album art from
104 private val ART_URIS =
105     arrayOf(
106         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
107         MediaMetadata.METADATA_KEY_ART_URI,
108         MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
109     )
110 
111 private const val TAG = "MediaDataManager"
112 private const val DEBUG = true
113 private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
114 
115 private val LOADING =
116     MediaData(
117         userId = -1,
118         initialized = false,
119         app = null,
120         appIcon = null,
121         artist = null,
122         song = null,
123         artwork = null,
124         actions = emptyList(),
125         actionsToShowInCompact = emptyList(),
126         packageName = "INVALID",
127         token = null,
128         clickIntent = null,
129         device = null,
130         active = true,
131         resumeAction = null,
132         instanceId = InstanceId.fakeInstanceId(-1),
133         appUid = Process.INVALID_UID
134     )
135 
136 internal val EMPTY_SMARTSPACE_MEDIA_DATA =
137     SmartspaceMediaData(
138         targetId = "INVALID",
139         isActive = false,
140         packageName = "INVALID",
141         cardAction = null,
142         recommendations = emptyList(),
143         dismissIntent = null,
144         headphoneConnectionTimeMillis = 0,
145         instanceId = InstanceId.fakeInstanceId(-1),
146         expiryTimeMs = 0,
147     )
148 
149 const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."
150 
151 /**
152  * Allow recommendations from smartspace to show in media controls. Requires
153  * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
154  */
155 private fun allowMediaRecommendations(context: Context): Boolean {
156     val flag =
157         Settings.Secure.getInt(
158             context.contentResolver,
159             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
160             1
161         )
162     return Utils.useQsMediaPlayer(context) && flag > 0
163 }
164 
165 /** A class that facilitates management and loading of Media Data, ready for binding. */
166 @SysUISingleton
167 class LegacyMediaDataManagerImpl(
168     private val context: Context,
169     @Background private val backgroundExecutor: Executor,
170     @Main private val uiExecutor: Executor,
171     @Main private val foregroundExecutor: DelayableExecutor,
172     private val mediaControllerFactory: MediaControllerFactory,
173     private val broadcastDispatcher: BroadcastDispatcher,
174     dumpManager: DumpManager,
175     mediaTimeoutListener: MediaTimeoutListener,
176     mediaResumeListener: MediaResumeListener,
177     mediaSessionBasedFilter: MediaSessionBasedFilter,
178     private val mediaDeviceManager: MediaDeviceManager,
179     mediaDataCombineLatest: MediaDataCombineLatest,
180     private val mediaDataFilter: LegacyMediaDataFilterImpl,
181     private val activityStarter: ActivityStarter,
182     private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
183     private var useMediaResumption: Boolean,
184     private val useQsMediaPlayer: Boolean,
185     private val systemClock: SystemClock,
186     private val tunerService: TunerService,
187     private val mediaFlags: MediaFlags,
188     private val logger: MediaUiEventLogger,
189     private val smartspaceManager: SmartspaceManager?,
190     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
191 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager {
192 
193     companion object {
194         // UI surface label for subscribing Smartspace updates.
195         @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
196 
197         // Smartspace package name's extra key.
198         @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
199 
200         // Maximum number of actions allowed in compact view
201         @JvmField val MAX_COMPACT_ACTIONS = 3
202 
203         // Maximum number of actions allowed in expanded view
204         @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
205     }
206 
207     private val themeText =
208         com.android.settingslib.Utils.getColorAttr(
209                 context,
210                 com.android.internal.R.attr.textColorPrimary
211             )
212             .defaultColor
213 
214     // Internal listeners are part of the internal pipeline. External listeners (those registered
215     // with [MediaDeviceManager.addListener]) receive events after they have propagated through
216     // the internal pipeline.
217     // Another way to think of the distinction between internal and external listeners is the
218     // following. Internal listeners are listeners that MediaDataManager depends on, and external
219     // listeners are listeners that depend on MediaDataManager.
220     // TODO(b/159539991#comment5): Move internal listeners to separate package.
221     private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
222     private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
223     // There should ONLY be at most one Smartspace media recommendation.
224     var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
225     @Keep private var smartspaceSession: SmartspaceSession? = null
226     private var allowMediaRecommendations = allowMediaRecommendations(context)
227 
228     private val artworkWidth =
229         context.resources.getDimensionPixelSize(
230             com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
231         )
232     private val artworkHeight =
233         context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
234 
235     @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
236     private val statusBarManager =
237         context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
238 
239     /** Check whether this notification is an RCN */
isRemoteCastNotificationnull240     private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
241         return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
242     }
243 
244     @Inject
245     constructor(
246         context: Context,
247         threadFactory: ThreadFactory,
248         @Main uiExecutor: Executor,
249         @Main foregroundExecutor: DelayableExecutor,
250         mediaControllerFactory: MediaControllerFactory,
251         dumpManager: DumpManager,
252         broadcastDispatcher: BroadcastDispatcher,
253         mediaTimeoutListener: MediaTimeoutListener,
254         mediaResumeListener: MediaResumeListener,
255         mediaSessionBasedFilter: MediaSessionBasedFilter,
256         mediaDeviceManager: MediaDeviceManager,
257         mediaDataCombineLatest: MediaDataCombineLatest,
258         mediaDataFilter: LegacyMediaDataFilterImpl,
259         activityStarter: ActivityStarter,
260         smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
261         clock: SystemClock,
262         tunerService: TunerService,
263         mediaFlags: MediaFlags,
264         logger: MediaUiEventLogger,
265         smartspaceManager: SmartspaceManager?,
266         keyguardUpdateMonitor: KeyguardUpdateMonitor,
267     ) : this(
268         context,
269         // Loading bitmap for UMO background can take longer time, so it cannot run on the default
270         // background thread. Use a custom thread for media.
271         threadFactory.buildExecutorOnNewThread(TAG),
272         uiExecutor,
273         foregroundExecutor,
274         mediaControllerFactory,
275         broadcastDispatcher,
276         dumpManager,
277         mediaTimeoutListener,
278         mediaResumeListener,
279         mediaSessionBasedFilter,
280         mediaDeviceManager,
281         mediaDataCombineLatest,
282         mediaDataFilter,
283         activityStarter,
284         smartspaceMediaDataProvider,
285         Utils.useMediaResumption(context),
286         Utils.useQsMediaPlayer(context),
287         clock,
288         tunerService,
289         mediaFlags,
290         logger,
291         smartspaceManager,
292         keyguardUpdateMonitor,
293     )
294 
295     private val appChangeReceiver =
296         object : BroadcastReceiver() {
onReceivenull297             override fun onReceive(context: Context, intent: Intent) {
298                 when (intent.action) {
299                     Intent.ACTION_PACKAGES_SUSPENDED -> {
300                         val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
301                         packages?.forEach { removeAllForPackage(it) }
302                     }
303                     Intent.ACTION_PACKAGE_REMOVED,
304                     Intent.ACTION_PACKAGE_RESTARTED -> {
305                         intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
306                     }
307                 }
308             }
309         }
310 
311     init {
312         dumpManager.registerDumpable(TAG, this)
313 
314         // Initialize the internal processing pipeline. The listeners at the front of the pipeline
315         // are set as internal listeners so that they receive events. From there, events are
316         // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
317         // so it is responsible for dispatching events to external listeners. To achieve this,
318         // external listeners that are registered with [MediaDataManager.addListener] are actually
319         // registered as listeners to mediaDataFilter.
320         addInternalListener(mediaTimeoutListener)
321         addInternalListener(mediaResumeListener)
322         addInternalListener(mediaSessionBasedFilter)
323         mediaSessionBasedFilter.addListener(mediaDeviceManager)
324         mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
325         mediaDeviceManager.addListener(mediaDataCombineLatest)
326         mediaDataCombineLatest.addListener(mediaDataFilter)
327 
328         // Set up links back into the pipeline for listeners that need to send events upstream.
timedOutnull329         mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
330             setInactive(key, timedOut)
331         }
statenull332         mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
333             updateState(key, state)
334         }
keynull335         mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
336         mediaResumeListener.setManager(this)
337         mediaDataFilter.mediaDataManager = this
338 
339         val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
340         broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
341 
342         val uninstallFilter =
<lambda>null343             IntentFilter().apply {
344                 addAction(Intent.ACTION_PACKAGE_REMOVED)
345                 addAction(Intent.ACTION_PACKAGE_RESTARTED)
346                 addDataScheme("package")
347             }
348         // BroadcastDispatcher does not allow filters with data schemes
349         context.registerReceiver(appChangeReceiver, uninstallFilter)
350 
351         // Register for Smartspace data updates.
352         smartspaceMediaDataProvider.registerListener(this)
353         smartspaceSession =
354             smartspaceManager?.createSmartspaceSession(
355                 SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
356             )
<lambda>null357         smartspaceSession?.let {
358             it.addOnTargetsAvailableListener(
359                 // Use a main uiExecutor thread listening to Smartspace updates instead of using
360                 // the existing background executor.
361                 // SmartspaceSession has scheduled routine updates which can be unpredictable on
362                 // test simulators, using the backgroundExecutor makes it's hard to test the threads
363                 // numbers.
364                 uiExecutor,
365                 SmartspaceSession.OnTargetsAvailableListener { targets ->
366                     smartspaceMediaDataProvider.onTargetsAvailable(targets)
367                 }
368             )
369         }
<lambda>null370         smartspaceSession?.let { it.requestSmartspaceUpdate() }
371         tunerService.addTunable(
372             object : TunerService.Tunable {
onTuningChangednull373                 override fun onTuningChanged(key: String?, newValue: String?) {
374                     allowMediaRecommendations = allowMediaRecommendations(context)
375                     if (!allowMediaRecommendations) {
376                         dismissSmartspaceRecommendation(
377                             key = smartspaceMediaData.targetId,
378                             delay = 0L
379                         )
380                     }
381                 }
382             },
383             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
384         )
385     }
386 
destroynull387     override fun destroy() {
388         smartspaceMediaDataProvider.unregisterListener(this)
389         smartspaceSession?.close()
390         smartspaceSession = null
391         context.unregisterReceiver(appChangeReceiver)
392     }
393 
onNotificationAddednull394     override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
395         if (useQsMediaPlayer && isMediaNotification(sbn)) {
396             var isNewlyActiveEntry = false
397             Assert.isMainThread()
398             val oldKey = findExistingEntry(key, sbn.packageName)
399             if (oldKey == null) {
400                 val instanceId = logger.getNewInstanceId()
401                 val temp =
402                     LOADING.copy(
403                         packageName = sbn.packageName,
404                         instanceId = instanceId,
405                         createdTimestampMillis = systemClock.currentTimeMillis(),
406                     )
407                 mediaEntries.put(key, temp)
408                 isNewlyActiveEntry = true
409             } else if (oldKey != key) {
410                 // Resume -> active conversion; move to new key
411                 val oldData = mediaEntries.remove(oldKey)!!
412                 isNewlyActiveEntry = true
413                 mediaEntries.put(key, oldData)
414             }
415             loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
416         } else {
417             onNotificationRemoved(key)
418         }
419     }
420 
removeAllForPackagenull421     private fun removeAllForPackage(packageName: String) {
422         Assert.isMainThread()
423         val toRemove = mediaEntries.filter { it.value.packageName == packageName }
424         toRemove.forEach { removeEntry(it.key) }
425     }
426 
setResumeActionnull427     override fun setResumeAction(key: String, action: Runnable?) {
428         mediaEntries.get(key)?.let {
429             it.resumeAction = action
430             it.hasCheckedForResume = true
431         }
432     }
433 
addResumptionControlsnull434     override fun addResumptionControls(
435         userId: Int,
436         desc: MediaDescription,
437         action: Runnable,
438         token: MediaSession.Token,
439         appName: String,
440         appIntent: PendingIntent,
441         packageName: String
442     ) {
443         // Resume controls don't have a notification key, so store by package name instead
444         if (!mediaEntries.containsKey(packageName)) {
445             val instanceId = logger.getNewInstanceId()
446             val appUid =
447                 try {
448                     context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
449                 } catch (e: PackageManager.NameNotFoundException) {
450                     Log.w(TAG, "Could not get app UID for $packageName", e)
451                     Process.INVALID_UID
452                 }
453 
454             val resumeData =
455                 LOADING.copy(
456                     packageName = packageName,
457                     resumeAction = action,
458                     hasCheckedForResume = true,
459                     instanceId = instanceId,
460                     appUid = appUid,
461                     createdTimestampMillis = systemClock.currentTimeMillis(),
462                 )
463             mediaEntries.put(packageName, resumeData)
464             logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
465             logger.logResumeMediaAdded(appUid, packageName, instanceId)
466         }
467         backgroundExecutor.execute {
468             loadMediaDataInBgForResumption(
469                 userId,
470                 desc,
471                 action,
472                 token,
473                 appName,
474                 appIntent,
475                 packageName
476             )
477         }
478     }
479 
480     /**
481      * Check if there is an existing entry that matches the key or package name. Returns the key
482      * that matches, or null if not found.
483      */
findExistingEntrynull484     private fun findExistingEntry(key: String, packageName: String): String? {
485         if (mediaEntries.containsKey(key)) {
486             return key
487         }
488         // Check if we already had a resume player
489         if (mediaEntries.containsKey(packageName)) {
490             return packageName
491         }
492         return null
493     }
494 
loadMediaDatanull495     private fun loadMediaData(
496         key: String,
497         sbn: StatusBarNotification,
498         oldKey: String?,
499         isNewlyActiveEntry: Boolean = false,
500     ) {
501         backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
502     }
503 
504     /** Add a listener for changes in this class */
addListenernull505     override fun addListener(listener: MediaDataManager.Listener) {
506         // mediaDataFilter is the current end of the internal pipeline. Register external
507         // listeners as listeners to it.
508         mediaDataFilter.addListener(listener)
509     }
510 
511     /** Remove a listener for changes in this class */
removeListenernull512     override fun removeListener(listener: MediaDataManager.Listener) {
513         // Since mediaDataFilter is the current end of the internal pipelie, external listeners
514         // have been registered to it. So, they need to be removed from it too.
515         mediaDataFilter.removeListener(listener)
516     }
517 
518     /** Add a listener for internal events. */
addInternalListenernull519     private fun addInternalListener(listener: MediaDataManager.Listener) =
520         internalListeners.add(listener)
521 
522     /**
523      * Notify internal listeners of media loaded event.
524      *
525      * External listeners registered with [addListener] will be notified after the event propagates
526      * through the internal listener pipeline.
527      */
528     private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
529         internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
530     }
531 
532     /**
533      * Notify internal listeners of Smartspace media loaded event.
534      *
535      * External listeners registered with [addListener] will be notified after the event propagates
536      * through the internal listener pipeline.
537      */
notifySmartspaceMediaDataLoadednull538     private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
539         internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
540     }
541 
542     /**
543      * Notify internal listeners of media removed event.
544      *
545      * External listeners registered with [addListener] will be notified after the event propagates
546      * through the internal listener pipeline.
547      */
notifyMediaDataRemovednull548     private fun notifyMediaDataRemoved(key: String, userInitiated: Boolean = false) {
549         internalListeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
550     }
551 
552     /**
553      * Notify internal listeners of Smartspace media removed event.
554      *
555      * External listeners registered with [addListener] will be notified after the event propagates
556      * through the internal listener pipeline.
557      *
558      * @param immediately indicates should apply the UI changes immediately, otherwise wait until
559      *   the next refresh-round before UI becomes visible. Should only be true if the update is
560      *   initiated by user's interaction.
561      */
notifySmartspaceMediaDataRemovednull562     private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
563         internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
564     }
565 
566     /**
567      * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
568      * will make the player not active anymore, hiding it from QQS and Keyguard.
569      *
570      * @see MediaData.active
571      */
setInactivenull572     override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) {
573         mediaEntries[key]?.let {
574             if (timedOut && !forceUpdate) {
575                 // Only log this event when media expires on its own
576                 logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
577             }
578             if (it.active == !timedOut && !forceUpdate) {
579                 if (it.resumption) {
580                     if (DEBUG) Log.d(TAG, "timing out resume player $key")
581                     dismissMediaData(key, delay = 0L, userInitiated = false)
582                 }
583                 return
584             }
585             // Update last active if media was still active.
586             if (it.active) {
587                 it.lastActive = systemClock.elapsedRealtime()
588             }
589             it.active = !timedOut
590             if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
591             onMediaDataLoaded(key, key, it)
592         }
593 
594         if (key == smartspaceMediaData.targetId) {
595             if (DEBUG) Log.d(TAG, "smartspace card expired")
596             dismissSmartspaceRecommendation(key, delay = 0L)
597         }
598     }
599 
600     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
updateStatenull601     private fun updateState(key: String, state: PlaybackState) {
602         mediaEntries.get(key)?.let {
603             val token = it.token
604             if (token == null) {
605                 if (DEBUG) Log.d(TAG, "State updated, but token was null")
606                 return
607             }
608             val actions =
609                 createActionsFromState(
610                     it.packageName,
611                     mediaControllerFactory.create(it.token),
612                     UserHandle(it.userId)
613                 )
614 
615             // Control buttons
616             // If flag is enabled and controller has a PlaybackState,
617             // create actions from session info
618             // otherwise, no need to update semantic actions.
619             val data =
620                 if (actions != null) {
621                     it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
622                 } else {
623                     it.copy(isPlaying = isPlayingState(state.state))
624                 }
625             if (DEBUG) Log.d(TAG, "State updated outside of notification")
626             onMediaDataLoaded(key, key, data)
627         }
628     }
629 
removeEntrynull630     private fun removeEntry(key: String, logEvent: Boolean = true, userInitiated: Boolean = false) {
631         mediaEntries.remove(key)?.let {
632             if (logEvent) {
633                 logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
634             }
635         }
636         notifyMediaDataRemoved(key, userInitiated)
637     }
638 
639     /** Dismiss a media entry. Returns false if the key was not found. */
dismissMediaDatanull640     override fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean {
641         val existed = mediaEntries[key] != null
642         backgroundExecutor.execute {
643             mediaEntries[key]?.let { mediaData ->
644                 if (mediaData.isLocalSession()) {
645                     mediaData.token?.let {
646                         val mediaController = mediaControllerFactory.create(it)
647                         mediaController.transportControls.stop()
648                     }
649                 }
650             }
651         }
652         foregroundExecutor.executeDelayed(
653             { removeEntry(key = key, userInitiated = userInitiated) },
654             delay
655         )
656         return existed
657     }
658 
659     /**
660      * Called whenever the recommendation has been expired or removed by the user. This will remove
661      * the recommendation card entirely from the carousel.
662      */
dismissSmartspaceRecommendationnull663     override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
664         if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
665             // If this doesn't match, or we've already invalidated the data, no action needed
666             return
667         }
668 
669         if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
670         if (smartspaceMediaData.isActive) {
671             smartspaceMediaData =
672                 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
673                     targetId = smartspaceMediaData.targetId,
674                     instanceId = smartspaceMediaData.instanceId
675                 )
676         }
677         foregroundExecutor.executeDelayed(
678             { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
679             delay
680         )
681     }
682 
683     /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
setRecommendationInactivenull684     override fun setRecommendationInactive(key: String) {
685         if (!mediaFlags.isPersistentSsCardEnabled()) {
686             Log.e(TAG, "Only persistent recommendation can be inactive!")
687             return
688         }
689         if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
690 
691         if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
692             // If this doesn't match, or we've already invalidated the data, no action needed
693             return
694         }
695 
696         smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
697         notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
698     }
699 
loadMediaDataInBgForResumptionnull700     private fun loadMediaDataInBgForResumption(
701         userId: Int,
702         desc: MediaDescription,
703         resumeAction: Runnable,
704         token: MediaSession.Token,
705         appName: String,
706         appIntent: PendingIntent,
707         packageName: String
708     ) {
709         if (desc.title.isNullOrBlank()) {
710             Log.e(TAG, "Description incomplete")
711             // Delete the placeholder entry
712             mediaEntries.remove(packageName)
713             return
714         }
715 
716         if (DEBUG) {
717             Log.d(TAG, "adding track for $userId from browser: $desc")
718         }
719 
720         val currentEntry = mediaEntries.get(packageName)
721         val appUid = currentEntry?.appUid ?: Process.INVALID_UID
722 
723         // Album art
724         var artworkBitmap = desc.iconBitmap
725         if (artworkBitmap == null && desc.iconUri != null) {
726             artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
727         }
728         val artworkIcon =
729             if (artworkBitmap != null) {
730                 Icon.createWithBitmap(artworkBitmap)
731             } else {
732                 null
733             }
734 
735         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
736         val isExplicit =
737             desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
738                 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
739 
740         val progress =
741             if (mediaFlags.isResumeProgressEnabled()) {
742                 MediaDataUtils.getDescriptionProgress(desc.extras)
743             } else null
744 
745         val mediaAction = getResumeMediaAction(resumeAction)
746         val lastActive = systemClock.elapsedRealtime()
747         val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
748         foregroundExecutor.execute {
749             onMediaDataLoaded(
750                 packageName,
751                 null,
752                 MediaData(
753                     userId,
754                     true,
755                     appName,
756                     null,
757                     desc.subtitle,
758                     desc.title,
759                     artworkIcon,
760                     listOf(mediaAction),
761                     listOf(0),
762                     MediaButton(playOrPause = mediaAction),
763                     packageName,
764                     token,
765                     appIntent,
766                     device = null,
767                     active = false,
768                     resumeAction = resumeAction,
769                     resumption = true,
770                     notificationKey = packageName,
771                     hasCheckedForResume = true,
772                     lastActive = lastActive,
773                     createdTimestampMillis = createdTimestampMillis,
774                     instanceId = instanceId,
775                     appUid = appUid,
776                     isExplicit = isExplicit,
777                     resumeProgress = progress,
778                 )
779             )
780         }
781     }
782 
loadMediaDataInBgnull783     fun loadMediaDataInBg(
784         key: String,
785         sbn: StatusBarNotification,
786         oldKey: String?,
787         isNewlyActiveEntry: Boolean = false,
788     ) {
789         val token =
790             sbn.notification.extras.getParcelable(
791                 Notification.EXTRA_MEDIA_SESSION,
792                 MediaSession.Token::class.java
793             )
794         if (token == null) {
795             return
796         }
797         val mediaController = mediaControllerFactory.create(token)
798         val metadata = mediaController.metadata
799         val notif: Notification = sbn.notification
800 
801         val appInfo =
802             notif.extras.getParcelable(
803                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
804                 ApplicationInfo::class.java
805             )
806                 ?: getAppInfoFromPackage(sbn.packageName)
807 
808         // App name
809         val appName = getAppName(sbn, appInfo)
810 
811         // Song name
812         var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
813         if (song.isNullOrBlank()) {
814             song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
815         }
816         if (song.isNullOrBlank()) {
817             song = HybridGroupManager.resolveTitle(notif)
818         }
819         if (song.isNullOrBlank()) {
820             // For apps that don't include a title, log and add a placeholder
821             song = context.getString(R.string.controls_media_empty_title, appName)
822             try {
823                 statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
824             } catch (e: RuntimeException) {
825                 Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
826             }
827         }
828 
829         // Album art
830         var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
831         if (artworkBitmap == null) {
832             artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
833         }
834         if (artworkBitmap == null) {
835             artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
836         }
837         val artWorkIcon =
838             if (artworkBitmap == null) {
839                 notif.getLargeIcon()
840             } else {
841                 Icon.createWithBitmap(artworkBitmap)
842             }
843 
844         // App Icon
845         val smallIcon = sbn.notification.smallIcon
846 
847         // Explicit Indicator
848         var isExplicit = false
849         val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
850         isExplicit =
851             mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
852                 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
853 
854         // Artist name
855         var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
856         if (artist.isNullOrBlank()) {
857             artist = HybridGroupManager.resolveText(notif)
858         }
859 
860         // Device name (used for remote cast notifications)
861         var device: MediaDeviceData? = null
862         if (isRemoteCastNotification(sbn)) {
863             val extras = sbn.notification.extras
864             val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
865             val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
866             val deviceIntent =
867                 extras.getParcelable(
868                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
869                     PendingIntent::class.java
870                 )
871             Log.d(TAG, "$key is RCN for $deviceName")
872 
873             if (deviceName != null && deviceIcon > -1) {
874                 // Name and icon must be present, but intent may be null
875                 val enabled = deviceIntent != null && deviceIntent.isActivity
876                 val deviceDrawable =
877                     Icon.createWithResource(sbn.packageName, deviceIcon)
878                         .loadDrawable(sbn.getPackageContext(context))
879                 device =
880                     MediaDeviceData(
881                         enabled,
882                         deviceDrawable,
883                         deviceName,
884                         deviceIntent,
885                         showBroadcastButton = false
886                     )
887             }
888         }
889 
890         // Control buttons
891         // If flag is enabled and controller has a PlaybackState, create actions from session info
892         // Otherwise, use the notification actions
893         var actionIcons: List<MediaAction> = emptyList()
894         var actionsToShowCollapsed: List<Int> = emptyList()
895         val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
896         if (semanticActions == null) {
897             val actions = createActionsFromNotification(sbn)
898             actionIcons = actions.first
899             actionsToShowCollapsed = actions.second
900         }
901 
902         val playbackLocation =
903             if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
904             else if (
905                 mediaController.playbackInfo?.playbackType ==
906                     MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
907             )
908                 MediaData.PLAYBACK_LOCAL
909             else MediaData.PLAYBACK_CAST_LOCAL
910         val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
911 
912         val currentEntry = mediaEntries.get(key)
913         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
914         val appUid = appInfo?.uid ?: Process.INVALID_UID
915 
916         if (isNewlyActiveEntry) {
917             logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
918             logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
919         } else if (playbackLocation != currentEntry?.playbackLocation) {
920             logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
921         }
922 
923         val lastActive = systemClock.elapsedRealtime()
924         val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
925         foregroundExecutor.execute {
926             val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
927             val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
928             val active = mediaEntries[key]?.active ?: true
929             onMediaDataLoaded(
930                 key,
931                 oldKey,
932                 MediaData(
933                     sbn.normalizedUserId,
934                     true,
935                     appName,
936                     smallIcon,
937                     artist,
938                     song,
939                     artWorkIcon,
940                     actionIcons,
941                     actionsToShowCollapsed,
942                     semanticActions,
943                     sbn.packageName,
944                     token,
945                     notif.contentIntent,
946                     device,
947                     active,
948                     resumeAction = resumeAction,
949                     playbackLocation = playbackLocation,
950                     notificationKey = key,
951                     hasCheckedForResume = hasCheckedForResume,
952                     isPlaying = isPlaying,
953                     isClearable = !sbn.isOngoing,
954                     lastActive = lastActive,
955                     createdTimestampMillis = createdTimestampMillis,
956                     instanceId = instanceId,
957                     appUid = appUid,
958                     isExplicit = isExplicit,
959                 )
960             )
961         }
962     }
963 
logSingleVsMultipleMediaAddednull964     private fun logSingleVsMultipleMediaAdded(
965         appUid: Int,
966         packageName: String,
967         instanceId: InstanceId
968     ) {
969         if (mediaEntries.size == 1) {
970             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
971         } else if (mediaEntries.size == 2) {
972             // Since this method is only called when there is a new media session added.
973             // logging needed once there is more than one media session in carousel.
974             logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
975         }
976     }
977 
getAppInfoFromPackagenull978     private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
979         try {
980             return context.packageManager.getApplicationInfo(packageName, 0)
981         } catch (e: PackageManager.NameNotFoundException) {
982             Log.w(TAG, "Could not get app info for $packageName", e)
983         }
984         return null
985     }
986 
getAppNamenull987     private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
988         val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
989         if (name != null) {
990             return name
991         }
992 
993         return if (appInfo != null) {
994             context.packageManager.getApplicationLabel(appInfo).toString()
995         } else {
996             sbn.packageName
997         }
998     }
999 
1000     /** Generate action buttons based on notification actions */
createActionsFromNotificationnull1001     private fun createActionsFromNotification(
1002         sbn: StatusBarNotification
1003     ): Pair<List<MediaAction>, List<Int>> {
1004         val notif = sbn.notification
1005         val actionIcons: MutableList<MediaAction> = ArrayList()
1006         val actions = notif.actions
1007         var actionsToShowCollapsed =
1008             notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
1009                 ?: mutableListOf()
1010         if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
1011             Log.e(
1012                 TAG,
1013                 "Too many compact actions for ${sbn.key}," +
1014                     "limiting to first $MAX_COMPACT_ACTIONS"
1015             )
1016             actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
1017         }
1018 
1019         if (actions != null) {
1020             for ((index, action) in actions.withIndex()) {
1021                 if (index == MAX_NOTIFICATION_ACTIONS) {
1022                     Log.w(
1023                         TAG,
1024                         "Too many notification actions for ${sbn.key}," +
1025                             " limiting to first $MAX_NOTIFICATION_ACTIONS"
1026                     )
1027                     break
1028                 }
1029                 if (action.getIcon() == null) {
1030                     if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
1031                     actionsToShowCollapsed.remove(index)
1032                     continue
1033                 }
1034                 val runnable =
1035                     if (action.actionIntent != null) {
1036                         Runnable {
1037                             if (action.actionIntent.isActivity) {
1038                                 activityStarter.startPendingIntentDismissingKeyguard(
1039                                     action.actionIntent
1040                                 )
1041                             } else if (action.isAuthenticationRequired()) {
1042                                 activityStarter.dismissKeyguardThenExecute(
1043                                     {
1044                                         var result = sendPendingIntent(action.actionIntent)
1045                                         result
1046                                     },
1047                                     {},
1048                                     true
1049                                 )
1050                             } else {
1051                                 sendPendingIntent(action.actionIntent)
1052                             }
1053                         }
1054                     } else {
1055                         null
1056                     }
1057                 val mediaActionIcon =
1058                     if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
1059                             Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
1060                         } else {
1061                             action.getIcon()
1062                         }
1063                         .setTint(themeText)
1064                         .loadDrawable(context)
1065                 val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
1066                 actionIcons.add(mediaAction)
1067             }
1068         }
1069         return Pair(actionIcons, actionsToShowCollapsed)
1070     }
1071 
1072     /**
1073      * Generates action button info for this media session based on the PlaybackState
1074      *
1075      * @param packageName Package name for the media app
1076      * @param controller MediaController for the current session
1077      * @return a Pair consisting of a list of media actions, and a list of ints representing which
1078      *
1079      * ```
1080      *      of those actions should be shown in the compact player
1081      * ```
1082      */
createActionsFromStatenull1083     private fun createActionsFromState(
1084         packageName: String,
1085         controller: MediaController,
1086         user: UserHandle
1087     ): MediaButton? {
1088         val state = controller.playbackState
1089         if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
1090             return null
1091         }
1092 
1093         // First, check for standard actions
1094         val playOrPause =
1095             if (isConnectingState(state.state)) {
1096                 // Spinner needs to be animating to render anything. Start it here.
1097                 val drawable =
1098                     context.getDrawable(com.android.internal.R.drawable.progress_small_material)
1099                 (drawable as Animatable).start()
1100                 MediaAction(
1101                     drawable,
1102                     null, // no action to perform when clicked
1103                     context.getString(R.string.controls_media_button_connecting),
1104                     context.getDrawable(R.drawable.ic_media_connecting_container),
1105                     // Specify a rebind id to prevent the spinner from restarting on later binds.
1106                     com.android.internal.R.drawable.progress_small_material
1107                 )
1108             } else if (isPlayingState(state.state)) {
1109                 getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
1110             } else {
1111                 getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
1112             }
1113         val prevButton =
1114             getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
1115         val nextButton =
1116             getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
1117 
1118         // Then, create a way to build any custom actions that will be needed
1119         val customActions =
1120             state.customActions
1121                 .asSequence()
1122                 .filterNotNull()
1123                 .map { getCustomAction(state, packageName, controller, it) }
1124                 .iterator()
1125         fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
1126 
1127         // Finally, assign the remaining button slots: play/pause A B C D
1128         // A = previous, else custom action (if not reserved)
1129         // B = next, else custom action (if not reserved)
1130         // C and D are always custom actions
1131         val reservePrev =
1132             controller.extras?.getBoolean(
1133                 MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
1134             ) == true
1135         val reserveNext =
1136             controller.extras?.getBoolean(
1137                 MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
1138             ) == true
1139 
1140         val prevOrCustom =
1141             if (prevButton != null) {
1142                 prevButton
1143             } else if (!reservePrev) {
1144                 nextCustomAction()
1145             } else {
1146                 null
1147             }
1148 
1149         val nextOrCustom =
1150             if (nextButton != null) {
1151                 nextButton
1152             } else if (!reserveNext) {
1153                 nextCustomAction()
1154             } else {
1155                 null
1156             }
1157 
1158         return MediaButton(
1159             playOrPause,
1160             nextOrCustom,
1161             prevOrCustom,
1162             nextCustomAction(),
1163             nextCustomAction(),
1164             reserveNext,
1165             reservePrev
1166         )
1167     }
1168 
1169     /**
1170      * Create a [MediaAction] for a given action and media session
1171      *
1172      * @param controller MediaController for the session
1173      * @param stateActions The actions included with the session's [PlaybackState]
1174      * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
1175      * ```
1176      *      [PlaybackState.ACTION_PLAY]
1177      *      [PlaybackState.ACTION_PAUSE]
1178      *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
1179      *      [PlaybackState.ACTION_SKIP_TO_NEXT]
1180      * @return
1181      * ```
1182      *
1183      * A [MediaAction] with correct values set, or null if the state doesn't support it
1184      */
getStandardActionnull1185     private fun getStandardAction(
1186         controller: MediaController,
1187         stateActions: Long,
1188         @PlaybackState.Actions action: Long
1189     ): MediaAction? {
1190         if (!includesAction(stateActions, action)) {
1191             return null
1192         }
1193 
1194         return when (action) {
1195             PlaybackState.ACTION_PLAY -> {
1196                 MediaAction(
1197                     context.getDrawable(R.drawable.ic_media_play),
1198                     { controller.transportControls.play() },
1199                     context.getString(R.string.controls_media_button_play),
1200                     context.getDrawable(R.drawable.ic_media_play_container)
1201                 )
1202             }
1203             PlaybackState.ACTION_PAUSE -> {
1204                 MediaAction(
1205                     context.getDrawable(R.drawable.ic_media_pause),
1206                     { controller.transportControls.pause() },
1207                     context.getString(R.string.controls_media_button_pause),
1208                     context.getDrawable(R.drawable.ic_media_pause_container)
1209                 )
1210             }
1211             PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
1212                 MediaAction(
1213                     context.getDrawable(R.drawable.ic_media_prev),
1214                     { controller.transportControls.skipToPrevious() },
1215                     context.getString(R.string.controls_media_button_prev),
1216                     null
1217                 )
1218             }
1219             PlaybackState.ACTION_SKIP_TO_NEXT -> {
1220                 MediaAction(
1221                     context.getDrawable(R.drawable.ic_media_next),
1222                     { controller.transportControls.skipToNext() },
1223                     context.getString(R.string.controls_media_button_next),
1224                     null
1225                 )
1226             }
1227             else -> null
1228         }
1229     }
1230 
1231     /** Check whether the actions from a [PlaybackState] include a specific action */
includesActionnull1232     private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
1233         if (
1234             (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
1235                 (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
1236         ) {
1237             return true
1238         }
1239         return (stateActions and action != 0L)
1240     }
1241 
1242     /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
getCustomActionnull1243     private fun getCustomAction(
1244         state: PlaybackState,
1245         packageName: String,
1246         controller: MediaController,
1247         customAction: PlaybackState.CustomAction
1248     ): MediaAction {
1249         return MediaAction(
1250             Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
1251             { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
1252             customAction.name,
1253             null
1254         )
1255     }
1256 
1257     /** Load a bitmap from the various Art metadata URIs */
loadBitmapFromUrinull1258     private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
1259         for (uri in ART_URIS) {
1260             val uriString = metadata.getString(uri)
1261             if (!TextUtils.isEmpty(uriString)) {
1262                 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
1263                 if (albumArt != null) {
1264                     if (DEBUG) Log.d(TAG, "loaded art from $uri")
1265                     return albumArt
1266                 }
1267             }
1268         }
1269         return null
1270     }
1271 
sendPendingIntentnull1272     private fun sendPendingIntent(intent: PendingIntent): Boolean {
1273         return try {
1274             val options = BroadcastOptions.makeBasic()
1275             options.setInteractive(true)
1276             options.setPendingIntentBackgroundActivityStartMode(
1277                 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
1278             )
1279             intent.send(options.toBundle())
1280             true
1281         } catch (e: PendingIntent.CanceledException) {
1282             Log.d(TAG, "Intent canceled", e)
1283             false
1284         }
1285     }
1286 
1287     /** Returns a bitmap if the user can access the given URI, else null */
loadBitmapFromUriForUsernull1288     private fun loadBitmapFromUriForUser(
1289         uri: Uri,
1290         userId: Int,
1291         appUid: Int,
1292         packageName: String,
1293     ): Bitmap? {
1294         try {
1295             val ugm = UriGrantsManager.getService()
1296             ugm.checkGrantUriPermission_ignoreNonSystem(
1297                 appUid,
1298                 packageName,
1299                 ContentProvider.getUriWithoutUserId(uri),
1300                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
1301                 ContentProvider.getUserIdFromUri(uri, userId)
1302             )
1303             return loadBitmapFromUri(uri)
1304         } catch (e: SecurityException) {
1305             Log.e(TAG, "Failed to get URI permission: $e")
1306         }
1307         return null
1308     }
1309 
1310     /**
1311      * Load a bitmap from a URI
1312      *
1313      * @param uri the uri to load
1314      * @return bitmap, or null if couldn't be loaded
1315      */
loadBitmapFromUrinull1316     private fun loadBitmapFromUri(uri: Uri): Bitmap? {
1317         // ImageDecoder requires a scheme of the following types
1318         if (uri.scheme == null) {
1319             return null
1320         }
1321 
1322         if (
1323             !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
1324                 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
1325                 !uri.scheme.equals(ContentResolver.SCHEME_FILE)
1326         ) {
1327             return null
1328         }
1329 
1330         val source = ImageDecoder.createSource(context.contentResolver, uri)
1331         return try {
1332             ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
1333                 val width = info.size.width
1334                 val height = info.size.height
1335                 val scale =
1336                     MediaDataUtils.getScaleFactor(
1337                         APair(width, height),
1338                         APair(artworkWidth, artworkHeight)
1339                     )
1340 
1341                 // Downscale if needed
1342                 if (scale != 0f && scale < 1) {
1343                     decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
1344                 }
1345                 decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
1346             }
1347         } catch (e: IOException) {
1348             Log.e(TAG, "Unable to load bitmap", e)
1349             null
1350         } catch (e: RuntimeException) {
1351             Log.e(TAG, "Unable to load bitmap", e)
1352             null
1353         }
1354     }
1355 
getResumeMediaActionnull1356     private fun getResumeMediaAction(action: Runnable): MediaAction {
1357         return MediaAction(
1358             Icon.createWithResource(context, R.drawable.ic_media_play)
1359                 .setTint(themeText)
1360                 .loadDrawable(context),
1361             action,
1362             context.getString(R.string.controls_media_resume),
1363             context.getDrawable(R.drawable.ic_media_play_container)
1364         )
1365     }
1366 
onMediaDataLoadednull1367     fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
1368         traceSection("MediaDataManager#onMediaDataLoaded") {
1369             Assert.isMainThread()
1370             if (mediaEntries.containsKey(key)) {
1371                 // Otherwise this was removed already
1372                 mediaEntries.put(key, data)
1373                 notifyMediaDataLoaded(key, oldKey, data)
1374             }
1375         }
1376 
onSmartspaceTargetsUpdatednull1377     override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
1378         if (!allowMediaRecommendations) {
1379             if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
1380             return
1381         }
1382 
1383         val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
1384         when (mediaTargets.size) {
1385             0 -> {
1386                 if (!smartspaceMediaData.isActive) {
1387                     return
1388                 }
1389                 if (DEBUG) {
1390                     Log.d(TAG, "Set Smartspace media to be inactive for the data update")
1391                 }
1392                 if (mediaFlags.isPersistentSsCardEnabled()) {
1393                     // Smartspace uses this signal to hide the card (e.g. when it expires or user
1394                     // disconnects headphones), so treat as setting inactive when flag is on
1395                     smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
1396                     notifySmartspaceMediaDataLoaded(
1397                         smartspaceMediaData.targetId,
1398                         smartspaceMediaData,
1399                     )
1400                 } else {
1401                     smartspaceMediaData =
1402                         EMPTY_SMARTSPACE_MEDIA_DATA.copy(
1403                             targetId = smartspaceMediaData.targetId,
1404                             instanceId = smartspaceMediaData.instanceId,
1405                         )
1406                     notifySmartspaceMediaDataRemoved(
1407                         smartspaceMediaData.targetId,
1408                         immediately = false,
1409                     )
1410                 }
1411             }
1412             1 -> {
1413                 val newMediaTarget = mediaTargets.get(0)
1414                 if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
1415                     // The same Smartspace updates can be received. Skip the duplicate updates.
1416                     return
1417                 }
1418                 if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
1419                 smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
1420                 notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
1421             }
1422             else -> {
1423                 // There should NOT be more than 1 Smartspace media update. When it happens, it
1424                 // indicates a bad state or an error. Reset the status accordingly.
1425                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
1426                 notifySmartspaceMediaDataRemoved(
1427                     smartspaceMediaData.targetId,
1428                     immediately = false,
1429                 )
1430                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
1431             }
1432         }
1433     }
1434 
onNotificationRemovednull1435     override fun onNotificationRemoved(key: String) {
1436         Assert.isMainThread()
1437         val removed = mediaEntries.remove(key) ?: return
1438         if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
1439             logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
1440         } else if (isAbleToResume(removed)) {
1441             convertToResumePlayer(key, removed)
1442         } else if (mediaFlags.isRetainingPlayersEnabled()) {
1443             handlePossibleRemoval(key, removed, notificationRemoved = true)
1444         } else {
1445             notifyMediaDataRemoved(key)
1446             logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
1447         }
1448     }
1449 
onSessionDestroyednull1450     private fun onSessionDestroyed(key: String) {
1451         if (DEBUG) Log.d(TAG, "session destroyed for $key")
1452         val entry = mediaEntries.remove(key) ?: return
1453         // Clear token since the session is no longer valid
1454         val updated = entry.copy(token = null)
1455         handlePossibleRemoval(key, updated)
1456     }
1457 
isAbleToResumenull1458     private fun isAbleToResume(data: MediaData): Boolean {
1459         val isEligibleForResume =
1460             data.isLocalSession() ||
1461                 (mediaFlags.isRemoteResumeAllowed() &&
1462                     data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
1463         return useMediaResumption && data.resumeAction != null && isEligibleForResume
1464     }
1465 
1466     /**
1467      * Convert to resume state if the player is no longer valid and active, then notify listeners
1468      * that the data was updated. Does not convert to resume state if the player is still valid, or
1469      * if it was removed before becoming inactive. (Assumes that [removed] was removed from
1470      * [mediaEntries] before this function was called)
1471      */
handlePossibleRemovalnull1472     private fun handlePossibleRemoval(
1473         key: String,
1474         removed: MediaData,
1475         notificationRemoved: Boolean = false
1476     ) {
1477         val hasSession = removed.token != null
1478         if (hasSession && removed.semanticActions != null) {
1479             // The app was using session actions, and the session is still valid: keep player
1480             if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
1481             mediaEntries.put(key, removed)
1482             notifyMediaDataLoaded(key, key, removed)
1483         } else if (!notificationRemoved && removed.semanticActions == null) {
1484             // The app was using notification actions, and notif wasn't removed yet: keep player
1485             if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
1486             mediaEntries.put(key, removed)
1487             notifyMediaDataLoaded(key, key, removed)
1488         } else if (removed.active && !isAbleToResume(removed)) {
1489             // This player was still active - it didn't last long enough to time out,
1490             // and its app doesn't normally support resume: remove
1491             if (DEBUG) Log.d(TAG, "Removing still-active player $key")
1492             notifyMediaDataRemoved(key)
1493             logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
1494         } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
1495             // Convert to resume
1496             if (DEBUG) {
1497                 Log.d(
1498                     TAG,
1499                     "Notification ($notificationRemoved) and/or session " +
1500                         "($hasSession) gone for inactive player $key"
1501                 )
1502             }
1503             convertToResumePlayer(key, removed)
1504         } else {
1505             // Retaining players flag is off and app doesn't support resume: remove player.
1506             if (DEBUG) Log.d(TAG, "Removing player $key")
1507             notifyMediaDataRemoved(key)
1508             logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
1509         }
1510     }
1511 
1512     /** Set the given [MediaData] as a resume state player and notify listeners */
convertToResumePlayernull1513     private fun convertToResumePlayer(key: String, data: MediaData) {
1514         if (DEBUG) Log.d(TAG, "Converting $key to resume")
1515         // Resumption controls must have a title.
1516         if (data.song.isNullOrBlank()) {
1517             Log.e(TAG, "Description incomplete")
1518             notifyMediaDataRemoved(key)
1519             logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
1520             return
1521         }
1522         // Move to resume key (aka package name) if that key doesn't already exist.
1523         val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
1524         val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
1525         val launcherIntent =
1526             context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
1527                 PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
1528             }
1529         val lastActive =
1530             if (data.active) {
1531                 systemClock.elapsedRealtime()
1532             } else {
1533                 data.lastActive
1534             }
1535         val updated =
1536             data.copy(
1537                 token = null,
1538                 actions = actions,
1539                 semanticActions = MediaButton(playOrPause = resumeAction),
1540                 actionsToShowInCompact = listOf(0),
1541                 active = false,
1542                 resumption = true,
1543                 isPlaying = false,
1544                 isClearable = true,
1545                 clickIntent = launcherIntent,
1546                 lastActive = lastActive,
1547             )
1548         val pkg = data.packageName
1549         val migrate = mediaEntries.put(pkg, updated) == null
1550         // Notify listeners of "new" controls when migrating or removed and update when not
1551         Log.d(TAG, "migrating? $migrate from $key -> $pkg")
1552         if (migrate) {
1553             notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
1554         } else {
1555             // Since packageName is used for the key of the resumption controls, it is
1556             // possible that another notification has already been reused for the resumption
1557             // controls of this package. In this case, rather than renaming this player as
1558             // packageName, just remove it and then send a update to the existing resumption
1559             // controls.
1560             notifyMediaDataRemoved(key)
1561             notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
1562         }
1563         logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
1564 
1565         // Limit total number of resume controls
1566         val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption }
1567         val numResume = resumeEntries.size
1568         if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
1569             resumeEntries
1570                 .toList()
1571                 .sortedBy { (key, data) -> data.lastActive }
1572                 .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
1573                 .forEach { (key, data) ->
1574                     Log.d(TAG, "Removing excess control $key")
1575                     mediaEntries.remove(key)
1576                     notifyMediaDataRemoved(key)
1577                     logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
1578                 }
1579         }
1580     }
1581 
setMediaResumptionEnablednull1582     override fun setMediaResumptionEnabled(isEnabled: Boolean) {
1583         if (useMediaResumption == isEnabled) {
1584             return
1585         }
1586 
1587         useMediaResumption = isEnabled
1588 
1589         if (!useMediaResumption) {
1590             // Remove any existing resume controls
1591             val filtered = mediaEntries.filter { !it.value.active }
1592             filtered.forEach {
1593                 mediaEntries.remove(it.key)
1594                 notifyMediaDataRemoved(it.key)
1595                 logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
1596             }
1597         }
1598     }
1599 
1600     /** Invoked when the user has dismissed the media carousel */
onSwipeToDismissnull1601     override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
1602 
1603     /** Are there any media notifications active, including the recommendations? */
1604     override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
1605 
1606     /**
1607      * Are there any media entries we should display, including the recommendations?
1608      * - If resumption is enabled, this will include inactive players
1609      * - If resumption is disabled, we only want to show active players
1610      */
1611     override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
1612 
1613     /** Are there any resume media notifications active, excluding the recommendations? */
1614     override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
1615 
1616     /**
1617      * Are there any resume media notifications active, excluding the recommendations?
1618      * - If resumption is enabled, this will include inactive players
1619      * - If resumption is disabled, we only want to show active players
1620      */
1621     override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
1622     override fun isRecommendationActive() = smartspaceMediaData.isActive
1623 
1624     /**
1625      * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
1626      *
1627      * @return An empty SmartspaceMediaData with the valid target Id is returned if the
1628      *   SmartspaceTarget's data is invalid.
1629      */
1630     private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
1631         val baseAction: SmartspaceAction? = target.baseAction
1632         val dismissIntent =
1633             baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
1634 
1635         val isActive =
1636             when {
1637                 !mediaFlags.isPersistentSsCardEnabled() -> true
1638                 baseAction == null -> true
1639                 else -> {
1640                     val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
1641                     triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
1642                 }
1643             }
1644 
1645         packageName(target)?.let {
1646             return SmartspaceMediaData(
1647                 targetId = target.smartspaceTargetId,
1648                 isActive = isActive,
1649                 packageName = it,
1650                 cardAction = target.baseAction,
1651                 recommendations = target.iconGrid,
1652                 dismissIntent = dismissIntent,
1653                 headphoneConnectionTimeMillis = target.creationTimeMillis,
1654                 instanceId = logger.getNewInstanceId(),
1655                 expiryTimeMs = target.expiryTimeMillis,
1656             )
1657         }
1658         return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
1659             targetId = target.smartspaceTargetId,
1660             isActive = isActive,
1661             dismissIntent = dismissIntent,
1662             headphoneConnectionTimeMillis = target.creationTimeMillis,
1663             instanceId = logger.getNewInstanceId(),
1664             expiryTimeMs = target.expiryTimeMillis,
1665         )
1666     }
1667 
packageNamenull1668     private fun packageName(target: SmartspaceTarget): String? {
1669         val recommendationList = target.iconGrid
1670         if (recommendationList == null || recommendationList.isEmpty()) {
1671             Log.w(TAG, "Empty or null media recommendation list.")
1672             return null
1673         }
1674         for (recommendation in recommendationList) {
1675             val extras = recommendation.extras
1676             extras?.let {
1677                 it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
1678                     return packageName
1679                 }
1680             }
1681         }
1682         Log.w(TAG, "No valid package name is provided.")
1683         return null
1684     }
1685 
dumpnull1686     override fun dump(pw: PrintWriter, args: Array<out String>) {
1687         pw.apply {
1688             println("internalListeners: $internalListeners")
1689             println("externalListeners: ${mediaDataFilter.listeners}")
1690             println("mediaEntries: $mediaEntries")
1691             println("useMediaResumption: $useMediaResumption")
1692             println("allowMediaRecommendations: $allowMediaRecommendations")
1693         }
1694         mediaDeviceManager.dump(pw)
1695     }
1696 }
1697