blob: 5e65a8f36bbcf51ec72d35281dd54edf4ba5488e [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,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070070 emptyList(), emptyList(), "INVALID", null, null, null, true, 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 notificationEntryManager: NotificationEntryManager,
Selim Cinek5dbef2d2020-05-07 17:44:38 -070092 @Background private val backgroundExecutor: Executor,
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040093 @Main private val foregroundExecutor: Executor,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070094 broadcastDispatcher: BroadcastDispatcher,
95 mediaTimeoutListener: MediaTimeoutListener,
96 mediaResumeListener: MediaResumeListener
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
Beth Thibodeau7fee1962020-06-11 23:19:01 -0400112 private val appChangeReceiver = object : BroadcastReceiver() {
113 override fun onReceive(context: Context, intent: Intent) {
114 when (intent.action) {
115 Intent.ACTION_PACKAGES_SUSPENDED -> {
116 val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
117 packages?.forEach {
118 removeAllForPackage(it)
119 }
120 }
121 Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> {
122 intent.data?.encodedSchemeSpecificPart?.let {
123 removeAllForPackage(it)
124 }
125 }
126 }
127 }
128 }
129
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700130 init {
131 mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
132 setTimedOut(token, timedOut) }
133 addListener(mediaTimeoutListener)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400134 if (useMediaResumption) {
135 mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
136 resumeAction: Runnable, token: MediaSession.Token, appName: String,
137 appIntent: PendingIntent, packageName: String ->
138 addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName)
139 }
140 mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? ->
141 mediaEntries.get(key)?.resumeAction = action
142 mediaEntries.get(key)?.hasCheckedForResume = true
143 }
144 addListener(mediaResumeListener)
145 }
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400146
147 val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED)
148 broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL)
Beth Thibodeau7fee1962020-06-11 23:19:01 -0400149
150 val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
151 broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
152
153 val uninstallFilter = IntentFilter().apply {
154 addAction(Intent.ACTION_PACKAGE_REMOVED)
155 addAction(Intent.ACTION_PACKAGE_RESTARTED)
156 addDataScheme("package")
157 }
158 // BroadcastDispatcher does not allow filters with data schemes
159 context.registerReceiver(appChangeReceiver, uninstallFilter)
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700160 }
161
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700162 fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
Robert Snoeberger70d0d6b2020-05-14 16:47:02 -0400163 if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400164 Assert.isMainThread()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400165 val oldKey = findExistingEntry(key, sbn.packageName)
166 if (oldKey == null) {
167 val temp = LOADING.copy(packageName = sbn.packageName)
168 mediaEntries.put(key, temp)
169 } else if (oldKey != key) {
170 // Move to new key
171 val oldData = mediaEntries.remove(oldKey)!!
172 mediaEntries.put(key, oldData)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700173 }
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400174 loadMediaData(key, sbn, oldKey)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700175 } else {
176 onNotificationRemoved(key)
177 }
178 }
179
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400180 private fun clearData() {
181 // Called on user change. Remove all current MediaData objects and inform listeners
182 val listenersCopy = listeners.toSet()
183 mediaEntries.forEach {
184 listenersCopy.forEach { listener ->
185 listener.onMediaDataRemoved(it.key)
186 }
187 }
188 mediaEntries.clear()
189 }
190
Beth Thibodeau7fee1962020-06-11 23:19:01 -0400191 private fun removeAllForPackage(packageName: String) {
192 Assert.isMainThread()
193 val listenersCopy = listeners.toSet()
194 val toRemove = mediaEntries.filter { it.value.packageName == packageName }
195 toRemove.forEach {
196 mediaEntries.remove(it.key)
197 listenersCopy.forEach { listener ->
198 listener.onMediaDataRemoved(it.key)
199 }
200 }
201 }
202
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400203 private fun addResumptionControls(
204 desc: MediaDescription,
205 action: Runnable,
206 token: MediaSession.Token,
207 appName: String,
208 appIntent: PendingIntent,
209 packageName: String
210 ) {
211 // Resume controls don't have a notification key, so store by package name instead
212 if (!mediaEntries.containsKey(packageName)) {
213 val resumeData = LOADING.copy(packageName = packageName, resumeAction = action)
214 mediaEntries.put(packageName, resumeData)
215 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700216 backgroundExecutor.execute {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700217 loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400218 }
219 }
220
221 /**
222 * Check if there is an existing entry that matches the key or package name.
223 * Returns the key that matches, or null if not found.
224 */
225 private fun findExistingEntry(key: String, packageName: String): String? {
226 if (mediaEntries.containsKey(key)) {
227 return key
228 }
229 // Check if we already had a resume player
230 if (mediaEntries.containsKey(packageName)) {
231 return packageName
232 }
233 return null
234 }
235
236 private fun loadMediaData(
237 key: String,
238 sbn: StatusBarNotification,
239 oldKey: String?
240 ) {
241 backgroundExecutor.execute {
242 loadMediaDataInBg(key, sbn, oldKey)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700243 }
244 }
245
Selim Cinekb52642b2020-04-17 14:30:29 -0700246 /**
247 * Add a listener for changes in this class
248 */
249 fun addListener(listener: Listener) = listeners.add(listener)
250
251 /**
252 * Remove a listener for changes in this class
253 */
254 fun removeListener(listener: Listener) = listeners.remove(listener)
255
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700256 private fun setTimedOut(token: String, timedOut: Boolean) {
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700257 mediaEntries[token]?.let {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700258 if (Utils.useMediaResumption(context)) {
259 if (it.active == !timedOut) {
260 return
261 }
262 it.active = !timedOut
263 onMediaDataLoaded(token, token, it)
Lucas Dupina73d43f2020-06-16 16:44:53 -0700264 } else if (timedOut) {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700265 notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */,
266 UNDEFINED_DISMISS_REASON)
267 }
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700268 }
269 }
270
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700271 private fun loadMediaDataInBgForResumption(
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400272 desc: MediaDescription,
273 resumeAction: Runnable,
274 token: MediaSession.Token,
275 appName: String,
276 appIntent: PendingIntent,
277 packageName: String
278 ) {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400279 if (TextUtils.isEmpty(desc.title)) {
280 Log.e(TAG, "Description incomplete")
281 return
282 }
283
284 Log.d(TAG, "adding track from browser: $desc")
285
286 // Album art
287 var artworkBitmap = desc.iconBitmap
288 if (artworkBitmap == null && desc.iconUri != null) {
289 artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
290 }
291 val artworkIcon = if (artworkBitmap != null) {
292 Icon.createWithBitmap(artworkBitmap)
293 } else {
294 null
295 }
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400296 val bgColor = artworkBitmap?.let { computeBackgroundColor(it) } ?: Color.DKGRAY
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400297
298 val mediaAction = getResumeMediaAction(resumeAction)
299 foregroundExecutor.execute {
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400300 onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700301 null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
302 packageName, token, appIntent, device = null, active = false,
303 resumeAction = resumeAction))
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400304 }
305 }
306
307 private fun loadMediaDataInBg(
308 key: String,
309 sbn: StatusBarNotification,
310 oldKey: String?
311 ) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700312 val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
313 as MediaSession.Token?
314 val metadata = mediaControllerFactory.create(token).metadata
315
316 if (metadata == null) {
317 // TODO: handle this better, removing media notification
318 return
319 }
320
321 // Foreground and Background colors computed from album art
322 val notif: Notification = sbn.notification
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700323 var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)
324 if (artworkBitmap == null) {
325 artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
326 }
Selim Cinekc5436712020-04-27 15:15:44 -0700327 if (artworkBitmap == null) {
328 artworkBitmap = loadBitmapFromUri(metadata)
329 }
330 val artWorkIcon = if (artworkBitmap == null) {
331 notif.getLargeIcon()
332 } else {
333 Icon.createWithBitmap(artworkBitmap)
334 }
335 if (artWorkIcon != null) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700336 // If we have art, get colors from that
Selim Cinekc5436712020-04-27 15:15:44 -0700337 if (artworkBitmap == null) {
Lucas Dupin03859d72020-05-12 12:27:47 -0700338 if (artWorkIcon.type == Icon.TYPE_BITMAP ||
339 artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) {
Selim Cinekc5436712020-04-27 15:15:44 -0700340 artworkBitmap = artWorkIcon.bitmap
341 } else {
342 val drawable: Drawable = artWorkIcon.loadDrawable(context)
343 artworkBitmap = Bitmap.createBitmap(
344 drawable.intrinsicWidth,
345 drawable.intrinsicHeight,
346 Bitmap.Config.ARGB_8888)
347 val canvas = Canvas(artworkBitmap)
348 drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
349 drawable.draw(canvas)
350 }
351 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700352 }
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400353 val bgColor = computeBackgroundColor(artworkBitmap)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700354
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700355 // App name
356 val builder = Notification.Builder.recoverBuilder(context, notif)
357 val app = builder.loadHeaderAppName()
358
359 // App Icon
360 val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context)
361
362 // Song name
Selim Cinekc5436712020-04-27 15:15:44 -0700363 var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
364 if (song == null) {
365 song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
366 }
367 if (song == null) {
368 song = HybridGroupManager.resolveTitle(notif)
369 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700370
371 // Artist name
Selim Cinekc5436712020-04-27 15:15:44 -0700372 var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
373 if (artist == null) {
374 artist = HybridGroupManager.resolveText(notif)
375 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700376
377 // Control buttons
378 val actionIcons: MutableList<MediaAction> = ArrayList()
379 val actions = notif.actions
380 val actionsToShowCollapsed = notif.extras.getIntArray(
Beth Thibodeauf75175f2020-05-21 16:52:04 -0400381 Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf<Int>()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700382 // TODO: b/153736623 look into creating actions when this isn't a media style notification
383
384 val packageContext: Context = sbn.getPackageContext(context)
Beth Thibodeau4580c832020-05-15 20:11:44 -0400385 if (actions != null) {
Beth Thibodeauf75175f2020-05-21 16:52:04 -0400386 for ((index, action) in actions.withIndex()) {
387 if (action.getIcon() == null) {
388 Log.i(TAG, "No icon for action $index ${action.title}")
389 actionsToShowCollapsed.remove(index)
390 continue
391 }
Beth Thibodeauf96f4fb2020-06-11 19:26:54 -0400392 val runnable = if (action.actionIntent != null) {
393 Runnable {
394 try {
395 action.actionIntent.send()
396 } catch (e: PendingIntent.CanceledException) {
397 Log.d(TAG, "Intent canceled", e)
398 }
399 }
400 } else {
401 null
402 }
Beth Thibodeau4580c832020-05-15 20:11:44 -0400403 val mediaAction = MediaAction(
404 action.getIcon().loadDrawable(packageContext),
Beth Thibodeauf96f4fb2020-06-11 19:26:54 -0400405 runnable,
Beth Thibodeau4580c832020-05-15 20:11:44 -0400406 action.title)
407 actionIcons.add(mediaAction)
408 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700409 }
410
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400411 val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction
Lucas Dupin5b27cbc2020-05-18 10:46:50 -0700412 foregroundExecutor.execute {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400413 onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
414 song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700415 notif.contentIntent, null, active = true, resumeAction = resumeAction,
416 notificationKey = key))
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700417 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700418 }
419
Selim Cinekc5436712020-04-27 15:15:44 -0700420 /**
421 * Load a bitmap from the various Art metadata URIs
422 */
423 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
424 for (uri in ART_URIS) {
425 val uriString = metadata.getString(uri)
426 if (!TextUtils.isEmpty(uriString)) {
427 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
428 if (albumArt != null) {
429 Log.d(TAG, "loaded art from $uri")
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400430 return albumArt
Selim Cinekc5436712020-04-27 15:15:44 -0700431 }
432 }
433 }
434 return null
435 }
436
437 /**
438 * Load a bitmap from a URI
439 * @param uri the uri to load
440 * @return bitmap, or null if couldn't be loaded
441 */
442 private fun loadBitmapFromUri(uri: Uri): Bitmap? {
443 // ImageDecoder requires a scheme of the following types
Lucas Dupin03859d72020-05-12 12:27:47 -0700444 if (uri.scheme == null) {
445 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700446 }
447
Lucas Dupin03859d72020-05-12 12:27:47 -0700448 if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
449 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
450 !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
451 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700452 }
453
454 val source = ImageDecoder.createSource(context.getContentResolver(), uri)
455 return try {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400456 ImageDecoder.decodeBitmap(source) {
457 decoder, info, source -> decoder.isMutableRequired = true
458 }
Selim Cinekc5436712020-04-27 15:15:44 -0700459 } catch (e: IOException) {
460 e.printStackTrace()
461 null
462 }
463 }
464
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400465 private fun computeBackgroundColor(artworkBitmap: Bitmap?): Int {
466 var color = Color.WHITE
467 if (artworkBitmap != null) {
468 // If we have art, get colors from that
469 val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
470 .generate()
471 val swatch = MediaNotificationProcessor.findBackgroundSwatch(p)
472 color = swatch.rgb
473 }
474 // Adapt background color, so it's always subdued and text is legible
475 val tmpHsl = floatArrayOf(0f, 0f, 0f)
476 ColorUtils.colorToHSL(color, tmpHsl)
477
478 val l = tmpHsl[2]
479 // Colors with very low luminosity can have any saturation. This means that changing the
480 // luminosity can make a black become red. Let's remove the saturation of very light or
481 // very dark colors to avoid this issue.
482 if (l < LUMINOSITY_THRESHOLD || l > 1f - LUMINOSITY_THRESHOLD) {
483 tmpHsl[1] = 0f
484 }
485 tmpHsl[1] *= SATURATION_MULTIPLIER
486 tmpHsl[2] = DEFAULT_LUMINOSITY
487
488 color = ColorUtils.HSLToColor(tmpHsl)
489 return color
490 }
491
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400492 private fun getResumeMediaAction(action: Runnable): MediaAction {
493 return MediaAction(
494 context.getDrawable(R.drawable.lb_ic_play),
495 action,
496 context.getString(R.string.controls_media_resume)
497 )
498 }
499
500 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400501 Assert.isMainThread()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700502 if (mediaEntries.containsKey(key)) {
503 // Otherwise this was removed already
504 mediaEntries.put(key, data)
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400505 val listenersCopy = listeners.toSet()
506 listenersCopy.forEach {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400507 it.onMediaDataLoaded(key, oldKey, data)
Selim Cinekb52642b2020-04-17 14:30:29 -0700508 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700509 }
510 }
511
512 fun onNotificationRemoved(key: String) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400513 Assert.isMainThread()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400514 if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) {
515 Log.d(TAG, "Not removing $key because resumable")
516 // Move to resume key aka package name
517 val data = mediaEntries.remove(key)!!
518 val resumeAction = getResumeMediaAction(data.resumeAction!!)
519 val updated = data.copy(token = null, actions = listOf(resumeAction),
520 actionsToShowInCompact = listOf(0))
521 mediaEntries.put(data.packageName, updated)
522 // Notify listeners of "new" controls
523 val listenersCopy = listeners.toSet()
524 listenersCopy.forEach {
525 it.onMediaDataLoaded(data.packageName, key, updated)
526 }
527 return
528 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700529 val removed = mediaEntries.remove(key)
530 if (removed != null) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400531 val listenersCopy = listeners.toSet()
532 listenersCopy.forEach {
Selim Cinekb52642b2020-04-17 14:30:29 -0700533 it.onMediaDataRemoved(key)
534 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700535 }
536 }
537
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700538 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700539 * Are there any media notifications active?
540 */
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700541 fun hasActiveMedia() = mediaEntries.any { it.value.active }
Selim Cinekb52642b2020-04-17 14:30:29 -0700542
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700543 fun isActive(token: MediaSession.Token?): Boolean {
544 if (token == null) {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400545 return false
546 }
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700547 val controller = mediaControllerFactory.create(token)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400548 val state = controller?.playbackState?.state
549 return state != null && NotificationMediaManager.isActiveState(state)
Selim Cinekb52642b2020-04-17 14:30:29 -0700550 }
551
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400552 /**
553 * Are there any media entries, including resume controls?
554 */
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700555 fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400556
Selim Cinekb52642b2020-04-17 14:30:29 -0700557 interface Listener {
558
559 /**
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400560 * Called whenever there's new MediaData Loaded for the consumption in views.
561 *
562 * oldKey is provided to check whether the view has changed keys, which can happen when a
563 * player has gone from resume state (key is package name) to active state (key is
564 * notification key) or vice versa.
Selim Cinekb52642b2020-04-17 14:30:29 -0700565 */
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400566 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
Selim Cinekb52642b2020-04-17 14:30:29 -0700567
568 /**
569 * Called whenever a previously existing Media notification was removed
570 */
571 fun onMediaDataRemoved(key: String) {}
572 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700573}