capacitor-plugin-playlist 0.11.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +133 -127
  2. package/android/build.gradle +20 -15
  3. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  4. package/android/src/main/java/org/dwbn/plugins/playlist/AudioMediaItemFactory.java +96 -0
  5. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +3 -3
  6. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +145 -188
  7. package/android/src/main/java/org/dwbn/plugins/playlist/RmxPlaybackErrorMapper.kt +26 -0
  8. package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +28 -28
  9. package/android/src/main/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridge.kt +102 -0
  10. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaybackProgress.kt +8 -0
  11. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +418 -183
  12. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicy.kt +98 -0
  13. package/android/src/main/java/org/dwbn/plugins/playlist/manager/RmxPlaybackState.kt +11 -0
  14. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +107 -211
  15. package/android/src/main/java/org/dwbn/plugins/playlist/service/GlideBitmapLoader.kt +121 -0
  16. package/android/src/main/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayer.kt +185 -0
  17. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicy.kt +253 -0
  18. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +260 -68
  19. package/android/src/main/java/org/dwbn/plugins/playlist/service/PlaylistMediaSessionCallback.kt +39 -0
  20. package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryMimeTest.kt +98 -0
  21. package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryTest.java +71 -0
  22. package/android/src/test/java/org/dwbn/plugins/playlist/data/AudioTrackTest.kt +53 -0
  23. package/android/src/test/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridgeTest.kt +84 -0
  24. package/android/src/test/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicyTest.kt +128 -0
  25. package/android/src/test/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandlerTest.kt +145 -0
  26. package/android/src/test/java/org/dwbn/plugins/playlist/service/GlideBitmapLoaderTest.kt +76 -0
  27. package/android/src/test/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayerTest.kt +282 -0
  28. package/android/src/test/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicyTest.kt +285 -0
  29. package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationCommandsTest.kt +54 -0
  30. package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationPolicyTest.kt +47 -0
  31. package/dist/docs.json +12 -846
  32. package/dist/esm/RmxAudioPlayer.d.ts +2 -2
  33. package/dist/esm/RmxAudioPlayer.js.map +1 -1
  34. package/dist/esm/definitions.d.ts +7 -7
  35. package/dist/esm/interfaces.d.ts +7 -1
  36. package/dist/esm/utils.d.ts +1 -1
  37. package/dist/esm/utils.js +2 -2
  38. package/dist/esm/utils.js.map +1 -1
  39. package/dist/esm/web.d.ts +3 -3
  40. package/dist/esm/web.js +5 -5
  41. package/dist/esm/web.js.map +1 -1
  42. package/dist/plugin.cjs.js +7 -7
  43. package/dist/plugin.cjs.js.map +1 -1
  44. package/dist/plugin.js +7 -7
  45. package/dist/plugin.js.map +1 -1
  46. package/ios/Sources/PlaylistPlugin/AVBidirectionalQueuePlayer.swift +45 -6
  47. package/ios/Sources/PlaylistPlugin/AudioTrack.swift +15 -2
  48. package/ios/Tests/PlaylistPluginTests/PositionResumeTests.swift +82 -0
  49. package/package.json +15 -19
  50. package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +0 -26
  51. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +0 -114
  52. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +0 -36
  53. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +0 -83
@@ -0,0 +1,84 @@
1
+ package org.dwbn.plugins.playlist.handoff
2
+
3
+ import androidx.media3.common.Player
4
+ import org.junit.After
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertSame
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Test
9
+ import java.lang.reflect.Proxy
10
+
11
+ class VideoPlayerBridgeTest {
12
+
13
+ @After
14
+ fun tearDown() {
15
+ VideoPlayerBridge.clearForTests()
16
+ }
17
+
18
+ @Test
19
+ fun attachAndDetach_trackActivePlayer() {
20
+ val player = dummyPlayer()
21
+ VideoPlayerBridge.attach(player)
22
+ assertSame(player, VideoPlayerBridge.activePlayer())
23
+ assertTrue(VideoPlayerBridge.hasActivePlayer())
24
+
25
+ VideoPlayerBridge.detach(player)
26
+ assertFalse(VideoPlayerBridge.hasActivePlayer())
27
+ }
28
+
29
+ @Test
30
+ fun detach_ignoresDifferentPlayerInstance() {
31
+ val attached = dummyPlayer()
32
+ val other = dummyPlayer()
33
+ VideoPlayerBridge.attach(attached)
34
+ VideoPlayerBridge.detach(other)
35
+ assertTrue(VideoPlayerBridge.hasActivePlayer())
36
+ VideoPlayerBridge.detach(attached)
37
+ assertFalse(VideoPlayerBridge.hasActivePlayer())
38
+ }
39
+
40
+ @Test
41
+ fun attach_notifiesPlaybackChangedListeners() {
42
+ var notifications = 0
43
+ VideoPlayerBridge.addPlaybackChangedListener { notifications++ }
44
+ VideoPlayerBridge.attach(dummyPlayer())
45
+ assertTrue(notifications > 0)
46
+ }
47
+
48
+ @Test
49
+ fun reAttach_replacesPreviousPlayer() {
50
+ val first = dummyPlayer()
51
+ val second = dummyPlayer()
52
+ VideoPlayerBridge.attach(first)
53
+ VideoPlayerBridge.attach(second)
54
+ assertSame(second, VideoPlayerBridge.activePlayer())
55
+ }
56
+
57
+ @Test
58
+ fun detach_notifiesPlaybackChangedListeners() {
59
+ val player = dummyPlayer()
60
+ var notifications = 0
61
+ VideoPlayerBridge.addPlaybackChangedListener { notifications++ }
62
+ VideoPlayerBridge.attach(player)
63
+ val afterAttach = notifications
64
+ VideoPlayerBridge.detach(player)
65
+ assertTrue(notifications > afterAttach)
66
+ }
67
+
68
+ @Test
69
+ fun detachNull_clearsActivePlayer() {
70
+ VideoPlayerBridge.attach(dummyPlayer())
71
+ VideoPlayerBridge.detach(null)
72
+ assertFalse(VideoPlayerBridge.hasActivePlayer())
73
+ }
74
+
75
+ private fun dummyPlayer(): Player =
76
+ Proxy.newProxyInstance(Player::class.java.classLoader, arrayOf(Player::class.java)) { _, method, _ ->
77
+ when (method.returnType) {
78
+ java.lang.Boolean.TYPE -> false
79
+ java.lang.Integer.TYPE -> Player.STATE_IDLE
80
+ java.lang.Long.TYPE -> 0L
81
+ else -> null
82
+ }
83
+ } as Player
84
+ }
@@ -0,0 +1,128 @@
1
+ package org.dwbn.plugins.playlist.manager
2
+
3
+ import androidx.media3.common.Player
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ /** Parity with the former PlaylistCore stack (loop, completion, error skip, play recovery). */
10
+ class PlaylistPlaybackPolicyTest {
11
+
12
+ @Test
13
+ fun repeatMode_loopWithSeveralItems_repeatsAll() {
14
+ assertEquals(Player.REPEAT_MODE_ALL, PlaylistPlaybackPolicy.repeatMode(loop = true, itemCount = 3))
15
+ }
16
+
17
+ @Test
18
+ fun repeatMode_loopWithSingleItem_stopsAtEndLikePlaylistCore() {
19
+ assertEquals(Player.REPEAT_MODE_OFF, PlaylistPlaybackPolicy.repeatMode(loop = true, itemCount = 1))
20
+ }
21
+
22
+ @Test
23
+ fun repeatMode_noLoop_isOff() {
24
+ assertEquals(Player.REPEAT_MODE_OFF, PlaylistPlaybackPolicy.repeatMode(loop = false, itemCount = 3))
25
+ }
26
+
27
+ @Test
28
+ fun nextAvailable_matchesFormerPlaylistManager() {
29
+ assertFalse(PlaylistPlaybackPolicy.nextAvailable(0, 1, loop = true))
30
+ assertTrue(PlaylistPlaybackPolicy.nextAvailable(0, 3, loop = false))
31
+ assertFalse(PlaylistPlaybackPolicy.nextAvailable(2, 3, loop = false))
32
+ assertTrue(PlaylistPlaybackPolicy.nextAvailable(2, 3, loop = true))
33
+ }
34
+
35
+ @Test
36
+ fun previousAvailable_atStartOnlyWhenLooping() {
37
+ assertFalse(PlaylistPlaybackPolicy.previousAvailable(0, loop = false, itemCount = 3))
38
+ assertTrue(PlaylistPlaybackPolicy.previousAvailable(0, loop = true, itemCount = 3))
39
+ assertTrue(PlaylistPlaybackPolicy.previousAvailable(1, loop = false, itemCount = 3))
40
+ }
41
+
42
+ @Test
43
+ fun previousAvailable_emptyPlaylist_isFalse() {
44
+ assertFalse(PlaylistPlaybackPolicy.previousAvailable(0, loop = true, itemCount = 0))
45
+ }
46
+
47
+ @Test
48
+ fun naturalItemEnd_isAutoOrRepeatTransitionOnly() {
49
+ assertTrue(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO))
50
+ assertTrue(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT))
51
+ assertFalse(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK))
52
+ assertFalse(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED))
53
+ }
54
+
55
+ @Test
56
+ fun errorSkip_upToThreeSequentialErrorsWhenNextExists() {
57
+ assertTrue(PlaylistPlaybackPolicy.shouldSkipAfterError(1, nextAvailable = true))
58
+ assertTrue(PlaylistPlaybackPolicy.shouldSkipAfterError(3, nextAvailable = true))
59
+ assertFalse(PlaylistPlaybackPolicy.shouldSkipAfterError(4, nextAvailable = true))
60
+ assertFalse(PlaylistPlaybackPolicy.shouldSkipAfterError(1, nextAvailable = false))
61
+ }
62
+
63
+ @Test
64
+ fun errorSkip_keepsPlayingUnlessFirstAutoBufferedItemFailed() {
65
+ assertTrue(PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(playWhenReady = true, failedIndex = 0))
66
+ assertFalse(PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(playWhenReady = false, failedIndex = 0))
67
+ assertTrue(PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(playWhenReady = false, failedIndex = 2))
68
+ }
69
+
70
+ @Test
71
+ fun playRecovery_preparesIdleAndRewindsEnded() {
72
+ assertTrue(PlaylistPlaybackPolicy.needsPrepareBeforePlay(Player.STATE_IDLE))
73
+ assertFalse(PlaylistPlaybackPolicy.needsPrepareBeforePlay(Player.STATE_READY))
74
+ assertTrue(PlaylistPlaybackPolicy.needsRewindBeforePlay(Player.STATE_ENDED))
75
+ assertFalse(PlaylistPlaybackPolicy.needsRewindBeforePlay(Player.STATE_BUFFERING))
76
+ }
77
+
78
+ @Test
79
+ fun liveEdge_onlyForLiveStreamsWithResetEnabled() {
80
+ assertTrue(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(true, isStream = true, isLive = true))
81
+ assertFalse(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(true, isStream = true, isLive = false))
82
+ assertFalse(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(false, isStream = true, isLive = true))
83
+ assertFalse(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(true, isStream = false, isLive = true))
84
+ }
85
+
86
+ @Test
87
+ fun errorSkipWindowIndex_midListAndWrapToStart() {
88
+ assertEquals(2, PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex = 1, itemCount = 5))
89
+ assertEquals(0, PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex = 4, itemCount = 5))
90
+ }
91
+
92
+ @Test
93
+ fun errorSkipWindowIndex_emptyListReturnsZero() {
94
+ assertEquals(0, PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex = 0, itemCount = 0))
95
+ }
96
+
97
+ @Test
98
+ fun nextAvailable_emptyPlaylist_isFalse() {
99
+ assertFalse(PlaylistPlaybackPolicy.nextAvailable(0, itemCount = 0, loop = true))
100
+ }
101
+
102
+ @Test
103
+ fun repeatMode_emptyPlaylist_isOff() {
104
+ assertEquals(Player.REPEAT_MODE_OFF, PlaylistPlaybackPolicy.repeatMode(loop = true, itemCount = 0))
105
+ }
106
+
107
+ @Test
108
+ fun nextSkipIndex_midListAdvancesByOne() {
109
+ assertEquals(2, PlaylistPlaybackPolicy.nextSkipIndex(fromIndex = 1, itemCount = 5, loop = false))
110
+ }
111
+
112
+ @Test
113
+ fun nextSkipIndex_atEnd_wrapsToStartOnlyWhenLooping() {
114
+ assertEquals(3, PlaylistPlaybackPolicy.nextSkipIndex(fromIndex = 2, itemCount = 3, loop = false))
115
+ assertEquals(0, PlaylistPlaybackPolicy.nextSkipIndex(fromIndex = 2, itemCount = 3, loop = true))
116
+ }
117
+
118
+ @Test
119
+ fun previousSkipIndex_midListGoesBackByOne() {
120
+ assertEquals(1, PlaylistPlaybackPolicy.previousSkipIndex(fromIndex = 2, itemCount = 5, loop = false))
121
+ }
122
+
123
+ @Test
124
+ fun previousSkipIndex_atStart_wrapsToLastOnlyWhenLooping() {
125
+ assertEquals(-1, PlaylistPlaybackPolicy.previousSkipIndex(fromIndex = 0, itemCount = 3, loop = false))
126
+ assertEquals(2, PlaylistPlaybackPolicy.previousSkipIndex(fromIndex = 0, itemCount = 3, loop = true))
127
+ }
128
+ }
@@ -0,0 +1,145 @@
1
+ package org.dwbn.plugins.playlist.playlist
2
+
3
+ import androidx.media3.common.Player
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Test
7
+ import java.lang.reflect.Proxy
8
+
9
+ class AudioPlaylistHandlerTest {
10
+
11
+ @Test
12
+ fun pauseForVideoHandoff_setsPlayWhenReadyFalseWhenNotPlaying() {
13
+ val recording = RecordingPlayer(isPlaying = false)
14
+ assertFalse(recording.proxy.isPlaying)
15
+
16
+ AudioPlaylistHandler.pauseForVideoHandoff(recording.proxy)
17
+
18
+ assertEquals(listOf(false), recording.setPlayWhenReadyCalls)
19
+ }
20
+
21
+ @Test
22
+ fun audibleResume_playsBeforeSeekSoAudioIsNotSilent() {
23
+ val recording = RecordingPlayer(isPlaying = false)
24
+
25
+ AudioPlaylistHandler.applyAudibleResume(recording.proxy, 175_000L)
26
+
27
+ assertEquals(listOf("playWhenReady:true", "seek:175000"), recording.calls)
28
+ }
29
+
30
+ @Test
31
+ fun audibleResume_atZero_playsWithoutSeek() {
32
+ val recording = RecordingPlayer(isPlaying = false)
33
+
34
+ AudioPlaylistHandler.applyAudibleResume(recording.proxy, 0L)
35
+
36
+ assertEquals(listOf("playWhenReady:true"), recording.calls)
37
+ }
38
+
39
+ @Test
40
+ fun play_onIdlePlayer_preparesBeforePlaying() {
41
+ val recording = RecordingPlayer(isPlaying = false, playbackState = Player.STATE_IDLE)
42
+
43
+ AudioPlaylistHandler.startAudible(recording.proxy)
44
+
45
+ assertEquals(listOf("prepare", "playWhenReady:true"), recording.calls)
46
+ }
47
+
48
+ @Test
49
+ fun play_onEndedPlayer_rewindsCurrentItemBeforePlaying() {
50
+ val recording = RecordingPlayer(isPlaying = false, playbackState = Player.STATE_ENDED, currentIndex = 2)
51
+
52
+ AudioPlaylistHandler.startAudible(recording.proxy)
53
+
54
+ assertEquals(listOf("seekToDefault:2", "playWhenReady:true"), recording.calls)
55
+ }
56
+
57
+ @Test
58
+ fun play_onReadyPlayer_onlyPlays() {
59
+ val recording = RecordingPlayer(isPlaying = false, playbackState = Player.STATE_READY)
60
+
61
+ AudioPlaylistHandler.startAudible(recording.proxy)
62
+
63
+ assertEquals(listOf("playWhenReady:true"), recording.calls)
64
+ }
65
+
66
+ @Test
67
+ fun resetStreamOnPause_liveStream_resumesAtLiveEdge() {
68
+ val recording = RecordingPlayer(isPlaying = false, isLive = true)
69
+
70
+ AudioPlaylistHandler.jumpToLiveEdgeIfReset(recording.proxy, true, true)
71
+
72
+ assertEquals(listOf("seekToDefault:current"), recording.calls)
73
+ }
74
+
75
+ @Test
76
+ fun resetStreamOnPause_vodHlsLecture_keepsPosition() {
77
+ val recording = RecordingPlayer(isPlaying = false, isLive = false)
78
+
79
+ AudioPlaylistHandler.jumpToLiveEdgeIfReset(recording.proxy, true, true)
80
+
81
+ assertEquals(emptyList<String>(), recording.calls)
82
+ }
83
+
84
+ @Test
85
+ fun resetStreamOnPauseOff_liveStream_keepsPosition() {
86
+ val recording = RecordingPlayer(isPlaying = false, isLive = true)
87
+
88
+ AudioPlaylistHandler.jumpToLiveEdgeIfReset(recording.proxy, false, true)
89
+
90
+ assertEquals(emptyList<String>(), recording.calls)
91
+ }
92
+
93
+ private class RecordingPlayer(
94
+ private val isPlaying: Boolean,
95
+ private val playbackState: Int = Player.STATE_READY,
96
+ private val currentIndex: Int = 0,
97
+ private val isLive: Boolean = false
98
+ ) {
99
+ val setPlayWhenReadyCalls = mutableListOf<Boolean>()
100
+ val calls = mutableListOf<String>()
101
+ val proxy: Player = Proxy.newProxyInstance(
102
+ Player::class.java.classLoader,
103
+ arrayOf(Player::class.java)
104
+ ) { _, method, args ->
105
+ when (method.name) {
106
+ "isPlaying" -> isPlaying
107
+ "getPlaybackState" -> playbackState
108
+ "getCurrentMediaItemIndex" -> currentIndex
109
+ "isCurrentMediaItemLive" -> isLive
110
+ "prepare" -> {
111
+ calls.add("prepare")
112
+ null
113
+ }
114
+ "seekToDefaultPosition" -> {
115
+ calls.add("seekToDefault:" + (args?.getOrNull(0) ?: "current"))
116
+ null
117
+ }
118
+ "setPlayWhenReady" -> {
119
+ val ready = args[0] as Boolean
120
+ setPlayWhenReadyCalls.add(ready)
121
+ calls.add("playWhenReady:$ready")
122
+ null
123
+ }
124
+ "seekTo" -> {
125
+ val pos = when (args.size) {
126
+ 1 -> args[0] as Long
127
+ else -> args[1] as Long
128
+ }
129
+ calls.add("seek:$pos")
130
+ null
131
+ }
132
+ else -> defaultValue(method.returnType)
133
+ }
134
+ } as Player
135
+
136
+ private fun defaultValue(returnType: Class<*>): Any? = when (returnType) {
137
+ java.lang.Boolean.TYPE, java.lang.Boolean::class.java -> false
138
+ java.lang.Integer.TYPE, java.lang.Integer::class.java -> 0
139
+ java.lang.Long.TYPE, java.lang.Long::class.java -> 0L
140
+ java.lang.Float.TYPE, java.lang.Float::class.java -> 0f
141
+ java.lang.Double.TYPE, java.lang.Double::class.java -> 0.0
142
+ else -> null
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,76 @@
1
+ package org.dwbn.plugins.playlist.service
2
+
3
+ import androidx.media3.common.MediaMetadata
4
+ import java.util.concurrent.ExecutionException
5
+ import java.util.concurrent.TimeUnit
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+ import org.robolectric.RuntimeEnvironment
13
+ import org.robolectric.annotation.Config
14
+
15
+ @RunWith(RobolectricTestRunner::class)
16
+ @Config(sdk = [28])
17
+ class GlideBitmapLoaderTest {
18
+
19
+ private val loader: GlideBitmapLoader
20
+ get() = GlideBitmapLoader(RuntimeEnvironment.getApplication())
21
+
22
+ @Test
23
+ fun imageJpeg_isSupportedMimeType() {
24
+ assertTrue(GlideBitmapLoader.isSupportedImageMimeType("image/jpeg"))
25
+ }
26
+
27
+ @Test
28
+ fun imagePng_isSupportedMimeType() {
29
+ assertTrue(GlideBitmapLoader.isSupportedImageMimeType("IMAGE/PNG"))
30
+ }
31
+
32
+ @Test
33
+ fun audioMpeg_isNotSupportedMimeType() {
34
+ assertFalse(GlideBitmapLoader.isSupportedImageMimeType("audio/mpeg"))
35
+ }
36
+
37
+ @Test
38
+ fun nullMimeType_isNotSupported() {
39
+ assertFalse(GlideBitmapLoader.isSupportedImageMimeType(null))
40
+ }
41
+
42
+ @Test
43
+ fun artworkUri_loadsRemoteArtwork() {
44
+ assertTrue(GlideBitmapLoader.shouldLoadRemoteArtwork("https://example.com/art.jpg"))
45
+ }
46
+
47
+ @Test
48
+ fun missingArtwork_doesNotLoadRemote() {
49
+ assertFalse(GlideBitmapLoader.shouldLoadRemoteArtwork(null))
50
+ assertFalse(GlideBitmapLoader.shouldLoadRemoteArtwork(""))
51
+ }
52
+
53
+ @Test
54
+ fun loadBitmapFromMetadata_returnsNullWhenNoArtwork() {
55
+ val metadata = MediaMetadata.Builder().setTitle("Lecture").build()
56
+ assertNull(loader.loadBitmapFromMetadata(metadata))
57
+ }
58
+
59
+ @Test
60
+ fun loadBitmapFromMetadata_returnsNullForEmptyArtworkBytes() {
61
+ val metadata = MediaMetadata.Builder()
62
+ .setArtworkData(ByteArray(0), MediaMetadata.PICTURE_TYPE_FRONT_COVER)
63
+ .build()
64
+ assertNull(loader.loadBitmapFromMetadata(metadata))
65
+ }
66
+
67
+ @Test
68
+ fun decodeBitmap_nonImageBytes_completesWithoutHanging() {
69
+ val future = loader.decodeBitmap(byteArrayOf(1, 2, 3, 4))
70
+ try {
71
+ future.get(2, TimeUnit.SECONDS)
72
+ } catch (e: ExecutionException) {
73
+ assertTrue(e.cause is IllegalStateException)
74
+ }
75
+ }
76
+ }