@@ -2,20 +2,16 @@ package sw2.io.mediafloat.media
22
33import android.content.ComponentName
44import android.content.Context
5- import android.graphics.Bitmap
6- import android.graphics.BitmapFactory
75import android.media.session.MediaController
86import android.media.MediaMetadata
97import android.media.session.MediaSessionManager
108import android.media.session.PlaybackState
11- import android.net.Uri
129import android.os.Handler
1310import android.os.Looper
1411import android.util.Log
1512import sw2.io.mediafloat.debug.DebugLogWriter
1613import sw2.io.mediafloat.debug.NoOpDebugLogWriter
1714import sw2.io.mediafloat.model.MediaArtwork
18- import sw2.io.mediafloat.model.MediaArtworkSource
1915import sw2.io.mediafloat.model.MediaCommand
2016import sw2.io.mediafloat.model.MediaSessionErrorReason
2117import sw2.io.mediafloat.model.MediaSessionLimitReason
@@ -37,14 +33,17 @@ class AndroidMediaSessionRepository(
3733 appContext.getSystemService(MediaSessionManager ::class .java)
3834 private val listenerComponent = ComponentName (appContext, MediaNotificationListenerService ::class .java)
3935 private val handler = Handler (Looper .getMainLooper())
36+ private val controllerSelector = MediaControllerSelector
37+ private val recoveryPolicy = MediaRecoveryPolicy
38+ private val artworkResolver = MediaArtworkCandidateResolver (appContext.contentResolver)
4039 private val listeners = linkedSetOf<MediaSessionStateListener >()
4140 private val activeSessionsListener = MediaSessionManager .OnActiveSessionsChangedListener { controllers ->
4241 handleControllersChanged(controllers.orEmpty())
4342 }
4443 private val controllerCallback = object : MediaController .Callback () {
4544 override fun onPlaybackStateChanged (state : PlaybackState ? ) {
4645 publishState(resolveState(currentController))
47- if (shouldRecoverAfterPlaybackStateChange(state)) {
46+ if (recoveryPolicy. shouldRecoverAfterPlaybackStateChange(state)) {
4847 requestRecovery(" playback_state_${state?.state ? : " unknown" } " )
4948 }
5049 }
@@ -221,25 +220,14 @@ class AndroidMediaSessionRepository(
221220 registerController(nextController)
222221 val nextState = resolveState(nextController)
223222 publishState(nextState)
224- if (shouldContinueRecovery(nextState)) {
223+ if (recoveryPolicy. shouldContinueRecovery(nextState)) {
225224 requestRecovery(" controller_state_changed" )
226225 } else {
227226 clearRecovery()
228227 }
229228 }
230229
231- private fun selectController (controllers : List <MediaController >): MediaController ? {
232- if (controllers.isEmpty()) {
233- return null
234- }
235-
236- return controllers
237- .sortedWith(
238- compareByDescending<MediaController > { it.playbackState.isActivelyPlaying() }
239- .thenByDescending { it.playbackState?.lastPositionUpdateTime ? : 0L }
240- )
241- .firstOrNull()
242- }
230+ private fun selectController (controllers : List <MediaController >): MediaController ? = controllerSelector.select(controllers)
243231
244232 private fun registerController (controller : MediaController ) {
245233 if (currentController?.sessionToken == controller.sessionToken) {
@@ -288,78 +276,7 @@ class AndroidMediaSessionRepository(
288276 }
289277
290278 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- )
279+ return artworkResolver.resolve(controller, notificationArtworkByPackage)
363280 }
364281
365282 private fun publishState (state : MediaSessionState ) {
@@ -377,7 +294,7 @@ class AndroidMediaSessionRepository(
377294 val nextState = refresh(reason = " recovery_${attempt + 1 } _$reason " )
378295 synchronized(this ) {
379296 recoveryRunnable = null
380- if (connected && attempt + 1 < MAX_RECOVERY_ATTEMPTS && shouldContinueRecovery(nextState)) {
297+ if (connected && attempt + 1 < MAX_RECOVERY_ATTEMPTS && recoveryPolicy. shouldContinueRecovery(nextState)) {
381298 scheduleRecovery(reason = reason, attempt = attempt + 1 )
382299 }
383300 }
@@ -393,33 +310,9 @@ class AndroidMediaSessionRepository(
393310 recoveryRunnable = null
394311 }
395312
396- private fun shouldRecoverAfterPlaybackStateChange (state : PlaybackState ? ): Boolean {
397- if (state == null ) {
398- return true
399- }
313+ private fun shouldRecoverAfterPlaybackStateChange (state : PlaybackState ? ): Boolean = recoveryPolicy.shouldRecoverAfterPlaybackStateChange(state)
400314
401- return when (state.state) {
402- PlaybackState .STATE_NONE ,
403- PlaybackState .STATE_STOPPED ,
404- PlaybackState .STATE_ERROR -> true
405- else -> state.actions.toSupportedCommands().isEmpty()
406- }
407- }
408-
409- private fun shouldContinueRecovery (state : MediaSessionState ): Boolean {
410- return when (state) {
411- is MediaSessionState .Active -> false
412- is MediaSessionState .Limited -> state.reason == MediaSessionLimitReason .PlaybackStateUnknown ||
413- state.reason == MediaSessionLimitReason .MissingTransportControls
414- MediaSessionState .Discovering ,
415- MediaSessionState .Unavailable -> true
416- is MediaSessionState .Error -> false
417- }
418- }
419-
420- private fun PlaybackState?.isActivelyPlaying (): Boolean {
421- return this ?.state == PlaybackState .STATE_PLAYING || this ?.state == PlaybackState .STATE_BUFFERING
422- }
315+ private fun shouldContinueRecovery (state : MediaSessionState ): Boolean = recoveryPolicy.shouldContinueRecovery(state)
423316
424317 internal companion object {
425318 fun buildMediaSessionState (
0 commit comments