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