blob: 8da864cb7eed274752fba5e4d54cde12eaef1f18 [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
Selim Cinekc5436712020-04-27 15:15:44 -070020import android.content.ContentResolver
Selim Cinek5dbef2d2020-05-07 17:44:38 -070021import android.content.Context
22import android.graphics.Bitmap
Selim Cinekc5436712020-04-27 15:15:44 -070023import android.graphics.Canvas
24import android.graphics.ImageDecoder
Selim Cinek5dbef2d2020-05-07 17:44:38 -070025import android.graphics.drawable.Drawable
Selim Cinekc5436712020-04-27 15:15:44 -070026import android.graphics.drawable.Icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -070027import android.media.MediaMetadata
28import android.media.session.MediaSession
Selim Cinekc5436712020-04-27 15:15:44 -070029import android.net.Uri
Selim Cinek5dbef2d2020-05-07 17:44:38 -070030import android.service.notification.StatusBarNotification
Selim Cinekc5436712020-04-27 15:15:44 -070031import android.text.TextUtils
32import android.util.Log
Selim Cinek5dbef2d2020-05-07 17:44:38 -070033import com.android.internal.util.ContrastColorUtil
Selim Cinek5dbef2d2020-05-07 17:44:38 -070034import com.android.systemui.dagger.qualifiers.Background
35import com.android.systemui.dagger.qualifiers.Main
Selim Cinek5dbef2d2020-05-07 17:44:38 -070036import com.android.systemui.statusbar.notification.MediaNotificationProcessor
Selim Cinekc5436712020-04-27 15:15:44 -070037import com.android.systemui.statusbar.notification.row.HybridGroupManager
Lucas Dupin03859d72020-05-12 12:27:47 -070038import com.android.systemui.util.Utils
Selim Cinekc5436712020-04-27 15:15:44 -070039import java.io.IOException
Selim Cinek5dbef2d2020-05-07 17:44:38 -070040import java.util.concurrent.Executor
41import javax.inject.Inject
42import javax.inject.Singleton
43import kotlin.collections.LinkedHashMap
44
Selim Cinekc5436712020-04-27 15:15:44 -070045// URI fields to try loading album art from
46private val ART_URIS = arrayOf(
47 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
48 MediaMetadata.METADATA_KEY_ART_URI,
49 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
50)
51
52private const val TAG = "MediaDataManager"
53
54private val LOADING = MediaData(false, 0, 0, null, null, null, null, null,
55 emptyList(), emptyList(), null, null, null)
56
Selim Cinek5dbef2d2020-05-07 17:44:38 -070057/**
58 * A class that facilitates management and loading of Media Data, ready for binding.
59 */
60@Singleton
61class MediaDataManager @Inject constructor(
62 private val context: Context,
63 private val mediaControllerFactory: MediaControllerFactory,
64 @Background private val backgroundExecutor: Executor,
65 @Main private val foregroundExcecutor: Executor
66) {
67
Selim Cinekb52642b2020-04-17 14:30:29 -070068 private val listeners: MutableSet<Listener> = mutableSetOf()
Selim Cinek5dbef2d2020-05-07 17:44:38 -070069 private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
70
Selim Cinek5dbef2d2020-05-07 17:44:38 -070071 fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
72 if (isMediaNotification(sbn)) {
73 if (!mediaEntries.containsKey(key)) {
74 mediaEntries.put(key, LOADING)
75 }
76 loadMediaData(key, sbn)
77 } else {
78 onNotificationRemoved(key)
79 }
80 }
81
82 private fun loadMediaData(key: String, sbn: StatusBarNotification) {
83 backgroundExecutor.execute {
84 loadMediaDataInBg(key, sbn)
85 }
86 }
87
Selim Cinekb52642b2020-04-17 14:30:29 -070088 /**
89 * Add a listener for changes in this class
90 */
91 fun addListener(listener: Listener) = listeners.add(listener)
92
93 /**
94 * Remove a listener for changes in this class
95 */
96 fun removeListener(listener: Listener) = listeners.remove(listener)
97
Selim Cinek5dbef2d2020-05-07 17:44:38 -070098 private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
99 val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
100 as MediaSession.Token?
101 val metadata = mediaControllerFactory.create(token).metadata
102
103 if (metadata == null) {
104 // TODO: handle this better, removing media notification
105 return
106 }
107
108 // Foreground and Background colors computed from album art
109 val notif: Notification = sbn.notification
110 var fgColor = notif.color
111 var bgColor = -1
112 var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)
113 if (artworkBitmap == null) {
114 artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
115 }
Selim Cinekc5436712020-04-27 15:15:44 -0700116 if (artworkBitmap == null) {
117 artworkBitmap = loadBitmapFromUri(metadata)
118 }
119 val artWorkIcon = if (artworkBitmap == null) {
120 notif.getLargeIcon()
121 } else {
122 Icon.createWithBitmap(artworkBitmap)
123 }
124 if (artWorkIcon != null) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700125 // If we have art, get colors from that
Selim Cinekc5436712020-04-27 15:15:44 -0700126 if (artworkBitmap == null) {
Lucas Dupin03859d72020-05-12 12:27:47 -0700127 if (artWorkIcon.type == Icon.TYPE_BITMAP ||
128 artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) {
Selim Cinekc5436712020-04-27 15:15:44 -0700129 artworkBitmap = artWorkIcon.bitmap
130 } else {
131 val drawable: Drawable = artWorkIcon.loadDrawable(context)
132 artworkBitmap = Bitmap.createBitmap(
133 drawable.intrinsicWidth,
134 drawable.intrinsicHeight,
135 Bitmap.Config.ARGB_8888)
136 val canvas = Canvas(artworkBitmap)
137 drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
138 drawable.draw(canvas)
139 }
140 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700141 val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
142 .generate()
143 val swatch = MediaNotificationProcessor.findBackgroundSwatch(p)
144 bgColor = swatch.rgb
145 fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p)
146 }
147 // Make sure colors will be legible
148 val isDark = !ContrastColorUtil.isColorLight(bgColor)
149 fgColor = ContrastColorUtil.resolveContrastColor(context, fgColor, bgColor,
150 isDark)
151 fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark)
152
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700153 // App name
154 val builder = Notification.Builder.recoverBuilder(context, notif)
155 val app = builder.loadHeaderAppName()
156
157 // App Icon
158 val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context)
159
160 // Song name
Selim Cinekc5436712020-04-27 15:15:44 -0700161 var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
162 if (song == null) {
163 song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
164 }
165 if (song == null) {
166 song = HybridGroupManager.resolveTitle(notif)
167 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700168
169 // Artist name
Selim Cinekc5436712020-04-27 15:15:44 -0700170 var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
171 if (artist == null) {
172 artist = HybridGroupManager.resolveText(notif)
173 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700174
175 // Control buttons
176 val actionIcons: MutableList<MediaAction> = ArrayList()
177 val actions = notif.actions
178 val actionsToShowCollapsed = notif.extras.getIntArray(
179 Notification.EXTRA_COMPACT_ACTIONS)?.toList() ?: emptyList()
180 // TODO: b/153736623 look into creating actions when this isn't a media style notification
181
182 val packageContext: Context = sbn.getPackageContext(context)
183 for (action in actions) {
184 val mediaAction = MediaAction(
185 action.getIcon().loadDrawable(packageContext),
186 action.actionIntent,
187 action.title)
188 actionIcons.add(mediaAction)
189 }
190
191 foregroundExcecutor.execute {
192 onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist,
Selim Cinekc5436712020-04-27 15:15:44 -0700193 song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700194 notif.contentIntent))
195 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700196 }
197
Selim Cinekc5436712020-04-27 15:15:44 -0700198 /**
199 * Load a bitmap from the various Art metadata URIs
200 */
201 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
202 for (uri in ART_URIS) {
203 val uriString = metadata.getString(uri)
204 if (!TextUtils.isEmpty(uriString)) {
205 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
206 if (albumArt != null) {
207 Log.d(TAG, "loaded art from $uri")
208 break
209 }
210 }
211 }
212 return null
213 }
214
215 /**
216 * Load a bitmap from a URI
217 * @param uri the uri to load
218 * @return bitmap, or null if couldn't be loaded
219 */
220 private fun loadBitmapFromUri(uri: Uri): Bitmap? {
221 // ImageDecoder requires a scheme of the following types
Lucas Dupin03859d72020-05-12 12:27:47 -0700222 if (uri.scheme == null) {
223 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700224 }
225
Lucas Dupin03859d72020-05-12 12:27:47 -0700226 if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
227 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
228 !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
229 return null
Selim Cinekc5436712020-04-27 15:15:44 -0700230 }
231
232 val source = ImageDecoder.createSource(context.getContentResolver(), uri)
233 return try {
234 ImageDecoder.decodeBitmap(source)
235 } catch (e: IOException) {
236 e.printStackTrace()
237 null
238 }
239 }
240
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700241 fun onMediaDataLoaded(key: String, data: MediaData) {
242 if (mediaEntries.containsKey(key)) {
243 // Otherwise this was removed already
244 mediaEntries.put(key, data)
Selim Cinekb52642b2020-04-17 14:30:29 -0700245 listeners.forEach {
246 it.onMediaDataLoaded(key, data)
247 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700248 }
249 }
250
251 fun onNotificationRemoved(key: String) {
252 val removed = mediaEntries.remove(key)
253 if (removed != null) {
Selim Cinekb52642b2020-04-17 14:30:29 -0700254 listeners.forEach {
255 it.onMediaDataRemoved(key)
256 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700257 }
258 }
259
Lucas Dupin03859d72020-05-12 12:27:47 -0700260 private fun isMediaNotification(sbn: StatusBarNotification): Boolean {
261 if (!Utils.useQsMediaPlayer(context)) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700262 return false
263 }
264 if (!sbn.notification.hasMediaSession()) {
265 return false
266 }
267 val notificationStyle = sbn.notification.notificationStyle
Lucas Dupin03859d72020-05-12 12:27:47 -0700268 if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) ||
269 Notification.MediaStyle::class.java.equals(notificationStyle)) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700270 return true
271 }
272 return false
273 }
274
275 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700276 * Are there any media notifications active?
277 */
278 fun hasActiveMedia() = mediaEntries.size > 0
Selim Cinekb52642b2020-04-17 14:30:29 -0700279
280 fun hasAnyMedia(): Boolean {
281 // TODO: implement this when we implemented resumption
282 return hasActiveMedia()
283 }
284
285 interface Listener {
286
287 /**
288 * Called whenever there's new MediaData Loaded for the consumption in views
289 */
290 fun onMediaDataLoaded(key: String, data: MediaData) {}
291
292 /**
293 * Called whenever a previously existing Media notification was removed
294 */
295 fun onMediaDataRemoved(key: String) {}
296 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700297}