blob: 5052386e65e1a43d16d3f03c355bc446d510fd95 [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
Lucas Dupinae437a12020-06-17 17:20:33 -070041import com.android.systemui.Dumpable
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040042import com.android.systemui.R
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040043import com.android.systemui.broadcast.BroadcastDispatcher
Selim Cinek5dbef2d2020-05-07 17:44:38 -070044import com.android.systemui.dagger.qualifiers.Background
45import com.android.systemui.dagger.qualifiers.Main
Lucas Dupinae437a12020-06-17 17:20:33 -070046import com.android.systemui.dump.DumpManager
Selim Cinek5dbef2d2020-05-07 17:44:38 -070047import com.android.systemui.statusbar.notification.MediaNotificationProcessor
Selim Cinekc5436712020-04-27 15:15:44 -070048import com.android.systemui.statusbar.notification.row.HybridGroupManager
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -040049import com.android.systemui.util.Assert
Lucas Dupin03859d72020-05-12 12:27:47 -070050import com.android.systemui.util.Utils
Lucas Dupinae437a12020-06-17 17:20:33 -070051import java.io.FileDescriptor
Selim Cinekc5436712020-04-27 15:15:44 -070052import java.io.IOException
Lucas Dupinae437a12020-06-17 17:20:33 -070053import java.io.PrintWriter
Selim Cinek5dbef2d2020-05-07 17:44:38 -070054import java.util.concurrent.Executor
55import javax.inject.Inject
56import javax.inject.Singleton
Selim Cinek5dbef2d2020-05-07 17:44:38 -070057
Selim Cinekc5436712020-04-27 15:15:44 -070058// URI fields to try loading album art from
59private val ART_URIS = arrayOf(
60 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
61 MediaMetadata.METADATA_KEY_ART_URI,
62 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
63)
64
65private const val TAG = "MediaDataManager"
Lucas Dupin96bdbee2020-05-14 15:51:32 -070066private const val DEFAULT_LUMINOSITY = 0.25f
67private const val LUMINOSITY_THRESHOLD = 0.05f
68private const val SATURATION_MULTIPLIER = 0.8f
Selim Cinekc5436712020-04-27 15:15:44 -070069
Beth Thibodeau4580c832020-05-15 20:11:44 -040070private val LOADING = MediaData(false, 0, null, null, null, null, null,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070071 emptyList(), emptyList(), "INVALID", null, null, null, true, null)
Robert Snoeberger70d0d6b2020-05-14 16:47:02 -040072
73fun isMediaNotification(sbn: StatusBarNotification): Boolean {
74 if (!sbn.notification.hasMediaSession()) {
75 return false
76 }
77 val notificationStyle = sbn.notification.notificationStyle
78 if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) ||
79 Notification.MediaStyle::class.java.equals(notificationStyle)) {
80 return true
81 }
82 return false
83}
Selim Cinekc5436712020-04-27 15:15:44 -070084
Selim Cinek5dbef2d2020-05-07 17:44:38 -070085/**
86 * A class that facilitates management and loading of Media Data, ready for binding.
87 */
88@Singleton
Lucas Dupinae437a12020-06-17 17:20:33 -070089class MediaDataManager(
Selim Cinek5dbef2d2020-05-07 17:44:38 -070090 private val context: Context,
Selim Cinek5dbef2d2020-05-07 17:44:38 -070091 @Background private val backgroundExecutor: Executor,
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -040092 @Main private val foregroundExecutor: Executor,
Lucas Dupinae437a12020-06-17 17:20:33 -070093 private val mediaControllerFactory: MediaControllerFactory,
94 private val broadcastDispatcher: BroadcastDispatcher,
95 dumpManager: DumpManager,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070096 mediaTimeoutListener: MediaTimeoutListener,
Lucas Dupinae437a12020-06-17 17:20:33 -070097 mediaResumeListener: MediaResumeListener,
Beth Thibodeauc1bc3072020-06-09 23:36:17 -040098 private var useMediaResumption: Boolean,
Lucas Dupinae437a12020-06-17 17:20:33 -070099 private val useQsMediaPlayer: Boolean
100) : Dumpable {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700101
Selim Cinekb52642b2020-04-17 14:30:29 -0700102 private val listeners: MutableSet<Listener> = mutableSetOf()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700103 private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
Lucas Dupinae437a12020-06-17 17:20:33 -0700104
105 @Inject
106 constructor(
107 context: Context,
108 @Background backgroundExecutor: Executor,
109 @Main foregroundExecutor: Executor,
110 mediaControllerFactory: MediaControllerFactory,
111 dumpManager: DumpManager,
112 broadcastDispatcher: BroadcastDispatcher,
113 mediaTimeoutListener: MediaTimeoutListener,
114 mediaResumeListener: MediaResumeListener
115 ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
116 broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
117 Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700118
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400119 private val userChangeReceiver = object : BroadcastReceiver() {
120 override fun onReceive(context: Context, intent: Intent) {
121 if (Intent.ACTION_USER_SWITCHED == intent.action) {
122 // Remove all controls, regardless of state
123 clearData()
124 }
125 }
126 }
127
Beth Thibodeau7fee1962020-06-11 23:19:01 -0400128 private val appChangeReceiver = object : BroadcastReceiver() {
129 override fun onReceive(context: Context, intent: Intent) {
130 when (intent.action) {
131 Intent.ACTION_PACKAGES_SUSPENDED -> {
132 val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
133 packages?.forEach {
134 removeAllForPackage(it)
135 }
136 }
137 Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> {
138 intent.data?.encodedSchemeSpecificPart?.let {
139 removeAllForPackage(it)
140 }
141 }
142 }
143 }
144 }
145
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700146 init {
Lucas Dupinae437a12020-06-17 17:20:33 -0700147 dumpManager.registerDumpable(TAG, this)
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700148 mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
149 setTimedOut(token, timedOut) }
150 addListener(mediaTimeoutListener)
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400151
152 mediaResumeListener.setManager(this)
153 addListener(mediaResumeListener)
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400154
155 val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED)
156 broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL)
Beth Thibodeau7fee1962020-06-11 23:19:01 -0400157
158 val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
159 broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
160
161 val uninstallFilter = IntentFilter().apply {
162 addAction(Intent.ACTION_PACKAGE_REMOVED)
163 addAction(Intent.ACTION_PACKAGE_RESTARTED)
164 addDataScheme("package")
165 }
166 // BroadcastDispatcher does not allow filters with data schemes
167 context.registerReceiver(appChangeReceiver, uninstallFilter)
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700168 }
169
Lucas Dupinae437a12020-06-17 17:20:33 -0700170 fun destroy() {
171 context.unregisterReceiver(appChangeReceiver)
172 broadcastDispatcher.unregisterReceiver(userChangeReceiver)
173 }
174
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700175 fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
Lucas Dupinae437a12020-06-17 17:20:33 -0700176 if (useQsMediaPlayer && isMediaNotification(sbn)) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400177 Assert.isMainThread()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400178 val oldKey = findExistingEntry(key, sbn.packageName)
179 if (oldKey == null) {
180 val temp = LOADING.copy(packageName = sbn.packageName)
181 mediaEntries.put(key, temp)
182 } else if (oldKey != key) {
183 // Move to new key
184 val oldData = mediaEntries.remove(oldKey)!!
185 mediaEntries.put(key, oldData)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700186 }
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400187 loadMediaData(key, sbn, oldKey)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700188 } else {
189 onNotificationRemoved(key)
190 }
191 }
192
Beth Thibodeau6a4fbe32020-06-09 01:04:41 -0400193 private fun clearData() {
194 // Called on user change. Remove all current MediaData objects and inform listeners
195 val listenersCopy = listeners.toSet()
196 mediaEntries.forEach {
197 listenersCopy.forEach { listener ->
198 listener.onMediaDataRemoved(it.key)
199 }
200 }
201 mediaEntries.clear()
202 }
203
Beth Thibodeau7fee1962020-06-11 23:19:01 -0400204 private fun removeAllForPackage(packageName: String) {
205 Assert.isMainThread()
206 val listenersCopy = listeners.toSet()
207 val toRemove = mediaEntries.filter { it.value.packageName == packageName }
208 toRemove.forEach {
209 mediaEntries.remove(it.key)
210 listenersCopy.forEach { listener ->
211 listener.onMediaDataRemoved(it.key)
212 }
213 }
214 }
215
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400216 fun setResumeAction(key: String, action: Runnable?) {
217 mediaEntries.get(key)?.let {
218 it.resumeAction = action
219 it.hasCheckedForResume = true
220 }
221 }
222
223 fun addResumptionControls(
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400224 desc: MediaDescription,
225 action: Runnable,
226 token: MediaSession.Token,
227 appName: String,
228 appIntent: PendingIntent,
229 packageName: String
230 ) {
231 // Resume controls don't have a notification key, so store by package name instead
232 if (!mediaEntries.containsKey(packageName)) {
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400233 val resumeData = LOADING.copy(packageName = packageName, resumeAction = action,
234 hasCheckedForResume = true)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400235 mediaEntries.put(packageName, resumeData)
236 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700237 backgroundExecutor.execute {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700238 loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400239 }
240 }
241
242 /**
243 * Check if there is an existing entry that matches the key or package name.
244 * Returns the key that matches, or null if not found.
245 */
246 private fun findExistingEntry(key: String, packageName: String): String? {
247 if (mediaEntries.containsKey(key)) {
248 return key
249 }
250 // Check if we already had a resume player
251 if (mediaEntries.containsKey(packageName)) {
252 return packageName
253 }
254 return null
255 }
256
257 private fun loadMediaData(
258 key: String,
259 sbn: StatusBarNotification,
260 oldKey: String?
261 ) {
262 backgroundExecutor.execute {
263 loadMediaDataInBg(key, sbn, oldKey)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700264 }
265 }
266
Selim Cinekb52642b2020-04-17 14:30:29 -0700267 /**
268 * Add a listener for changes in this class
269 */
270 fun addListener(listener: Listener) = listeners.add(listener)
271
272 /**
273 * Remove a listener for changes in this class
274 */
275 fun removeListener(listener: Listener) = listeners.remove(listener)
276
Lucas Dupinae437a12020-06-17 17:20:33 -0700277 /**
278 * Called whenever the player has been paused or stopped for a while.
279 * This will make the player not active anymore, hiding it from QQS and Keyguard.
280 * @see MediaData.active
281 */
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700282 private fun setTimedOut(token: String, timedOut: Boolean) {
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700283 mediaEntries[token]?.let {
Lucas Dupinae437a12020-06-17 17:20:33 -0700284 if (it.active == !timedOut) {
285 return
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700286 }
Lucas Dupinae437a12020-06-17 17:20:33 -0700287 it.active = !timedOut
288 onMediaDataLoaded(token, token, it)
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700289 }
290 }
291
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700292 private fun loadMediaDataInBgForResumption(
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400293 desc: MediaDescription,
294 resumeAction: Runnable,
295 token: MediaSession.Token,
296 appName: String,
297 appIntent: PendingIntent,
298 packageName: String
299 ) {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400300 if (TextUtils.isEmpty(desc.title)) {
301 Log.e(TAG, "Description incomplete")
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400302 // Delete the placeholder entry
303 mediaEntries.remove(packageName)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400304 return
305 }
306
307 Log.d(TAG, "adding track from browser: $desc")
308
309 // Album art
310 var artworkBitmap = desc.iconBitmap
311 if (artworkBitmap == null && desc.iconUri != null) {
312 artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
313 }
314 val artworkIcon = if (artworkBitmap != null) {
315 Icon.createWithBitmap(artworkBitmap)
316 } else {
317 null
318 }
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400319 val bgColor = artworkBitmap?.let { computeBackgroundColor(it) } ?: Color.DKGRAY
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400320
321 val mediaAction = getResumeMediaAction(resumeAction)
322 foregroundExecutor.execute {
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400323 onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700324 null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
325 packageName, token, appIntent, device = null, active = false,
Robert Snoebergerf05aa68f2020-06-18 16:14:29 -0400326 resumeAction = resumeAction, resumption = true, notificationKey = packageName,
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400327 hasCheckedForResume = true))
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400328 }
329 }
330
331 private fun loadMediaDataInBg(
332 key: String,
333 sbn: StatusBarNotification,
334 oldKey: String?
335 ) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700336 val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
337 as MediaSession.Token?
338 val metadata = mediaControllerFactory.create(token).metadata
339
340 if (metadata == null) {
341 // TODO: handle this better, removing media notification
342 return
343 }
344
345 // Foreground and Background colors computed from album art
346 val notif: Notification = sbn.notification
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700347 var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)
348 if (artworkBitmap == null) {
349 artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
350 }
Selim Cinekc5436712020-04-27 15:15:44 -0700351 if (artworkBitmap == null) {
352 artworkBitmap = loadBitmapFromUri(metadata)
353 }
354 val artWorkIcon = if (artworkBitmap == null) {
355 notif.getLargeIcon()
356 } else {
357 Icon.createWithBitmap(artworkBitmap)
358 }
359 if (artWorkIcon != null) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700360 // If we have art, get colors from that
Selim Cinekc5436712020-04-27 15:15:44 -0700361 if (artworkBitmap == null) {
Lucas Dupin03859d72020-05-12 12:27:47 -0700362 if (artWorkIcon.type == Icon.TYPE_BITMAP ||
363 artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) {
Selim Cinekc5436712020-04-27 15:15:44 -0700364 artworkBitmap = artWorkIcon.bitmap
365 } else {
366 val drawable: Drawable = artWorkIcon.loadDrawable(context)
367 artworkBitmap = Bitmap.createBitmap(
368 drawable.intrinsicWidth,
369 drawable.intrinsicHeight,
370 Bitmap.Config.ARGB_8888)
371 val canvas = Canvas(artworkBitmap)
372 drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
373 drawable.draw(canvas)
374 }
375 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700376 }
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400377 val bgColor = computeBackgroundColor(artworkBitmap)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700378
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700379 // App name
380 val builder = Notification.Builder.recoverBuilder(context, notif)
381 val app = builder.loadHeaderAppName()
382
383 // App Icon
384 val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context)
385
386 // Song name
Selim Cinekc5436712020-04-27 15:15:44 -0700387 var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
388 if (song == null) {
389 song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
390 }
391 if (song == null) {
392 song = HybridGroupManager.resolveTitle(notif)
393 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700394
395 // Artist name
Selim Cinekc5436712020-04-27 15:15:44 -0700396 var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
397 if (artist == null) {
398 artist = HybridGroupManager.resolveText(notif)
399 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700400
401 // Control buttons
402 val actionIcons: MutableList<MediaAction> = ArrayList()
403 val actions = notif.actions
404 val actionsToShowCollapsed = notif.extras.getIntArray(
Beth Thibodeauf75175f2020-05-21 16:52:04 -0400405 Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf<Int>()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700406 // TODO: b/153736623 look into creating actions when this isn't a media style notification
407
408 val packageContext: Context = sbn.getPackageContext(context)
Beth Thibodeau4580c832020-05-15 20:11:44 -0400409 if (actions != null) {
Beth Thibodeauf75175f2020-05-21 16:52:04 -0400410 for ((index, action) in actions.withIndex()) {
411 if (action.getIcon() == null) {
412 Log.i(TAG, "No icon for action $index ${action.title}")
413 actionsToShowCollapsed.remove(index)
414 continue
415 }
Beth Thibodeauf96f4fb2020-06-11 19:26:54 -0400416 val runnable = if (action.actionIntent != null) {
417 Runnable {
418 try {
419 action.actionIntent.send()
420 } catch (e: PendingIntent.CanceledException) {
421 Log.d(TAG, "Intent canceled", e)
422 }
423 }
424 } else {
425 null
426 }
Beth Thibodeau4580c832020-05-15 20:11:44 -0400427 val mediaAction = MediaAction(
428 action.getIcon().loadDrawable(packageContext),
Beth Thibodeauf96f4fb2020-06-11 19:26:54 -0400429 runnable,
Beth Thibodeau4580c832020-05-15 20:11:44 -0400430 action.title)
431 actionIcons.add(mediaAction)
432 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700433 }
434
Lucas Dupin5b27cbc2020-05-18 10:46:50 -0700435 foregroundExecutor.execute {
Lucas Dupine267f4d2020-06-22 22:55:23 -0700436 val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
437 val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
438 val active = mediaEntries[key]?.active ?: true
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400439 onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
440 song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
Lucas Dupine267f4d2020-06-22 22:55:23 -0700441 notif.contentIntent, null, active, resumeAction = resumeAction,
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400442 notificationKey = key, hasCheckedForResume = hasCheckedForResume))
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700443 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700444 }
445
Selim Cinekc5436712020-04-27 15:15:44 -0700446 /**
447 * Load a bitmap from the various Art metadata URIs
448 */
449 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
450 for (uri in ART_URIS) {
451 val uriString = metadata.getString(uri)
452 if (!TextUtils.isEmpty(uriString)) {
453 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
454 if (albumArt != null) {
455 Log.d(TAG, "loaded art from $uri")
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400456 return albumArt
Selim Cinekc5436712020-04-27 15:15:44 -0700457 }
458 }
459 }
460 return null
461 }
462
463 /**
464 * Load a bitmap from a URI
465 * @param uri the uri to load
466 * @return bitmap, or null if couldn't be loaded
467 */
468 private fun loadBitmapFromUri(uri: Uri): Bitmap? {
469 // ImageDecoder requires a scheme of the following types
Lucas Dupin03859d72020-05-12 12:27:47 -0700470 if (uri.scheme == null) {
471 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700472 }
473
Lucas Dupin03859d72020-05-12 12:27:47 -0700474 if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
475 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
476 !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
477 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700478 }
479
480 val source = ImageDecoder.createSource(context.getContentResolver(), uri)
481 return try {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400482 ImageDecoder.decodeBitmap(source) {
483 decoder, info, source -> decoder.isMutableRequired = true
484 }
Selim Cinekc5436712020-04-27 15:15:44 -0700485 } catch (e: IOException) {
486 e.printStackTrace()
487 null
488 }
489 }
490
Robert Snoebergerc57e0692020-06-18 14:54:26 -0400491 private fun computeBackgroundColor(artworkBitmap: Bitmap?): Int {
492 var color = Color.WHITE
493 if (artworkBitmap != null) {
494 // If we have art, get colors from that
495 val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
496 .generate()
497 val swatch = MediaNotificationProcessor.findBackgroundSwatch(p)
498 color = swatch.rgb
499 }
500 // Adapt background color, so it's always subdued and text is legible
501 val tmpHsl = floatArrayOf(0f, 0f, 0f)
502 ColorUtils.colorToHSL(color, tmpHsl)
503
504 val l = tmpHsl[2]
505 // Colors with very low luminosity can have any saturation. This means that changing the
506 // luminosity can make a black become red. Let's remove the saturation of very light or
507 // very dark colors to avoid this issue.
508 if (l < LUMINOSITY_THRESHOLD || l > 1f - LUMINOSITY_THRESHOLD) {
509 tmpHsl[1] = 0f
510 }
511 tmpHsl[1] *= SATURATION_MULTIPLIER
512 tmpHsl[2] = DEFAULT_LUMINOSITY
513
514 color = ColorUtils.HSLToColor(tmpHsl)
515 return color
516 }
517
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400518 private fun getResumeMediaAction(action: Runnable): MediaAction {
519 return MediaAction(
520 context.getDrawable(R.drawable.lb_ic_play),
521 action,
522 context.getString(R.string.controls_media_resume)
523 )
524 }
525
526 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400527 Assert.isMainThread()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700528 if (mediaEntries.containsKey(key)) {
529 // Otherwise this was removed already
530 mediaEntries.put(key, data)
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400531 val listenersCopy = listeners.toSet()
532 listenersCopy.forEach {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400533 it.onMediaDataLoaded(key, oldKey, data)
Selim Cinekb52642b2020-04-17 14:30:29 -0700534 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700535 }
536 }
537
538 fun onNotificationRemoved(key: String) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400539 Assert.isMainThread()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400540 if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) {
541 Log.d(TAG, "Not removing $key because resumable")
542 // Move to resume key aka package name
543 val data = mediaEntries.remove(key)!!
544 val resumeAction = getResumeMediaAction(data.resumeAction!!)
545 val updated = data.copy(token = null, actions = listOf(resumeAction),
Robert Snoebergerf05aa68f2020-06-18 16:14:29 -0400546 actionsToShowInCompact = listOf(0), active = false, resumption = true)
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400547 mediaEntries.put(data.packageName, updated)
548 // Notify listeners of "new" controls
549 val listenersCopy = listeners.toSet()
550 listenersCopy.forEach {
551 it.onMediaDataLoaded(data.packageName, key, updated)
552 }
553 return
554 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700555 val removed = mediaEntries.remove(key)
556 if (removed != null) {
Beth Thibodeau8a2af3b32020-05-26 19:57:42 -0400557 val listenersCopy = listeners.toSet()
558 listenersCopy.forEach {
Selim Cinekb52642b2020-04-17 14:30:29 -0700559 it.onMediaDataRemoved(key)
560 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700561 }
562 }
563
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700564 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700565 * Are there any media notifications active?
566 */
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700567 fun hasActiveMedia() = mediaEntries.any { it.value.active }
Selim Cinekb52642b2020-04-17 14:30:29 -0700568
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400569 /**
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400570 * Are there any media entries we should display?
571 * If resumption is enabled, this will include inactive players
572 * If resumption is disabled, we only want to show active players
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400573 */
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700574 fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400575
Beth Thibodeauc1bc3072020-06-09 23:36:17 -0400576 fun setMediaResumptionEnabled(isEnabled: Boolean) {
577 if (useMediaResumption == isEnabled) {
578 return
579 }
580
581 useMediaResumption = isEnabled
582
583 if (!useMediaResumption) {
584 // Remove any existing resume controls
585 val listenersCopy = listeners.toSet()
586 val filtered = mediaEntries.filter { !it.value.active }
587 filtered.forEach {
588 mediaEntries.remove(it.key)
589 listenersCopy.forEach { listener ->
590 listener.onMediaDataRemoved(it.key)
591 }
592 }
593 }
594 }
595
Selim Cinekafae4e72020-06-16 18:21:41 -0700596 /**
597 * Invoked when the user has dismissed the media carousel
598 */
599 fun onSwipeToDismiss() {
600 val mediaKeys = mediaEntries.keys.toSet()
601 mediaKeys.forEach {
602 setTimedOut(it, timedOut = true)
603 }
604 }
605
Selim Cinekb52642b2020-04-17 14:30:29 -0700606 interface Listener {
607
608 /**
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400609 * Called whenever there's new MediaData Loaded for the consumption in views.
610 *
611 * oldKey is provided to check whether the view has changed keys, which can happen when a
612 * player has gone from resume state (key is package name) to active state (key is
613 * notification key) or vice versa.
Selim Cinekb52642b2020-04-17 14:30:29 -0700614 */
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -0400615 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
Selim Cinekb52642b2020-04-17 14:30:29 -0700616
617 /**
618 * Called whenever a previously existing Media notification was removed
619 */
620 fun onMediaDataRemoved(key: String) {}
621 }
Lucas Dupinae437a12020-06-17 17:20:33 -0700622
623 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
624 pw.apply {
625 println("listeners: $listeners")
626 println("mediaEntries: $mediaEntries")
627 println("useMediaResumption: $useMediaResumption")
628 }
629 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700630}