@@ -2,21 +2,31 @@ package sw2.io.mediafloat.media
22
33import android.content.ComponentName
44import android.content.Context
5+ import android.graphics.Bitmap
6+ import android.graphics.BitmapFactory
57import android.media.session.MediaController
68import android.media.MediaMetadata
79import android.media.session.MediaSessionManager
810import android.media.session.PlaybackState
11+ import android.net.Uri
912import android.os.Handler
1013import android.os.Looper
1114import android.util.Log
1215import sw2.io.mediafloat.debug.DebugLogWriter
1316import sw2.io.mediafloat.debug.NoOpDebugLogWriter
17+ import sw2.io.mediafloat.model.MediaArtwork
18+ import sw2.io.mediafloat.model.MediaArtworkSource
1419import sw2.io.mediafloat.model.MediaCommand
1520import sw2.io.mediafloat.model.MediaSessionErrorReason
1621import sw2.io.mediafloat.model.MediaSessionLimitReason
1722import sw2.io.mediafloat.model.MediaSessionState
1823import sw2.io.mediafloat.model.PlaybackStatus
1924
25+ internal data class NotificationArtworkSnapshot (
26+ val packageName : String ,
27+ val artwork : MediaArtwork .BitmapSource
28+ )
29+
2030class AndroidMediaSessionRepository (
2131 context : Context ,
2232 private val debugLogWriter : DebugLogWriter = NoOpDebugLogWriter
@@ -55,6 +65,9 @@ class AndroidMediaSessionRepository(
5565 @Volatile
5666 private var currentController: MediaController ? = null
5767
68+ @Volatile
69+ private var notificationArtworkByPackage: Map <String , MediaArtwork .BitmapSource > = emptyMap()
70+
5871 private var connected = false
5972 private var connectionCount = 0
6073 private var recoveryRunnable: Runnable ? = null
@@ -186,9 +199,16 @@ class AndroidMediaSessionRepository(
186199 fun onNotificationListenerDisconnected () {
187200 debugLogWriter.warn(TAG , " Notification listener disconnected; clearing pending media recovery" )
188201 clearRecovery()
202+ notificationArtworkByPackage = emptyMap()
189203 publishState(MediaSessionState .Discovering )
190204 }
191205
206+ @Synchronized
207+ internal fun replaceNotificationArtworkSnapshot (snapshots : List <NotificationArtworkSnapshot >) {
208+ notificationArtworkByPackage = snapshots.associate { it.packageName to it.artwork }
209+ currentController?.let { publishState(resolveState(it)) }
210+ }
211+
192212 private fun handleControllersChanged (controllers : List <MediaController >) {
193213 val nextController = selectController(controllers)
194214 if (nextController == null ) {
@@ -247,23 +267,101 @@ class AndroidMediaSessionRepository(
247267 displayTitle = controller.metadata?.getText(MediaMetadata .METADATA_KEY_DISPLAY_TITLE ),
248268 title = controller.metadata?.getText(MediaMetadata .METADATA_KEY_TITLE )
249269 )
270+ val artworkCandidates = resolveArtworkCandidates(controller)
250271 val sessionId = " ${controller.packageName} :${controller.sessionToken} "
251272 val playbackState = controller.playbackState
252273 ? : return buildMediaSessionState(
253274 sessionId = sessionId,
254275 title = title,
276+ artworkCandidates = artworkCandidates,
255277 playbackStatus = null ,
256278 supportedActions = null
257279 )
258280
259281 return buildMediaSessionState(
260282 sessionId = sessionId,
261283 title = title,
284+ artworkCandidates = artworkCandidates,
262285 playbackStatus = playbackState.state.toPlaybackStatus(),
263286 supportedActions = playbackState.actions.toSupportedCommands()
264287 )
265288 }
266289
290+ private fun resolveArtworkCandidates (controller : MediaController ): List <MediaArtwork > {
291+ val metadata = controller.metadata
292+
293+ return buildArtworkCandidates(
294+ metadataDisplayIconUri = resolveArtworkUriCandidate(
295+ rawUri = metadata?.getString(MediaMetadata .METADATA_KEY_DISPLAY_ICON_URI ),
296+ source = MediaArtworkSource .MetadataDisplayIconUri
297+ ),
298+ metadataArtUri = resolveArtworkUriCandidate(
299+ rawUri = metadata?.getString(MediaMetadata .METADATA_KEY_ART_URI ),
300+ source = MediaArtworkSource .MetadataArtUri
301+ ),
302+ metadataAlbumArtUri = resolveArtworkUriCandidate(
303+ rawUri = metadata?.getString(MediaMetadata .METADATA_KEY_ALBUM_ART_URI ),
304+ source = MediaArtworkSource .MetadataAlbumArtUri
305+ ),
306+ metadataDisplayIconBitmap = resolveArtworkBitmapCandidate(
307+ bitmap = metadata?.getBitmap(MediaMetadata .METADATA_KEY_DISPLAY_ICON ),
308+ source = MediaArtworkSource .MetadataDisplayIconBitmap
309+ ),
310+ metadataArtBitmap = resolveArtworkBitmapCandidate(
311+ bitmap = metadata?.getBitmap(MediaMetadata .METADATA_KEY_ART ),
312+ source = MediaArtworkSource .MetadataArtBitmap
313+ ),
314+ metadataAlbumArtBitmap = resolveArtworkBitmapCandidate(
315+ bitmap = metadata?.getBitmap(MediaMetadata .METADATA_KEY_ALBUM_ART ),
316+ source = MediaArtworkSource .MetadataAlbumArtBitmap
317+ ),
318+ notificationLargeIcon = notificationArtworkByPackage[controller.packageName]
319+ )
320+ }
321+
322+ private fun resolveArtworkUriCandidate (
323+ rawUri : String? ,
324+ source : MediaArtworkSource
325+ ): MediaArtwork .UriSource ? {
326+ val normalizedUri = rawUri
327+ ?.trim()
328+ ?.takeIf { it.isNotEmpty() }
329+ ? : return null
330+
331+ val artworkBounds = runCatching {
332+ appContext.contentResolver.openInputStream(Uri .parse(normalizedUri))?.use { stream ->
333+ val options = BitmapFactory .Options ().apply { inJustDecodeBounds = true }
334+ BitmapFactory .decodeStream(stream, null , options)
335+ if (options.outWidth > 0 && options.outHeight > 0 ) {
336+ options.outWidth to options.outHeight
337+ } else {
338+ null
339+ }
340+ }
341+ }.getOrNull() ? : return null
342+
343+ return MediaArtwork .UriSource (
344+ source = source,
345+ uri = normalizedUri,
346+ widthPx = artworkBounds.first,
347+ heightPx = artworkBounds.second
348+ )
349+ }
350+
351+ private fun resolveArtworkBitmapCandidate (
352+ bitmap : Bitmap ? ,
353+ source : MediaArtworkSource
354+ ): MediaArtwork .BitmapSource ? {
355+ val resolvedBitmap = bitmap?.takeIf { it.width > 0 && it.height > 0 } ? : return null
356+
357+ return MediaArtwork .BitmapSource (
358+ source = source,
359+ bitmap = resolvedBitmap,
360+ widthPx = resolvedBitmap.width,
361+ heightPx = resolvedBitmap.height
362+ )
363+ }
364+
267365 private fun publishState (state : MediaSessionState ) {
268366 if (currentState != state) {
269367 debugLogWriter.debug(TAG , " Media session state updated" , " state=$state " )
@@ -327,13 +425,15 @@ class AndroidMediaSessionRepository(
327425 fun buildMediaSessionState (
328426 sessionId : String ,
329427 title : String? ,
428+ artworkCandidates : List <MediaArtwork > = emptyList(),
330429 playbackStatus : PlaybackStatus ? ,
331430 supportedActions : Set <MediaCommand >?
332431 ): MediaSessionState {
333432 val resolvedPlaybackStatus = playbackStatus
334433 ? : return MediaSessionState .Limited (
335434 reason = MediaSessionLimitReason .PlaybackStateUnknown ,
336435 title = title,
436+ artworkCandidates = artworkCandidates,
337437 supportedActions = emptySet()
338438 )
339439
@@ -343,18 +443,40 @@ class AndroidMediaSessionRepository(
343443 MediaSessionState .Limited (
344444 reason = MediaSessionLimitReason .MissingTransportControls ,
345445 title = title,
446+ artworkCandidates = artworkCandidates,
346447 supportedActions = emptySet()
347448 )
348449 } else {
349450 MediaSessionState .Active (
350451 sessionId = sessionId,
351452 title = title,
453+ artworkCandidates = artworkCandidates,
352454 supportedActions = resolvedSupportedActions,
353455 playbackStatus = resolvedPlaybackStatus
354456 )
355457 }
356458 }
357459
460+ fun buildArtworkCandidates (
461+ metadataDisplayIconUri : MediaArtwork .UriSource ? ,
462+ metadataArtUri : MediaArtwork .UriSource ? ,
463+ metadataAlbumArtUri : MediaArtwork .UriSource ? ,
464+ metadataDisplayIconBitmap : MediaArtwork .BitmapSource ? ,
465+ metadataArtBitmap : MediaArtwork .BitmapSource ? ,
466+ metadataAlbumArtBitmap : MediaArtwork .BitmapSource ? ,
467+ notificationLargeIcon : MediaArtwork .BitmapSource ?
468+ ): List <MediaArtwork > {
469+ return listOfNotNull(
470+ metadataDisplayIconUri,
471+ metadataArtUri,
472+ metadataAlbumArtUri,
473+ metadataDisplayIconBitmap,
474+ metadataArtBitmap,
475+ metadataAlbumArtBitmap,
476+ notificationLargeIcon
477+ )
478+ }
479+
358480 fun resolveMediaTitle (displayTitle : CharSequence? , title : CharSequence? ): String? {
359481 return displayTitle.normalizedMediaTitle() ? : title.normalizedMediaTitle()
360482 }
0 commit comments