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