blob: e7d0f7ec1a375afb003c854445302c88a726d52f [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
25import android.graphics.Rect
Selim Cinek5dbef2d2020-05-07 17:44:38 -070026import android.graphics.drawable.Drawable
Selim Cinekc5436712020-04-27 15:15:44 -070027import android.graphics.drawable.Icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -070028import android.media.MediaMetadata
29import android.media.session.MediaSession
Selim Cinekc5436712020-04-27 15:15:44 -070030import android.net.Uri
Selim Cinek5dbef2d2020-05-07 17:44:38 -070031import android.provider.Settings
32import android.service.notification.StatusBarNotification
Selim Cinekc5436712020-04-27 15:15:44 -070033import android.text.TextUtils
34import android.util.Log
Selim Cinek5dbef2d2020-05-07 17:44:38 -070035import com.android.internal.util.ContrastColorUtil
Selim Cinek5dbef2d2020-05-07 17:44:38 -070036import com.android.systemui.dagger.qualifiers.Background
37import com.android.systemui.dagger.qualifiers.Main
Selim Cinek5dbef2d2020-05-07 17:44:38 -070038import com.android.systemui.statusbar.notification.MediaNotificationProcessor
Selim Cinekc5436712020-04-27 15:15:44 -070039import com.android.systemui.statusbar.notification.row.HybridGroupManager
40import java.io.IOException
Selim Cinek5dbef2d2020-05-07 17:44:38 -070041import java.util.*
42import java.util.concurrent.Executor
43import javax.inject.Inject
44import javax.inject.Singleton
45import kotlin.collections.LinkedHashMap
46
Selim Cinekc5436712020-04-27 15:15:44 -070047// URI fields to try loading album art from
48private val ART_URIS = arrayOf(
49 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
50 MediaMetadata.METADATA_KEY_ART_URI,
51 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
52)
53
54private const val TAG = "MediaDataManager"
55
56private val LOADING = MediaData(false, 0, 0, null, null, null, null, null,
57 emptyList(), emptyList(), null, null, null)
58
Selim Cinek5dbef2d2020-05-07 17:44:38 -070059/**
60 * A class that facilitates management and loading of Media Data, ready for binding.
61 */
62@Singleton
63class MediaDataManager @Inject constructor(
64 private val context: Context,
65 private val mediaControllerFactory: MediaControllerFactory,
66 @Background private val backgroundExecutor: Executor,
67 @Main private val foregroundExcecutor: Executor
68) {
69
Selim Cinekb52642b2020-04-17 14:30:29 -070070 private val listeners: MutableSet<Listener> = mutableSetOf()
Selim Cinek5dbef2d2020-05-07 17:44:38 -070071 private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
72
Selim Cinek5dbef2d2020-05-07 17:44:38 -070073 fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
74 if (isMediaNotification(sbn)) {
75 if (!mediaEntries.containsKey(key)) {
76 mediaEntries.put(key, LOADING)
77 }
78 loadMediaData(key, sbn)
79 } else {
80 onNotificationRemoved(key)
81 }
82 }
83
84 private fun loadMediaData(key: String, sbn: StatusBarNotification) {
85 backgroundExecutor.execute {
86 loadMediaDataInBg(key, sbn)
87 }
88 }
89
Selim Cinekb52642b2020-04-17 14:30:29 -070090 /**
91 * Add a listener for changes in this class
92 */
93 fun addListener(listener: Listener) = listeners.add(listener)
94
95 /**
96 * Remove a listener for changes in this class
97 */
98 fun removeListener(listener: Listener) = listeners.remove(listener)
99
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700100 private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
101 val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
102 as MediaSession.Token?
103 val metadata = mediaControllerFactory.create(token).metadata
104
105 if (metadata == null) {
106 // TODO: handle this better, removing media notification
107 return
108 }
109
110 // Foreground and Background colors computed from album art
111 val notif: Notification = sbn.notification
112 var fgColor = notif.color
113 var bgColor = -1
114 var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)
115 if (artworkBitmap == null) {
116 artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
117 }
Selim Cinekc5436712020-04-27 15:15:44 -0700118 if (artworkBitmap == null) {
119 artworkBitmap = loadBitmapFromUri(metadata)
120 }
121 val artWorkIcon = if (artworkBitmap == null) {
122 notif.getLargeIcon()
123 } else {
124 Icon.createWithBitmap(artworkBitmap)
125 }
126 if (artWorkIcon != null) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700127 // If we have art, get colors from that
Selim Cinekc5436712020-04-27 15:15:44 -0700128 if (artworkBitmap == null) {
129 if (artWorkIcon.type == Icon.TYPE_BITMAP
130 || artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) {
131 artworkBitmap = artWorkIcon.bitmap
132 } else {
133 val drawable: Drawable = artWorkIcon.loadDrawable(context)
134 artworkBitmap = Bitmap.createBitmap(
135 drawable.intrinsicWidth,
136 drawable.intrinsicHeight,
137 Bitmap.Config.ARGB_8888)
138 val canvas = Canvas(artworkBitmap)
139 drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
140 drawable.draw(canvas)
141 }
142 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700143 val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
144 .generate()
145 val swatch = MediaNotificationProcessor.findBackgroundSwatch(p)
146 bgColor = swatch.rgb
147 fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p)
148 }
149 // Make sure colors will be legible
150 val isDark = !ContrastColorUtil.isColorLight(bgColor)
151 fgColor = ContrastColorUtil.resolveContrastColor(context, fgColor, bgColor,
152 isDark)
153 fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark)
154
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700155 // App name
156 val builder = Notification.Builder.recoverBuilder(context, notif)
157 val app = builder.loadHeaderAppName()
158
159 // App Icon
160 val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context)
161
162 // Song name
Selim Cinekc5436712020-04-27 15:15:44 -0700163 var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
164 if (song == null) {
165 song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
166 }
167 if (song == null) {
168 song = HybridGroupManager.resolveTitle(notif)
169 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700170
171 // Artist name
Selim Cinekc5436712020-04-27 15:15:44 -0700172 var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
173 if (artist == null) {
174 artist = HybridGroupManager.resolveText(notif)
175 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700176
177 // Control buttons
178 val actionIcons: MutableList<MediaAction> = ArrayList()
179 val actions = notif.actions
180 val actionsToShowCollapsed = notif.extras.getIntArray(
181 Notification.EXTRA_COMPACT_ACTIONS)?.toList() ?: emptyList()
182 // TODO: b/153736623 look into creating actions when this isn't a media style notification
183
184 val packageContext: Context = sbn.getPackageContext(context)
185 for (action in actions) {
186 val mediaAction = MediaAction(
187 action.getIcon().loadDrawable(packageContext),
188 action.actionIntent,
189 action.title)
190 actionIcons.add(mediaAction)
191 }
192
193 foregroundExcecutor.execute {
194 onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist,
Selim Cinekc5436712020-04-27 15:15:44 -0700195 song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700196 notif.contentIntent))
197 }
198
199 }
200
Selim Cinekc5436712020-04-27 15:15:44 -0700201 /**
202 * Load a bitmap from the various Art metadata URIs
203 */
204 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
205 for (uri in ART_URIS) {
206 val uriString = metadata.getString(uri)
207 if (!TextUtils.isEmpty(uriString)) {
208 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
209 if (albumArt != null) {
210 Log.d(TAG, "loaded art from $uri")
211 break
212 }
213 }
214 }
215 return null
216 }
217
218 /**
219 * Load a bitmap from a URI
220 * @param uri the uri to load
221 * @return bitmap, or null if couldn't be loaded
222 */
223 private fun loadBitmapFromUri(uri: Uri): Bitmap? {
224 // ImageDecoder requires a scheme of the following types
225 if (uri.getScheme() == null) {
226 return null;
227 }
228
229 if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
230 && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
231 && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
232 return null;
233 }
234
235 val source = ImageDecoder.createSource(context.getContentResolver(), uri)
236 return try {
237 ImageDecoder.decodeBitmap(source)
238 } catch (e: IOException) {
239 e.printStackTrace()
240 null
241 }
242 }
243
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700244 fun onMediaDataLoaded(key: String, data: MediaData) {
245 if (mediaEntries.containsKey(key)) {
246 // Otherwise this was removed already
247 mediaEntries.put(key, data)
Selim Cinekb52642b2020-04-17 14:30:29 -0700248 listeners.forEach {
249 it.onMediaDataLoaded(key, data)
250 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700251 }
252 }
253
254 fun onNotificationRemoved(key: String) {
255 val removed = mediaEntries.remove(key)
256 if (removed != null) {
Selim Cinekb52642b2020-04-17 14:30:29 -0700257 listeners.forEach {
258 it.onMediaDataRemoved(key)
259 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700260 }
261 }
262
263 private fun isMediaNotification(sbn: StatusBarNotification) : Boolean {
264 if (!useUniversalMediaPlayer()) {
265 return false
266 }
267 if (!sbn.notification.hasMediaSession()) {
268 return false
269 }
270 val notificationStyle = sbn.notification.notificationStyle
271 if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle)
272 || Notification.MediaStyle::class.java.equals(notificationStyle)) {
273 return true
274 }
275 return false
276 }
277
278 /**
279 * are we using the universal media player
280 */
281 private fun useUniversalMediaPlayer()
282 = Settings.System.getInt(context.contentResolver, "qs_media_player", 1) > 0
283
284 /**
285 * Are there any media notifications active?
286 */
287 fun hasActiveMedia() = mediaEntries.size > 0
Selim Cinekb52642b2020-04-17 14:30:29 -0700288
289 fun hasAnyMedia(): Boolean {
290 // TODO: implement this when we implemented resumption
291 return hasActiveMedia()
292 }
293
294 interface Listener {
295
296 /**
297 * Called whenever there's new MediaData Loaded for the consumption in views
298 */
299 fun onMediaDataLoaded(key: String, data: MediaData) {}
300
301 /**
302 * Called whenever a previously existing Media notification was removed
303 */
304 fun onMediaDataRemoved(key: String) {}
305 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700306}