blob: 6ea36ab3af2fe016c0f463c85a4f8e9703c90099 [file] [log] [blame]
Selim Cinek5dbef2d2020-05-07 17:44:38 -07001/*
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
17package com.android.systemui.media
18
19import android.app.Notification
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040020import android.app.PendingIntent
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040021import android.content.BroadcastReceiver
Selim Cinekc5436712020-04-27 15:15:44 -070022import android.content.ContentResolver
Selim Cinek5dbef2d2020-05-07 17:44:38 -070023import android.content.Context
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040024import android.content.Intent
25import android.content.IntentFilter
Selim Cinek5dbef2d2020-05-07 17:44:38 -070026import android.graphics.Bitmap
Selim Cinekc5436712020-04-27 15:15:44 -070027import android.graphics.Canvas
Lucas Dupin96bdbee2020-05-14 15:51:32 -070028import android.graphics.Color
Selim Cinekc5436712020-04-27 15:15:44 -070029import android.graphics.ImageDecoder
Selim Cinek5dbef2d2020-05-07 17:44:38 -070030import android.graphics.drawable.Drawable
Selim Cinekc5436712020-04-27 15:15:44 -070031import android.graphics.drawable.Icon
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040032import android.media.MediaDescription
Selim Cinek5dbef2d2020-05-07 17:44:38 -070033import android.media.MediaMetadata
34import android.media.session.MediaSession
Selim Cinekc5436712020-04-27 15:15:44 -070035import android.net.Uri
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040036import android.os.UserHandle
Selim Cinek5dbef2d2020-05-07 17:44:38 -070037import android.service.notification.StatusBarNotification
Selim Cinekc5436712020-04-27 15:15:44 -070038import android.text.TextUtils
39import android.util.Log
Lucas Dupin96bdbee2020-05-14 15:51:32 -070040import com.android.internal.graphics.ColorUtils
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040041import com.android.systemui.R
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040042import com.android.systemui.broadcast.BroadcastDispatcher
Selim Cinek5dbef2d2020-05-07 17:44:38 -070043import com.android.systemui.dagger.qualifiers.Background
44import com.android.systemui.dagger.qualifiers.Main
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040045import com.android.systemui.statusbar.NotificationMediaManager
Selim Cinek5dbef2d2020-05-07 17:44:38 -070046import com.android.systemui.statusbar.notification.MediaNotificationProcessor
Lucas Dupin6f0bd312020-05-28 18:19:29 -070047import com.android.systemui.statusbar.notification.NotificationEntryManager
48import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON
Selim Cinekc5436712020-04-27 15:15:44 -070049import com.android.systemui.statusbar.notification.row.HybridGroupManager
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -040050import com.android.systemui.util.Assert
Lucas Dupin03859d72020-05-12 12:27:47 -070051import com.android.systemui.util.Utils
Selim Cinekc5436712020-04-27 15:15:44 -070052import java.io.IOException
Selim Cinek5dbef2d2020-05-07 17:44:38 -070053import java.util.concurrent.Executor
54import javax.inject.Inject
55import javax.inject.Singleton
Selim Cinek5dbef2d2020-05-07 17:44:38 -070056
Selim Cinekc5436712020-04-27 15:15:44 -070057// URI fields to try loading album art from
58private val ART_URIS = arrayOf(
59 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
60 MediaMetadata.METADATA_KEY_ART_URI,
61 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
62)
63
64private const val TAG = "MediaDataManager"
Lucas Dupin96bdbee2020-05-14 15:51:32 -070065private const val DEFAULT_LUMINOSITY = 0.25f
66private const val LUMINOSITY_THRESHOLD = 0.05f
67private const val SATURATION_MULTIPLIER = 0.8f
Selim Cinekc5436712020-04-27 15:15:44 -070068
Beth Thibodeau4580c832020-05-15 20:11:44 -040069private val LOADING = MediaData(false, 0, null, null, null, null, null,
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040070 emptyList(), emptyList(), "INVALID", null, null, null, null)
Robert Snoeberger70d0d6b2020-05-14 16:47:02 -040071
72fun isMediaNotification(sbn: StatusBarNotification): Boolean {
73 if (!sbn.notification.hasMediaSession()) {
74 return false
75 }
76 val notificationStyle = sbn.notification.notificationStyle
77 if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) ||
78 Notification.MediaStyle::class.java.equals(notificationStyle)) {
79 return true
80 }
81 return false
82}
Selim Cinekc5436712020-04-27 15:15:44 -070083
Selim Cinek5dbef2d2020-05-07 17:44:38 -070084/**
85 * A class that facilitates management and loading of Media Data, ready for binding.
86 */
87@Singleton
88class MediaDataManager @Inject constructor(
89 private val context: Context,
90 private val mediaControllerFactory: MediaControllerFactory,
Lucas Dupin6f0bd312020-05-28 18:19:29 -070091 private val mediaTimeoutListener: MediaTimeoutListener,
92 private val notificationEntryManager: NotificationEntryManager,
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040093 private val mediaResumeListener: MediaResumeListener,
Selim Cinek5dbef2d2020-05-07 17:44:38 -070094 @Background private val backgroundExecutor: Executor,
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040095 @Main private val foregroundExecutor: Executor,
96 private val broadcastDispatcher: BroadcastDispatcher
Selim Cinek5dbef2d2020-05-07 17:44:38 -070097) {
98
Selim Cinekb52642b2020-04-17 14:30:29 -070099 private val listeners: MutableSet<Listener> = mutableSetOf()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700100 private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400101 private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700102
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400103 private val userChangeReceiver = object : BroadcastReceiver() {
104 override fun onReceive(context: Context, intent: Intent) {
105 if (Intent.ACTION_USER_SWITCHED == intent.action) {
106 // Remove all controls, regardless of state
107 clearData()
108 }
109 }
110 }
111
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700112 init {
113 mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
114 setTimedOut(token, timedOut) }
115 addListener(mediaTimeoutListener)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400116
117 if (useMediaResumption) {
118 mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
119 resumeAction: Runnable, token: MediaSession.Token, appName: String,
120 appIntent: PendingIntent, packageName: String ->
121 addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName)
122 }
123 mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? ->
124 mediaEntries.get(key)?.resumeAction = action
125 mediaEntries.get(key)?.hasCheckedForResume = true
126 }
127 addListener(mediaResumeListener)
128 }
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400129
130 val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED)
131 broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL)
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700132 }
133
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700134 fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
Robert Snoeberger70d0d6b2020-05-14 16:47:02 -0400135 if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400136 Assert.isMainThread()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400137 val oldKey = findExistingEntry(key, sbn.packageName)
138 if (oldKey == null) {
139 val temp = LOADING.copy(packageName = sbn.packageName)
140 mediaEntries.put(key, temp)
141 } else if (oldKey != key) {
142 // Move to new key
143 val oldData = mediaEntries.remove(oldKey)!!
144 mediaEntries.put(key, oldData)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700145 }
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400146 loadMediaData(key, sbn, oldKey)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700147 } else {
148 onNotificationRemoved(key)
149 }
150 }
151
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400152 private fun clearData() {
153 // Called on user change. Remove all current MediaData objects and inform listeners
154 val listenersCopy = listeners.toSet()
155 mediaEntries.forEach {
156 listenersCopy.forEach { listener ->
157 listener.onMediaDataRemoved(it.key)
158 }
159 }
160 mediaEntries.clear()
161 }
162
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400163 private fun addResumptionControls(
164 desc: MediaDescription,
165 action: Runnable,
166 token: MediaSession.Token,
167 appName: String,
168 appIntent: PendingIntent,
169 packageName: String
170 ) {
171 // Resume controls don't have a notification key, so store by package name instead
172 if (!mediaEntries.containsKey(packageName)) {
173 val resumeData = LOADING.copy(packageName = packageName, resumeAction = action)
174 mediaEntries.put(packageName, resumeData)
175 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700176 backgroundExecutor.execute {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400177 loadMediaDataInBg(desc, action, token, appName, appIntent, packageName)
178 }
179 }
180
181 /**
182 * Check if there is an existing entry that matches the key or package name.
183 * Returns the key that matches, or null if not found.
184 */
185 private fun findExistingEntry(key: String, packageName: String): String? {
186 if (mediaEntries.containsKey(key)) {
187 return key
188 }
189 // Check if we already had a resume player
190 if (mediaEntries.containsKey(packageName)) {
191 return packageName
192 }
193 return null
194 }
195
196 private fun loadMediaData(
197 key: String,
198 sbn: StatusBarNotification,
199 oldKey: String?
200 ) {
201 backgroundExecutor.execute {
202 loadMediaDataInBg(key, sbn, oldKey)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700203 }
204 }
205
Selim Cinekb52642b2020-04-17 14:30:29 -0700206 /**
207 * Add a listener for changes in this class
208 */
209 fun addListener(listener: Listener) = listeners.add(listener)
210
211 /**
212 * Remove a listener for changes in this class
213 */
214 fun removeListener(listener: Listener) = listeners.remove(listener)
215
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700216 private fun setTimedOut(token: String, timedOut: Boolean) {
217 if (!timedOut) {
218 return
219 }
220 mediaEntries[token]?.let {
221 notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */,
222 UNDEFINED_DISMISS_REASON)
223 }
224 }
225
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400226 private fun loadMediaDataInBg(
227 desc: MediaDescription,
228 resumeAction: Runnable,
229 token: MediaSession.Token,
230 appName: String,
231 appIntent: PendingIntent,
232 packageName: String
233 ) {
234 if (resumeAction == null) {
235 Log.e(TAG, "Resume action cannot be null")
236 return
237 }
238
239 if (TextUtils.isEmpty(desc.title)) {
240 Log.e(TAG, "Description incomplete")
241 return
242 }
243
244 Log.d(TAG, "adding track from browser: $desc")
245
246 // Album art
247 var artworkBitmap = desc.iconBitmap
248 if (artworkBitmap == null && desc.iconUri != null) {
249 artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
250 }
251 val artworkIcon = if (artworkBitmap != null) {
252 Icon.createWithBitmap(artworkBitmap)
253 } else {
254 null
255 }
256
257 val mediaAction = getResumeMediaAction(resumeAction)
258 foregroundExecutor.execute {
259 onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName,
260 null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
261 packageName, token, appIntent, null, resumeAction, packageName))
262 }
263 }
264
265 private fun loadMediaDataInBg(
266 key: String,
267 sbn: StatusBarNotification,
268 oldKey: String?
269 ) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700270 val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
271 as MediaSession.Token?
272 val metadata = mediaControllerFactory.create(token).metadata
273
274 if (metadata == null) {
275 // TODO: handle this better, removing media notification
276 return
277 }
278
279 // Foreground and Background colors computed from album art
280 val notif: Notification = sbn.notification
Lucas Dupin96bdbee2020-05-14 15:51:32 -0700281 var bgColor = Color.WHITE
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700282 var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)
283 if (artworkBitmap == null) {
284 artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
285 }
Selim Cinekc5436712020-04-27 15:15:44 -0700286 if (artworkBitmap == null) {
287 artworkBitmap = loadBitmapFromUri(metadata)
288 }
289 val artWorkIcon = if (artworkBitmap == null) {
290 notif.getLargeIcon()
291 } else {
292 Icon.createWithBitmap(artworkBitmap)
293 }
294 if (artWorkIcon != null) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700295 // If we have art, get colors from that
Selim Cinekc5436712020-04-27 15:15:44 -0700296 if (artworkBitmap == null) {
Lucas Dupin03859d72020-05-12 12:27:47 -0700297 if (artWorkIcon.type == Icon.TYPE_BITMAP ||
298 artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) {
Selim Cinekc5436712020-04-27 15:15:44 -0700299 artworkBitmap = artWorkIcon.bitmap
300 } else {
301 val drawable: Drawable = artWorkIcon.loadDrawable(context)
302 artworkBitmap = Bitmap.createBitmap(
303 drawable.intrinsicWidth,
304 drawable.intrinsicHeight,
305 Bitmap.Config.ARGB_8888)
306 val canvas = Canvas(artworkBitmap)
307 drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
308 drawable.draw(canvas)
309 }
310 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700311 val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
312 .generate()
313 val swatch = MediaNotificationProcessor.findBackgroundSwatch(p)
314 bgColor = swatch.rgb
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700315 }
Lucas Dupin96bdbee2020-05-14 15:51:32 -0700316 // Adapt background color, so it's always subdued and text is legible
317 val tmpHsl = floatArrayOf(0f, 0f, 0f)
318 ColorUtils.colorToHSL(bgColor, tmpHsl)
319
320 val l = tmpHsl[2]
321 // Colors with very low luminosity can have any saturation. This means that changing the
322 // luminosity can make a black become red. Let's remove the saturation of very light or
323 // very dark colors to avoid this issue.
324 if (l < LUMINOSITY_THRESHOLD || l > 1f - LUMINOSITY_THRESHOLD) {
325 tmpHsl[1] = 0f
326 }
327 tmpHsl[1] *= SATURATION_MULTIPLIER
328 tmpHsl[2] = DEFAULT_LUMINOSITY
329
330 bgColor = ColorUtils.HSLToColor(tmpHsl)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700331
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700332 // App name
333 val builder = Notification.Builder.recoverBuilder(context, notif)
334 val app = builder.loadHeaderAppName()
335
336 // App Icon
337 val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context)
338
339 // Song name
Selim Cinekc5436712020-04-27 15:15:44 -0700340 var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
341 if (song == null) {
342 song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
343 }
344 if (song == null) {
345 song = HybridGroupManager.resolveTitle(notif)
346 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700347
348 // Artist name
Selim Cinekc5436712020-04-27 15:15:44 -0700349 var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
350 if (artist == null) {
351 artist = HybridGroupManager.resolveText(notif)
352 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700353
354 // Control buttons
355 val actionIcons: MutableList<MediaAction> = ArrayList()
356 val actions = notif.actions
357 val actionsToShowCollapsed = notif.extras.getIntArray(
Beth Thibodeauf75175f2020-05-21 16:52:04 -0400358 Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf<Int>()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700359 // TODO: b/153736623 look into creating actions when this isn't a media style notification
360
361 val packageContext: Context = sbn.getPackageContext(context)
Beth Thibodeau4580c832020-05-15 20:11:44 -0400362 if (actions != null) {
Beth Thibodeauf75175f2020-05-21 16:52:04 -0400363 for ((index, action) in actions.withIndex()) {
364 if (action.getIcon() == null) {
365 Log.i(TAG, "No icon for action $index ${action.title}")
366 actionsToShowCollapsed.remove(index)
367 continue
368 }
Beth Thibodeauf96f4fb2020-06-11 19:26:54 -0400369 val runnable = if (action.actionIntent != null) {
370 Runnable {
371 try {
372 action.actionIntent.send()
373 } catch (e: PendingIntent.CanceledException) {
374 Log.d(TAG, "Intent canceled", e)
375 }
376 }
377 } else {
378 null
379 }
Beth Thibodeau4580c832020-05-15 20:11:44 -0400380 val mediaAction = MediaAction(
381 action.getIcon().loadDrawable(packageContext),
Beth Thibodeauf96f4fb2020-06-11 19:26:54 -0400382 runnable,
Beth Thibodeau4580c832020-05-15 20:11:44 -0400383 action.title)
384 actionIcons.add(mediaAction)
385 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700386 }
387
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400388 val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction
Lucas Dupin5b27cbc2020-05-18 10:46:50 -0700389 foregroundExecutor.execute {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400390 onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
391 song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
392 notif.contentIntent, null, resumeAction, key))
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700393 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700394 }
395
Selim Cinekc5436712020-04-27 15:15:44 -0700396 /**
397 * Load a bitmap from the various Art metadata URIs
398 */
399 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
400 for (uri in ART_URIS) {
401 val uriString = metadata.getString(uri)
402 if (!TextUtils.isEmpty(uriString)) {
403 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
404 if (albumArt != null) {
405 Log.d(TAG, "loaded art from $uri")
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400406 return albumArt
Selim Cinekc5436712020-04-27 15:15:44 -0700407 }
408 }
409 }
410 return null
411 }
412
413 /**
414 * Load a bitmap from a URI
415 * @param uri the uri to load
416 * @return bitmap, or null if couldn't be loaded
417 */
418 private fun loadBitmapFromUri(uri: Uri): Bitmap? {
419 // ImageDecoder requires a scheme of the following types
Lucas Dupin03859d72020-05-12 12:27:47 -0700420 if (uri.scheme == null) {
421 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700422 }
423
Lucas Dupin03859d72020-05-12 12:27:47 -0700424 if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
425 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
426 !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
427 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700428 }
429
430 val source = ImageDecoder.createSource(context.getContentResolver(), uri)
431 return try {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400432 ImageDecoder.decodeBitmap(source) {
433 decoder, info, source -> decoder.isMutableRequired = true
434 }
Selim Cinekc5436712020-04-27 15:15:44 -0700435 } catch (e: IOException) {
436 e.printStackTrace()
437 null
438 }
439 }
440
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400441 private fun getResumeMediaAction(action: Runnable): MediaAction {
442 return MediaAction(
443 context.getDrawable(R.drawable.lb_ic_play),
444 action,
445 context.getString(R.string.controls_media_resume)
446 )
447 }
448
449 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400450 Assert.isMainThread()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700451 if (mediaEntries.containsKey(key)) {
452 // Otherwise this was removed already
453 mediaEntries.put(key, data)
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400454 val listenersCopy = listeners.toSet()
455 listenersCopy.forEach {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400456 it.onMediaDataLoaded(key, oldKey, data)
Selim Cinekb52642b2020-04-17 14:30:29 -0700457 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700458 }
459 }
460
461 fun onNotificationRemoved(key: String) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400462 Assert.isMainThread()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400463 if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) {
464 Log.d(TAG, "Not removing $key because resumable")
465 // Move to resume key aka package name
466 val data = mediaEntries.remove(key)!!
467 val resumeAction = getResumeMediaAction(data.resumeAction!!)
468 val updated = data.copy(token = null, actions = listOf(resumeAction),
469 actionsToShowInCompact = listOf(0))
470 mediaEntries.put(data.packageName, updated)
471 // Notify listeners of "new" controls
472 val listenersCopy = listeners.toSet()
473 listenersCopy.forEach {
474 it.onMediaDataLoaded(data.packageName, key, updated)
475 }
476 return
477 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700478 val removed = mediaEntries.remove(key)
479 if (removed != null) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400480 val listenersCopy = listeners.toSet()
481 listenersCopy.forEach {
Selim Cinekb52642b2020-04-17 14:30:29 -0700482 it.onMediaDataRemoved(key)
483 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700484 }
485 }
486
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700487 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700488 * Are there any media notifications active?
489 */
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400490 fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) })
Selim Cinekb52642b2020-04-17 14:30:29 -0700491
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400492 fun isActive(data: MediaData): Boolean {
493 if (data.token == null) {
494 return false
495 }
496 val controller = mediaControllerFactory.create(data.token)
497 val state = controller?.playbackState?.state
498 return state != null && NotificationMediaManager.isActiveState(state)
Selim Cinekb52642b2020-04-17 14:30:29 -0700499 }
500
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400501 /**
502 * Are there any media entries, including resume controls?
503 */
504 fun hasAnyMedia() = mediaEntries.isNotEmpty()
505
Selim Cinekb52642b2020-04-17 14:30:29 -0700506 interface Listener {
507
508 /**
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400509 * Called whenever there's new MediaData Loaded for the consumption in views.
510 *
511 * oldKey is provided to check whether the view has changed keys, which can happen when a
512 * player has gone from resume state (key is package name) to active state (key is
513 * notification key) or vice versa.
Selim Cinekb52642b2020-04-17 14:30:29 -0700514 */
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400515 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
Selim Cinekb52642b2020-04-17 14:30:29 -0700516
517 /**
518 * Called whenever a previously existing Media notification was removed
519 */
520 fun onMediaDataRemoved(key: String) {}
521 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700522}