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
18 
19 import android.app.Notification
20 import android.app.PendingIntent
21 import android.app.smartspace.SmartspaceConfig
22 import android.app.smartspace.SmartspaceManager
23 import android.app.smartspace.SmartspaceSession
24 import android.app.smartspace.SmartspaceTarget
25 import android.content.BroadcastReceiver
26 import android.content.ContentResolver
27 import android.content.Context
28 import android.content.Intent
29 import android.content.IntentFilter
30 import android.content.pm.ApplicationInfo
31 import android.content.pm.PackageManager
32 import android.graphics.Bitmap
33 import android.graphics.Canvas
34 import android.graphics.ImageDecoder
35 import android.graphics.drawable.Drawable
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.net.Uri
42 import android.os.Parcelable
43 import android.os.UserHandle
44 import android.provider.Settings
45 import android.service.notification.StatusBarNotification
46 import android.text.TextUtils
47 import android.util.Log
48 import com.android.internal.annotations.VisibleForTesting
49 import com.android.systemui.Dumpable
50 import com.android.systemui.R
51 import com.android.systemui.broadcast.BroadcastDispatcher
52 import com.android.systemui.dagger.SysUISingleton
53 import com.android.systemui.dagger.qualifiers.Background
54 import com.android.systemui.dagger.qualifiers.Main
55 import com.android.systemui.dump.DumpManager
56 import com.android.systemui.plugins.ActivityStarter
57 import com.android.systemui.plugins.BcSmartspaceDataPlugin
58 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
59 import com.android.systemui.statusbar.notification.row.HybridGroupManager
60 import com.android.systemui.tuner.TunerService
61 import com.android.systemui.util.Assert
62 import com.android.systemui.util.Utils
63 import com.android.systemui.util.concurrency.DelayableExecutor
64 import com.android.systemui.util.time.SystemClock
65 import java.io.FileDescriptor
66 import java.io.IOException
67 import java.io.PrintWriter
68 import java.util.concurrent.Executor
69 import java.util.concurrent.Executors
70 import javax.inject.Inject
71 
72 // URI fields to try loading album art from
73 private val ART_URIS = arrayOf(
74         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
75         MediaMetadata.METADATA_KEY_ART_URI,
76         MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
77 )
78 
79 private const val TAG = "MediaDataManager"
80 private const val DEBUG = true
81 private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
82 
83 private val LOADING = MediaData(-1, false, 0, null, null, null, null, null,
84         emptyList(), emptyList(), "INVALID", null, null, null, true, null)
85 @VisibleForTesting
86 internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData("INVALID", false, false,
87     "INVALID", null, emptyList(), null, 0, 0)
88 
89 fun isMediaNotification(sbn: StatusBarNotification): Boolean {
90     return sbn.notification.isMediaNotification()
91 }
92 
93 /**
94  * A class that facilitates management and loading of Media Data, ready for binding.
95  */
96 @SysUISingleton
97 class MediaDataManager(
98     private val context: Context,
99     @Background private val backgroundExecutor: Executor,
100     @Main private val foregroundExecutor: DelayableExecutor,
101     private val mediaControllerFactory: MediaControllerFactory,
102     private val broadcastDispatcher: BroadcastDispatcher,
103     dumpManager: DumpManager,
104     mediaTimeoutListener: MediaTimeoutListener,
105     mediaResumeListener: MediaResumeListener,
106     mediaSessionBasedFilter: MediaSessionBasedFilter,
107     mediaDeviceManager: MediaDeviceManager,
108     mediaDataCombineLatest: MediaDataCombineLatest,
109     private val mediaDataFilter: MediaDataFilter,
110     private val activityStarter: ActivityStarter,
111     private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
112     private var useMediaResumption: Boolean,
113     private val useQsMediaPlayer: Boolean,
114     private val systemClock: SystemClock,
115     private val tunerService: TunerService
116 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
117 
118     companion object {
119         // UI surface label for subscribing Smartspace updates.
120         @JvmField
121         val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
122 
123         // Smartspace package name's extra key.
124         @JvmField
125         val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
126 
127         // Maximum number of actions allowed in compact view
128         @JvmField
129         val MAX_COMPACT_ACTIONS = 3
130     }
131 
132     private val themeText = com.android.settingslib.Utils.getColorAttr(context,
133             com.android.internal.R.attr.textColorPrimary).defaultColor
134     private val bgColor = context.getColor(android.R.color.system_accent2_50)
135 
136     // Internal listeners are part of the internal pipeline. External listeners (those registered
137     // with [MediaDeviceManager.addListener]) receive events after they have propagated through
138     // the internal pipeline.
139     // Another way to think of the distinction between internal and external listeners is the
140     // following. Internal listeners are listeners that MediaDataManager depends on, and external
141     // listeners are listeners that depend on MediaDataManager.
142     // TODO(b/159539991#comment5): Move internal listeners to separate package.
143     private val internalListeners: MutableSet<Listener> = mutableSetOf()
144     private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
145     // There should ONLY be at most one Smartspace media recommendation.
146     var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
147     private var smartspaceSession: SmartspaceSession? = null
148     private var allowMediaRecommendations = Utils.allowMediaRecommendations(context)
149 
150     /**
151      * Check whether this notification is an RCN
152      * TODO(b/204910409) implement new API for explicitly declaring this
153      */
154     private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
155         val pm = context.packageManager
156         try {
157             val info = pm.getApplicationInfo(sbn.packageName, PackageManager.MATCH_SYSTEM_ONLY)
158             if (info.privateFlags and ApplicationInfo.PRIVATE_FLAG_PRIVILEGED != 0) {
159                 val extras = sbn.notification.extras
160                 if (extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
161                     return true
162                 }
163             }
164         } catch (e: PackageManager.NameNotFoundException) { }
165         return false
166     }
167 
168     @Inject
169     constructor(
170         context: Context,
171         @Background backgroundExecutor: Executor,
172         @Main foregroundExecutor: DelayableExecutor,
173         mediaControllerFactory: MediaControllerFactory,
174         dumpManager: DumpManager,
175         broadcastDispatcher: BroadcastDispatcher,
176         mediaTimeoutListener: MediaTimeoutListener,
177         mediaResumeListener: MediaResumeListener,
178         mediaSessionBasedFilter: MediaSessionBasedFilter,
179         mediaDeviceManager: MediaDeviceManager,
180         mediaDataCombineLatest: MediaDataCombineLatest,
181         mediaDataFilter: MediaDataFilter,
182         activityStarter: ActivityStarter,
183         smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
184         clock: SystemClock,
185         tunerService: TunerService
186     ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
187             broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
188             mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
189             activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context),
190             Utils.useQsMediaPlayer(context), clock, tunerService)
191 
192     private val appChangeReceiver = object : BroadcastReceiver() {
193         override fun onReceive(context: Context, intent: Intent) {
194             when (intent.action) {
195                 Intent.ACTION_PACKAGES_SUSPENDED -> {
196                     val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
197                     packages?.forEach {
198                         removeAllForPackage(it)
199                     }
200                 }
201                 Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> {
202                     intent.data?.encodedSchemeSpecificPart?.let {
203                         removeAllForPackage(it)
204                     }
205                 }
206             }
207         }
208     }
209 
210     init {
211         dumpManager.registerDumpable(TAG, this)
212 
213         // Initialize the internal processing pipeline. The listeners at the front of the pipeline
214         // are set as internal listeners so that they receive events. From there, events are
215         // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
216         // so it is responsible for dispatching events to external listeners. To achieve this,
217         // external listeners that are registered with [MediaDataManager.addListener] are actually
218         // registered as listeners to mediaDataFilter.
219         addInternalListener(mediaTimeoutListener)
220         addInternalListener(mediaResumeListener)
221         addInternalListener(mediaSessionBasedFilter)
222         mediaSessionBasedFilter.addListener(mediaDeviceManager)
223         mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
224         mediaDeviceManager.addListener(mediaDataCombineLatest)
225         mediaDataCombineLatest.addListener(mediaDataFilter)
226 
227         // Set up links back into the pipeline for listeners that need to send events upstream.
228         mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
229             setTimedOut(key, timedOut) }
230         mediaResumeListener.setManager(this)
231         mediaDataFilter.mediaDataManager = this
232 
233         val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
234         broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
235 
236         val uninstallFilter = IntentFilter().apply {
237             addAction(Intent.ACTION_PACKAGE_REMOVED)
238             addAction(Intent.ACTION_PACKAGE_RESTARTED)
239             addDataScheme("package")
240         }
241         // BroadcastDispatcher does not allow filters with data schemes
242         context.registerReceiver(appChangeReceiver, uninstallFilter)
243 
244         // Register for Smartspace data updates.
245         smartspaceMediaDataProvider.registerListener(this)
246         val smartspaceManager: SmartspaceManager =
247             context.getSystemService(SmartspaceManager::class.java)
248         smartspaceSession = smartspaceManager.createSmartspaceSession(
249             SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build())
250         smartspaceSession?.let {
251             it.addOnTargetsAvailableListener(
252                 // Use a new thread listening to Smartspace updates instead of using the existing
253                 // backgroundExecutor. SmartspaceSession has scheduled routine updates which can be
254                 // unpredictable on test simulators, using the backgroundExecutor makes it's hard to
255                 // test the threads numbers.
256                 // Switch to use backgroundExecutor when SmartspaceSession has a good way to be
257                 // mocked.
258                 Executors.newCachedThreadPool(),
259                 SmartspaceSession.OnTargetsAvailableListener { targets ->
260                     smartspaceMediaDataProvider.onTargetsAvailable(targets)
261                 })
262         }
263         smartspaceSession?.let { it.requestSmartspaceUpdate() }
264         tunerService.addTunable(object : TunerService.Tunable {
265             override fun onTuningChanged(key: String?, newValue: String?) {
266                 allowMediaRecommendations = Utils.allowMediaRecommendations(context)
267                 if (!allowMediaRecommendations) {
268                     dismissSmartspaceRecommendation(key = smartspaceMediaData.targetId, delay = 0L)
269                 }
270             }
271         }, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)
272     }
273 
274     fun destroy() {
275         smartspaceMediaDataProvider.unregisterListener(this)
276         context.unregisterReceiver(appChangeReceiver)
277     }
278 
279     fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
280         if (useQsMediaPlayer && isMediaNotification(sbn)) {
281             Assert.isMainThread()
282             val oldKey = findExistingEntry(key, sbn.packageName)
283             if (oldKey == null) {
284                 val temp = LOADING.copy(packageName = sbn.packageName)
285                 mediaEntries.put(key, temp)
286             } else if (oldKey != key) {
287                 // Move to new key
288                 val oldData = mediaEntries.remove(oldKey)!!
289                 mediaEntries.put(key, oldData)
290             }
291             loadMediaData(key, sbn, oldKey)
292         } else {
293             onNotificationRemoved(key)
294         }
295     }
296 
297     private fun removeAllForPackage(packageName: String) {
298         Assert.isMainThread()
299         val toRemove = mediaEntries.filter { it.value.packageName == packageName }
300         toRemove.forEach {
301             removeEntry(it.key)
302         }
303     }
304 
305     fun setResumeAction(key: String, action: Runnable?) {
306         mediaEntries.get(key)?.let {
307             it.resumeAction = action
308             it.hasCheckedForResume = true
309         }
310     }
311 
312     fun addResumptionControls(
313         userId: Int,
314         desc: MediaDescription,
315         action: Runnable,
316         token: MediaSession.Token,
317         appName: String,
318         appIntent: PendingIntent,
319         packageName: String
320     ) {
321         // Resume controls don't have a notification key, so store by package name instead
322         if (!mediaEntries.containsKey(packageName)) {
323             val resumeData = LOADING.copy(packageName = packageName, resumeAction = action,
324                 hasCheckedForResume = true)
325             mediaEntries.put(packageName, resumeData)
326         }
327         backgroundExecutor.execute {
328             loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent,
329                 packageName)
330         }
331     }
332 
333     /**
334      * Check if there is an existing entry that matches the key or package name.
335      * Returns the key that matches, or null if not found.
336      */
337     private fun findExistingEntry(key: String, packageName: String): String? {
338         if (mediaEntries.containsKey(key)) {
339             return key
340         }
341         // Check if we already had a resume player
342         if (mediaEntries.containsKey(packageName)) {
343             return packageName
344         }
345         return null
346     }
347 
348     private fun loadMediaData(
349         key: String,
350         sbn: StatusBarNotification,
351         oldKey: String?
352     ) {
353         backgroundExecutor.execute {
354             loadMediaDataInBg(key, sbn, oldKey)
355         }
356     }
357 
358     /**
359      * Add a listener for changes in this class
360      */
361     fun addListener(listener: Listener) {
362         // mediaDataFilter is the current end of the internal pipeline. Register external
363         // listeners as listeners to it.
364         mediaDataFilter.addListener(listener)
365     }
366 
367     /**
368      * Remove a listener for changes in this class
369      */
370     fun removeListener(listener: Listener) {
371         // Since mediaDataFilter is the current end of the internal pipelie, external listeners
372         // have been registered to it. So, they need to be removed from it too.
373         mediaDataFilter.removeListener(listener)
374     }
375 
376     /**
377      * Add a listener for internal events.
378      */
379     private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
380 
381     /**
382      * Notify internal listeners of media loaded event.
383      *
384      * External listeners registered with [addListener] will be notified after the event propagates
385      * through the internal listener pipeline.
386      */
387     private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
388         internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
389     }
390 
391     /**
392      * Notify internal listeners of Smartspace media loaded event.
393      *
394      * External listeners registered with [addListener] will be notified after the event propagates
395      * through the internal listener pipeline.
396      */
397     private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
398         internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
399     }
400 
401     /**
402      * Notify internal listeners of media removed event.
403      *
404      * External listeners registered with [addListener] will be notified after the event propagates
405      * through the internal listener pipeline.
406      */
407     private fun notifyMediaDataRemoved(key: String) {
408         internalListeners.forEach { it.onMediaDataRemoved(key) }
409     }
410 
411     /**
412      * Notify internal listeners of Smartspace media removed event.
413      *
414      * External listeners registered with [addListener] will be notified after the event propagates
415      * through the internal listener pipeline.
416      *
417      * @param immediately indicates should apply the UI changes immediately, otherwise wait until
418      * the next refresh-round before UI becomes visible. Should only be true if the update is
419      * initiated by user's interaction.
420      */
421     private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
422         internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
423     }
424 
425     /**
426      * Called whenever the player has been paused or stopped for a while, or swiped from QQS.
427      * This will make the player not active anymore, hiding it from QQS and Keyguard.
428      * @see MediaData.active
429      */
430     internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
431         mediaEntries[key]?.let {
432             if (it.active == !timedOut && !forceUpdate) {
433                 if (it.resumption) {
434                     if (DEBUG) Log.d(TAG, "timing out resume player $key")
435                     dismissMediaData(key, 0L /* delay */)
436                 }
437                 return
438             }
439             it.active = !timedOut
440             if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
441             onMediaDataLoaded(key, key, it)
442         }
443     }
444 
445     private fun removeEntry(key: String) {
446         mediaEntries.remove(key)
447         notifyMediaDataRemoved(key)
448     }
449 
450     /**
451      * Dismiss a media entry. Returns false if the key was not found.
452      */
453     fun dismissMediaData(key: String, delay: Long): Boolean {
454         val existed = mediaEntries[key] != null
455         backgroundExecutor.execute {
456             mediaEntries[key]?.let { mediaData ->
457                 if (mediaData.isLocalSession()) {
458                     mediaData.token?.let {
459                         val mediaController = mediaControllerFactory.create(it)
460                         mediaController.transportControls.stop()
461                     }
462                 }
463             }
464         }
465         foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
466         return existed
467     }
468 
469     /**
470      * Called whenever the recommendation has been expired, or swiped from QQS.
471      * This will make the recommendation view to not be shown anymore during this headphone
472      * connection session.
473      */
474     fun dismissSmartspaceRecommendation(key: String, delay: Long) {
475         if (smartspaceMediaData.targetId != key) {
476             return
477         }
478 
479         if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
480         if (smartspaceMediaData.isActive) {
481             smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
482                 targetId = smartspaceMediaData.targetId)
483         }
484         foregroundExecutor.executeDelayed(
485             { notifySmartspaceMediaDataRemoved(
486                 smartspaceMediaData.targetId, immediately = true) }, delay)
487     }
488 
489     private fun loadMediaDataInBgForResumption(
490         userId: Int,
491         desc: MediaDescription,
492         resumeAction: Runnable,
493         token: MediaSession.Token,
494         appName: String,
495         appIntent: PendingIntent,
496         packageName: String
497     ) {
498         if (TextUtils.isEmpty(desc.title)) {
499             Log.e(TAG, "Description incomplete")
500             // Delete the placeholder entry
501             mediaEntries.remove(packageName)
502             return
503         }
504 
505         if (DEBUG) {
506             Log.d(TAG, "adding track for $userId from browser: $desc")
507         }
508 
509         // Album art
510         var artworkBitmap = desc.iconBitmap
511         if (artworkBitmap == null && desc.iconUri != null) {
512             artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
513         }
514         val artworkIcon = if (artworkBitmap != null) {
515             Icon.createWithBitmap(artworkBitmap)
516         } else {
517             null
518         }
519 
520         val mediaAction = getResumeMediaAction(resumeAction)
521         val lastActive = systemClock.elapsedRealtime()
522         foregroundExecutor.execute {
523             onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName,
524                     null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
525                     packageName, token, appIntent, device = null, active = false,
526                     resumeAction = resumeAction, resumption = true, notificationKey = packageName,
527                     hasCheckedForResume = true, lastActive = lastActive))
528         }
529     }
530 
531     private fun loadMediaDataInBg(
532         key: String,
533         sbn: StatusBarNotification,
534         oldKey: String?
535     ) {
536         val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
537                 as MediaSession.Token?
538         val mediaController = mediaControllerFactory.create(token)
539         val metadata = mediaController.metadata
540 
541         // Foreground and Background colors computed from album art
542         val notif: Notification = sbn.notification
543         var artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
544         if (artworkBitmap == null) {
545             artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
546         }
547         if (artworkBitmap == null && metadata != null) {
548             artworkBitmap = loadBitmapFromUri(metadata)
549         }
550         val artWorkIcon = if (artworkBitmap == null) {
551             notif.getLargeIcon()
552         } else {
553             Icon.createWithBitmap(artworkBitmap)
554         }
555         if (artWorkIcon != null) {
556             // If we have art, get colors from that
557             if (artworkBitmap == null) {
558                 if (artWorkIcon.type == Icon.TYPE_BITMAP ||
559                         artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) {
560                     artworkBitmap = artWorkIcon.bitmap
561                 } else {
562                     val drawable: Drawable = artWorkIcon.loadDrawable(context)
563                     artworkBitmap = Bitmap.createBitmap(
564                             drawable.intrinsicWidth,
565                             drawable.intrinsicHeight,
566                             Bitmap.Config.ARGB_8888)
567                     val canvas = Canvas(artworkBitmap)
568                     drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
569                     drawable.draw(canvas)
570                 }
571             }
572         }
573 
574         // App name
575         val builder = Notification.Builder.recoverBuilder(context, notif)
576         val app = builder.loadHeaderAppName()
577 
578         // App Icon
579         val smallIcon = sbn.notification.smallIcon
580 
581         // Song name
582         var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
583         if (song == null) {
584             song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
585         }
586         if (song == null) {
587             song = HybridGroupManager.resolveTitle(notif)
588         }
589 
590         // Artist name
591         var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
592         if (artist == null) {
593             artist = HybridGroupManager.resolveText(notif)
594         }
595 
596         // Control buttons
597         val actionIcons: MutableList<MediaAction> = ArrayList()
598         val actions = notif.actions
599         var actionsToShowCollapsed = notif.extras.getIntArray(
600                 Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf<Int>()
601         if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
602             Log.e(TAG, "Too many compact actions for $key, limiting to first $MAX_COMPACT_ACTIONS")
603             actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
604         }
605         // TODO: b/153736623 look into creating actions when this isn't a media style notification
606 
607         if (actions != null) {
608             for ((index, action) in actions.withIndex()) {
609                 if (action.getIcon() == null) {
610                     if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
611                     actionsToShowCollapsed.remove(index)
612                     continue
613                 }
614                 val runnable = if (action.actionIntent != null) {
615                     Runnable {
616                         if (action.isAuthenticationRequired()) {
617                             activityStarter.dismissKeyguardThenExecute({
618                                 var result = sendPendingIntent(action.actionIntent)
619                                 result
620                             }, {}, true)
621                         } else {
622                             sendPendingIntent(action.actionIntent)
623                         }
624                     }
625                 } else {
626                     null
627                 }
628                 val mediaActionIcon = if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
629                     Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
630                 } else {
631                     action.getIcon()
632                 }.setTint(themeText)
633                 val mediaAction = MediaAction(
634                         mediaActionIcon,
635                         runnable,
636                         action.title)
637                 actionIcons.add(mediaAction)
638             }
639         }
640 
641         val playbackLocation =
642                 if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
643                 else if (mediaController.playbackInfo?.playbackType ==
644                     MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
645                 else MediaData.PLAYBACK_CAST_LOCAL
646         val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
647         val lastActive = systemClock.elapsedRealtime()
648         foregroundExecutor.execute {
649             val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
650             val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
651             val active = mediaEntries[key]?.active ?: true
652             onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
653                     smallIcon, artist, song, artWorkIcon, actionIcons,
654                     actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
655                     active, resumeAction = resumeAction, playbackLocation = playbackLocation,
656                     notificationKey = key, hasCheckedForResume = hasCheckedForResume,
657                     isPlaying = isPlaying, isClearable = sbn.isClearable(),
658                     lastActive = lastActive))
659         }
660     }
661 
662     /**
663      * Load a bitmap from the various Art metadata URIs
664      */
665     private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
666         for (uri in ART_URIS) {
667             val uriString = metadata.getString(uri)
668             if (!TextUtils.isEmpty(uriString)) {
669                 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
670                 if (albumArt != null) {
671                     if (DEBUG) Log.d(TAG, "loaded art from $uri")
672                     return albumArt
673                 }
674             }
675         }
676         return null
677     }
678 
679     private fun sendPendingIntent(intent: PendingIntent): Boolean {
680         return try {
681             intent.send()
682             true
683         } catch (e: PendingIntent.CanceledException) {
684             Log.d(TAG, "Intent canceled", e)
685             false
686         }
687     }
688     /**
689      * Load a bitmap from a URI
690      * @param uri the uri to load
691      * @return bitmap, or null if couldn't be loaded
692      */
693     private fun loadBitmapFromUri(uri: Uri): Bitmap? {
694         // ImageDecoder requires a scheme of the following types
695         if (uri.scheme == null) {
696             return null
697         }
698 
699         if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
700                 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
701                 !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
702             return null
703         }
704 
705         val source = ImageDecoder.createSource(context.getContentResolver(), uri)
706         return try {
707             ImageDecoder.decodeBitmap(source) {
708                 decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
709             }
710         } catch (e: IOException) {
711             Log.e(TAG, "Unable to load bitmap", e)
712             null
713         } catch (e: RuntimeException) {
714             Log.e(TAG, "Unable to load bitmap", e)
715             null
716         }
717     }
718 
719     private fun getResumeMediaAction(action: Runnable): MediaAction {
720         return MediaAction(
721             Icon.createWithResource(context, R.drawable.lb_ic_play).setTint(themeText),
722             action,
723             context.getString(R.string.controls_media_resume)
724         )
725     }
726 
727     fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
728         Assert.isMainThread()
729         if (mediaEntries.containsKey(key)) {
730             // Otherwise this was removed already
731             mediaEntries.put(key, data)
732             notifyMediaDataLoaded(key, oldKey, data)
733         }
734     }
735 
736     override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
737         if (!allowMediaRecommendations) {
738             if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
739             return
740         }
741 
742         val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
743         when (mediaTargets.size) {
744             0 -> {
745                 if (!smartspaceMediaData.isActive) {
746                     return
747                 }
748                 if (DEBUG) {
749                     Log.d(TAG, "Set Smartspace media to be inactive for the data update")
750                 }
751                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
752                     targetId = smartspaceMediaData.targetId)
753                 notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
754             }
755             1 -> {
756                 val newMediaTarget = mediaTargets.get(0)
757                 if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
758                     // The same Smartspace updates can be received. Skip the duplicate updates.
759                     return
760                 }
761                 if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
762                 smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
763                 notifySmartspaceMediaDataLoaded(
764                     smartspaceMediaData.targetId, smartspaceMediaData)
765             }
766             else -> {
767                 // There should NOT be more than 1 Smartspace media update. When it happens, it
768                 // indicates a bad state or an error. Reset the status accordingly.
769                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
770                 notifySmartspaceMediaDataRemoved(
771                     smartspaceMediaData.targetId, false /* immediately */)
772                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
773             }
774         }
775     }
776 
777     fun onNotificationRemoved(key: String) {
778         Assert.isMainThread()
779         val removed = mediaEntries.remove(key)
780         if (useMediaResumption && removed?.resumeAction != null && removed?.isLocalSession()) {
781             Log.d(TAG, "Not removing $key because resumable")
782             // Move to resume key (aka package name) if that key doesn't already exist.
783             val resumeAction = getResumeMediaAction(removed.resumeAction!!)
784             val updated = removed.copy(token = null, actions = listOf(resumeAction),
785                     actionsToShowInCompact = listOf(0), active = false, resumption = true,
786                     isPlaying = false, isClearable = true)
787             val pkg = removed.packageName
788             val migrate = mediaEntries.put(pkg, updated) == null
789             // Notify listeners of "new" controls when migrating or removed and update when not
790             if (migrate) {
791                 notifyMediaDataLoaded(pkg, key, updated)
792             } else {
793                 // Since packageName is used for the key of the resumption controls, it is
794                 // possible that another notification has already been reused for the resumption
795                 // controls of this package. In this case, rather than renaming this player as
796                 // packageName, just remove it and then send a update to the existing resumption
797                 // controls.
798                 notifyMediaDataRemoved(key)
799                 notifyMediaDataLoaded(pkg, pkg, updated)
800             }
801             return
802         }
803         if (removed != null) {
804             notifyMediaDataRemoved(key)
805         }
806     }
807 
808     fun setMediaResumptionEnabled(isEnabled: Boolean) {
809         if (useMediaResumption == isEnabled) {
810             return
811         }
812 
813         useMediaResumption = isEnabled
814 
815         if (!useMediaResumption) {
816             // Remove any existing resume controls
817             val filtered = mediaEntries.filter { !it.value.active }
818             filtered.forEach {
819                 mediaEntries.remove(it.key)
820                 notifyMediaDataRemoved(it.key)
821             }
822         }
823     }
824 
825     /**
826      * Invoked when the user has dismissed the media carousel
827      */
828     fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
829 
830     /**
831      * Are there any media notifications active?
832      */
833     fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
834 
835     /**
836      * Are there any media entries we should display?
837      * If resumption is enabled, this will include inactive players
838      * If resumption is disabled, we only want to show active players
839      */
840     fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
841 
842     interface Listener {
843 
844         /**
845          * Called whenever there's new MediaData Loaded for the consumption in views.
846          *
847          * oldKey is provided to check whether the view has changed keys, which can happen when a
848          * player has gone from resume state (key is package name) to active state (key is
849          * notification key) or vice versa.
850          *
851          * @param immediately indicates should apply the UI changes immediately, otherwise wait
852          * until the next refresh-round before UI becomes visible. True by default to take in place
853          * immediately.
854          *
855          * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI
856          * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace
857          * signal.
858          */
859         fun onMediaDataLoaded(
860             key: String,
861             oldKey: String?,
862             data: MediaData,
863             immediately: Boolean = true,
864             receivedSmartspaceCardLatency: Int = 0
865         ) {}
866 
867         /**
868          * Called whenever there's new Smartspace media data loaded.
869          *
870          * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true,
871          * it will be prioritized as the first card. Otherwise, it will show up as the last card as
872          * default.
873          *
874          * @param isSsReactivated indicates resume media card is reactivated by Smartspace
875          * recommendation signal
876          */
877         fun onSmartspaceMediaDataLoaded(
878             key: String,
879             data: SmartspaceMediaData,
880             shouldPrioritize: Boolean = false,
881             isSsReactivated: Boolean = false
882         ) {}
883 
884         /** Called whenever a previously existing Media notification was removed. */
885         fun onMediaDataRemoved(key: String) {}
886 
887         /**
888          * Called whenever a previously existing Smartspace media data was removed.
889          *
890          * @param immediately indicates should apply the UI changes immediately, otherwise wait
891          * until the next refresh-round before UI becomes visible. True by default to take in place
892          * immediately.
893          */
894         fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
895     }
896 
897     /**
898      * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
899      *
900      * @return An empty SmartspaceMediaData with the valid target Id is returned if the
901      * SmartspaceTarget's data is invalid.
902      */
903     private fun toSmartspaceMediaData(
904         target: SmartspaceTarget,
905         isActive: Boolean
906     ): SmartspaceMediaData {
907         var dismissIntent: Intent? = null
908         if (target.baseAction != null && target.baseAction.extras != null) {
909             dismissIntent = target
910                 .baseAction
911                 .extras
912                 .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
913         }
914         packageName(target)?.let {
915             return SmartspaceMediaData(target.smartspaceTargetId, isActive, true, it,
916                 target.baseAction, target.iconGrid,
917                 dismissIntent, 0, target.creationTimeMillis)
918         }
919         return EMPTY_SMARTSPACE_MEDIA_DATA
920             .copy(targetId = target.smartspaceTargetId,
921                     isActive = isActive,
922                     dismissIntent = dismissIntent,
923                     headphoneConnectionTimeMillis = target.creationTimeMillis)
924     }
925 
926     private fun packageName(target: SmartspaceTarget): String? {
927         val recommendationList = target.iconGrid
928         if (recommendationList == null || recommendationList.isEmpty()) {
929             Log.w(TAG, "Empty or null media recommendation list.")
930             return null
931         }
932         for (recommendation in recommendationList) {
933             val extras = recommendation.extras
934             extras?.let {
935                 it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let {
936                     packageName -> return packageName }
937             }
938         }
939         Log.w(TAG, "No valid package name is provided.")
940         return null
941     }
942 
943     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
944         pw.apply {
945             println("internalListeners: $internalListeners")
946             println("externalListeners: ${mediaDataFilter.listeners}")
947             println("mediaEntries: $mediaEntries")
948             println("useMediaResumption: $useMediaResumption")
949         }
950     }
951 }
952