capacitor-plugin-playlist 0.11.2 → 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.
- package/README.md +133 -127
- package/android/build.gradle +31 -18
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/java/org/dwbn/plugins/playlist/AudioMediaItemFactory.java +96 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +3 -3
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +145 -188
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxPlaybackErrorMapper.kt +26 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +28 -28
- package/android/src/main/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridge.kt +102 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaybackProgress.kt +8 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +418 -183
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicy.kt +98 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/RmxPlaybackState.kt +11 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +107 -211
- package/android/src/main/java/org/dwbn/plugins/playlist/service/GlideBitmapLoader.kt +121 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayer.kt +185 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicy.kt +253 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +260 -68
- package/android/src/main/java/org/dwbn/plugins/playlist/service/PlaylistMediaSessionCallback.kt +39 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryMimeTest.kt +98 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryTest.java +71 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/data/AudioTrackTest.kt +53 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridgeTest.kt +84 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicyTest.kt +128 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandlerTest.kt +145 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/GlideBitmapLoaderTest.kt +76 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayerTest.kt +282 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicyTest.kt +285 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationCommandsTest.kt +54 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationPolicyTest.kt +47 -0
- package/dist/docs.json +12 -846
- package/dist/esm/RmxAudioPlayer.d.ts +2 -2
- package/dist/esm/RmxAudioPlayer.js.map +1 -1
- package/dist/esm/definitions.d.ts +7 -7
- package/dist/esm/interfaces.d.ts +7 -1
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.js +2 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/web.d.ts +3 -3
- package/dist/esm/web.js +5 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +7 -7
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +7 -7
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/PlaylistPlugin/AVBidirectionalQueuePlayer.swift +45 -6
- package/ios/Sources/PlaylistPlugin/AudioTrack.swift +15 -2
- package/ios/Tests/PlaylistPluginTests/PositionResumeTests.swift +82 -0
- package/package.json +15 -19
- package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +0 -26
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +0 -114
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +0 -36
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +0 -83
package/android/src/test/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayerTest.kt
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.service
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.Player
|
|
4
|
+
import org.dwbn.plugins.playlist.handoff.VideoPlayerBridge
|
|
5
|
+
import org.junit.After
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertFalse
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
import java.lang.reflect.Proxy
|
|
11
|
+
|
|
12
|
+
class HandoffForwardingPlayerTest {
|
|
13
|
+
|
|
14
|
+
@After
|
|
15
|
+
fun tearDown() {
|
|
16
|
+
VideoPlayerBridge.clearForTests()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun retain_playAndSetPlayWhenReadyAreNoOps() {
|
|
21
|
+
val recording = RecordingPlayer()
|
|
22
|
+
val forwarding = HandoffForwardingPlayer(recording.proxy, retain = { true })
|
|
23
|
+
|
|
24
|
+
forwarding.play()
|
|
25
|
+
forwarding.setPlayWhenReady(true)
|
|
26
|
+
|
|
27
|
+
assertEquals(0, recording.playCount)
|
|
28
|
+
assertTrue(recording.setPlayWhenReadyCalls.isEmpty())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun afterRetainCleared_playReachesPlayer() {
|
|
33
|
+
var retain = true
|
|
34
|
+
val recording = RecordingPlayer()
|
|
35
|
+
val forwarding = HandoffForwardingPlayer(recording.proxy, retain = { retain })
|
|
36
|
+
|
|
37
|
+
forwarding.setPlayWhenReady(true)
|
|
38
|
+
assertTrue(recording.setPlayWhenReadyCalls.isEmpty())
|
|
39
|
+
|
|
40
|
+
retain = false
|
|
41
|
+
forwarding.setPlayWhenReady(true)
|
|
42
|
+
|
|
43
|
+
assertEquals(listOf(true), recording.setPlayWhenReadyCalls)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun seekToNextMediaItem_delegatesToCallback() {
|
|
48
|
+
var skipNextCount = 0
|
|
49
|
+
val recording = RecordingPlayer()
|
|
50
|
+
val forwarding = HandoffForwardingPlayer(
|
|
51
|
+
recording.proxy,
|
|
52
|
+
retain = { false },
|
|
53
|
+
onSkipToNext = { skipNextCount++ }
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
forwarding.seekToNextMediaItem()
|
|
57
|
+
|
|
58
|
+
assertEquals(1, skipNextCount)
|
|
59
|
+
assertEquals(0, recording.seekToNextCount)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun seekToPreviousMediaItem_delegatesToCallback() {
|
|
64
|
+
var skipPreviousCount = 0
|
|
65
|
+
val recording = RecordingPlayer()
|
|
66
|
+
val forwarding = HandoffForwardingPlayer(
|
|
67
|
+
recording.proxy,
|
|
68
|
+
retain = { false },
|
|
69
|
+
onSkipToPrevious = { skipPreviousCount++ }
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
forwarding.seekToPreviousMediaItem()
|
|
73
|
+
|
|
74
|
+
assertEquals(1, skipPreviousCount)
|
|
75
|
+
assertEquals(0, recording.seekToPreviousCount)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test
|
|
79
|
+
fun retainWithVideoAttached_playReachesVideoPlayer() {
|
|
80
|
+
val audio = RecordingPlayer()
|
|
81
|
+
val video = RecordingPlayer()
|
|
82
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
83
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
84
|
+
|
|
85
|
+
forwarding.play()
|
|
86
|
+
|
|
87
|
+
assertEquals(0, audio.playCount)
|
|
88
|
+
assertEquals(1, video.playCount)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
fun retainWithVideoAttached_isPlayingReflectsVideo() {
|
|
93
|
+
val audio = RecordingPlayer()
|
|
94
|
+
val video = RecordingPlayer(playWhenReady = true, playbackState = Player.STATE_READY)
|
|
95
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
96
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
97
|
+
|
|
98
|
+
assertTrue(forwarding.isPlaying())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test
|
|
102
|
+
fun retainWithVideoAttached_notPlayingWhileBufferingEvenIfPlayWhenReady() {
|
|
103
|
+
val audio = RecordingPlayer()
|
|
104
|
+
val video = RecordingPlayer(playWhenReady = true, playbackState = Player.STATE_BUFFERING)
|
|
105
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
106
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
107
|
+
|
|
108
|
+
assertFalse(forwarding.isPlaying())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test
|
|
112
|
+
fun retainWithVideoAttached_pauseAndSeekHitVideo() {
|
|
113
|
+
val audio = RecordingPlayer()
|
|
114
|
+
val video = RecordingPlayer()
|
|
115
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
116
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
117
|
+
|
|
118
|
+
forwarding.pause()
|
|
119
|
+
forwarding.setPlayWhenReady(false)
|
|
120
|
+
forwarding.seekTo(99_000L)
|
|
121
|
+
|
|
122
|
+
assertEquals(1, video.pauseCount)
|
|
123
|
+
assertEquals(listOf(false), video.setPlayWhenReadyCalls)
|
|
124
|
+
assertEquals(listOf(99_000L), video.seekToPositionMsCalls)
|
|
125
|
+
assertEquals(0, audio.pauseCount)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun retainWithVideoAttached_positionAndDurationFromVideo() {
|
|
130
|
+
val audio = RecordingPlayer(currentPosition = 1L, duration = 2L, contentPosition = 3L)
|
|
131
|
+
val video = RecordingPlayer(currentPosition = 50L, duration = 100L, contentPosition = 55L)
|
|
132
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
133
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
134
|
+
|
|
135
|
+
assertEquals(50L, forwarding.getCurrentPosition())
|
|
136
|
+
assertEquals(100L, forwarding.getDuration())
|
|
137
|
+
assertEquals(55L, forwarding.getContentPosition())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun retainWithVideoAttached_skipMediaItemsAreNoOps() {
|
|
142
|
+
var skipNextCount = 0
|
|
143
|
+
var skipPreviousCount = 0
|
|
144
|
+
val audio = RecordingPlayer()
|
|
145
|
+
val video = RecordingPlayer()
|
|
146
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
147
|
+
val forwarding = HandoffForwardingPlayer(
|
|
148
|
+
audio.proxy,
|
|
149
|
+
retain = { true },
|
|
150
|
+
onSkipToNext = { skipNextCount++ },
|
|
151
|
+
onSkipToPrevious = { skipPreviousCount++ }
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
forwarding.seekToNextMediaItem()
|
|
155
|
+
forwarding.seekToPreviousMediaItem()
|
|
156
|
+
|
|
157
|
+
assertEquals(0, skipNextCount)
|
|
158
|
+
assertEquals(0, skipPreviousCount)
|
|
159
|
+
assertEquals(0, video.seekToNextCount)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Test
|
|
163
|
+
fun afterVideoDetach_skipCallbacksWorkAgainWhileRetainStillOn() {
|
|
164
|
+
var skipNextCount = 0
|
|
165
|
+
val audio = RecordingPlayer()
|
|
166
|
+
val video = RecordingPlayer()
|
|
167
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
168
|
+
val forwarding = HandoffForwardingPlayer(
|
|
169
|
+
audio.proxy,
|
|
170
|
+
retain = { true },
|
|
171
|
+
onSkipToNext = { skipNextCount++ }
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
forwarding.seekToNextMediaItem()
|
|
175
|
+
assertEquals(0, skipNextCount)
|
|
176
|
+
|
|
177
|
+
VideoPlayerBridge.detach(video.proxy)
|
|
178
|
+
forwarding.seekToNextMediaItem()
|
|
179
|
+
assertEquals(1, skipNextCount)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Test
|
|
183
|
+
fun afterVideoDetach_playStillIgnoredWhileRetainOn() {
|
|
184
|
+
val audio = RecordingPlayer()
|
|
185
|
+
val video = RecordingPlayer()
|
|
186
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
187
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
188
|
+
|
|
189
|
+
VideoPlayerBridge.detach(video.proxy)
|
|
190
|
+
forwarding.play()
|
|
191
|
+
|
|
192
|
+
assertEquals(0, audio.playCount)
|
|
193
|
+
assertEquals(0, video.playCount)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Test
|
|
197
|
+
fun retainWithVideoAttached_notifiesSessionWhenVideoAttaches() {
|
|
198
|
+
val audio = RecordingPlayer()
|
|
199
|
+
val video = RecordingPlayer(playWhenReady = true, playbackState = Player.STATE_READY)
|
|
200
|
+
val forwarding = HandoffForwardingPlayer(audio.proxy, retain = { true })
|
|
201
|
+
var playingUpdates = 0
|
|
202
|
+
forwarding.addListener(object : Player.Listener {
|
|
203
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
204
|
+
if (isPlaying) {
|
|
205
|
+
playingUpdates++
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
VideoPlayerBridge.attach(video.proxy)
|
|
211
|
+
|
|
212
|
+
assertTrue(playingUpdates > 0)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private class RecordingPlayer(
|
|
216
|
+
private var playWhenReady: Boolean = false,
|
|
217
|
+
private val playbackState: Int = Player.STATE_IDLE,
|
|
218
|
+
private val isPlayingValue: Boolean = false,
|
|
219
|
+
private val currentPosition: Long = 0L,
|
|
220
|
+
private val duration: Long = 0L,
|
|
221
|
+
private val contentPosition: Long = 0L
|
|
222
|
+
) {
|
|
223
|
+
var playCount = 0
|
|
224
|
+
var pauseCount = 0
|
|
225
|
+
var seekToNextCount = 0
|
|
226
|
+
var seekToPreviousCount = 0
|
|
227
|
+
val seekToPositionMsCalls = mutableListOf<Long>()
|
|
228
|
+
val setPlayWhenReadyCalls = mutableListOf<Boolean>()
|
|
229
|
+
val proxy: Player = Proxy.newProxyInstance(
|
|
230
|
+
Player::class.java.classLoader,
|
|
231
|
+
arrayOf(Player::class.java)
|
|
232
|
+
) { _, method, args ->
|
|
233
|
+
when (method.name) {
|
|
234
|
+
"play" -> {
|
|
235
|
+
playCount++
|
|
236
|
+
null
|
|
237
|
+
}
|
|
238
|
+
"pause" -> {
|
|
239
|
+
pauseCount++
|
|
240
|
+
null
|
|
241
|
+
}
|
|
242
|
+
"setPlayWhenReady" -> {
|
|
243
|
+
val ready = args[0] as Boolean
|
|
244
|
+
playWhenReady = ready
|
|
245
|
+
setPlayWhenReadyCalls.add(ready)
|
|
246
|
+
null
|
|
247
|
+
}
|
|
248
|
+
"seekTo" -> when (args?.size) {
|
|
249
|
+
1 -> {
|
|
250
|
+
seekToPositionMsCalls.add(args[0] as Long)
|
|
251
|
+
null
|
|
252
|
+
}
|
|
253
|
+
else -> defaultValue(method.returnType)
|
|
254
|
+
}
|
|
255
|
+
"seekToNextMediaItem" -> {
|
|
256
|
+
seekToNextCount++
|
|
257
|
+
null
|
|
258
|
+
}
|
|
259
|
+
"seekToPreviousMediaItem" -> {
|
|
260
|
+
seekToPreviousCount++
|
|
261
|
+
null
|
|
262
|
+
}
|
|
263
|
+
"isPlaying", "getIsPlaying" -> isPlayingValue
|
|
264
|
+
"isPlayWhenReady", "getPlayWhenReady" -> playWhenReady
|
|
265
|
+
"getPlaybackState" -> playbackState
|
|
266
|
+
"getCurrentPosition" -> currentPosition
|
|
267
|
+
"getDuration" -> duration
|
|
268
|
+
"getContentPosition" -> contentPosition
|
|
269
|
+
else -> defaultValue(method.returnType)
|
|
270
|
+
}
|
|
271
|
+
} as Player
|
|
272
|
+
|
|
273
|
+
private fun defaultValue(returnType: Class<*>): Any? = when (returnType) {
|
|
274
|
+
java.lang.Boolean.TYPE, java.lang.Boolean::class.java -> false
|
|
275
|
+
java.lang.Integer.TYPE, java.lang.Integer::class.java -> 0
|
|
276
|
+
java.lang.Long.TYPE, java.lang.Long::class.java -> 0L
|
|
277
|
+
java.lang.Float.TYPE, java.lang.Float::class.java -> 0f
|
|
278
|
+
java.lang.Double.TYPE, java.lang.Double::class.java -> 0.0
|
|
279
|
+
else -> null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
package/android/src/test/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicyTest.kt
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.service
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import androidx.media3.session.DefaultMediaNotificationProvider
|
|
5
|
+
import androidx.media3.session.MediaSessionService
|
|
6
|
+
import org.dwbn.plugins.playlist.manager.PlaylistManager
|
|
7
|
+
import org.dwbn.plugins.playlist.playlist.AudioPlaylistHandler
|
|
8
|
+
import org.junit.Assert.assertEquals
|
|
9
|
+
import org.junit.Assert.assertFalse
|
|
10
|
+
import org.junit.Assert.assertTrue
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
|
|
13
|
+
class MediaNotificationPolicyTest {
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun backgroundPlay_usesDefaultMediaNotificationProviderOnMediaSessionService() {
|
|
17
|
+
assertEquals(
|
|
18
|
+
DefaultMediaNotificationProvider::class.java,
|
|
19
|
+
MediaNotificationPolicy.mediaNotificationProviderClass
|
|
20
|
+
)
|
|
21
|
+
assertTrue(MediaSessionService::class.java.isAssignableFrom(MediaService::class.java))
|
|
22
|
+
assertFalse(MediaNotificationPolicy.shouldStartForegroundOnPromote())
|
|
23
|
+
assertFalse(MediaNotificationPolicy.ALLOW_START_FOREGROUND_ON_PROMOTE)
|
|
24
|
+
assertEquals(
|
|
25
|
+
MediaSessionService.SHOW_NOTIFICATION_FOR_IDLE_PLAYER_ALWAYS,
|
|
26
|
+
MediaNotificationPolicy.SHOW_NOTIFICATION_WHEN_IDLE
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun artworkPresent_loadsRemoteArtwork() {
|
|
32
|
+
val uri = "https://example.com/art.jpg"
|
|
33
|
+
assertTrue(MediaNotificationPolicy.shouldLoadRemoteArtwork(uri))
|
|
34
|
+
assertFalse(MediaNotificationPolicy.hasNoLargeArtwork(null, uri))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun artworkMissing_hasNoLargeArt() {
|
|
39
|
+
assertTrue(MediaNotificationPolicy.hasNoLargeArtwork(null, null))
|
|
40
|
+
assertTrue(MediaNotificationPolicy.hasNoLargeArtwork(null, ""))
|
|
41
|
+
assertTrue(MediaNotificationPolicy.hasNoLargeArtwork(ByteArray(0), null))
|
|
42
|
+
assertFalse(MediaNotificationPolicy.hasNoLargeArtwork(ByteArray(0), "https://example.com/art.jpg"))
|
|
43
|
+
assertFalse(MediaNotificationPolicy.shouldLoadRemoteArtwork(null))
|
|
44
|
+
assertFalse(MediaNotificationPolicy.shouldLoadRemoteArtwork(""))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun focusLoss_media3HandlesFocusWithoutAudioManagerApi() {
|
|
49
|
+
assertTrue(MediaNotificationPolicy.HANDLE_AUDIO_FOCUS)
|
|
50
|
+
assertFalse(MediaNotificationPolicy.shouldRequestLegacyAudioFocus())
|
|
51
|
+
val managerMethods = PlaylistManager::class.java.methods.map { it.name }
|
|
52
|
+
assertFalse(managerMethods.contains("requestAudioFocus"))
|
|
53
|
+
assertFalse(managerMethods.contains("abandonAudioFocus"))
|
|
54
|
+
val handlerMethods = AudioPlaylistHandler::class.java.methods.map { it.name }
|
|
55
|
+
assertFalse(handlerMethods.contains("requestAudioFocus"))
|
|
56
|
+
assertFalse(handlerMethods.contains("abandonAudioFocus"))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun notificationTap_usesHostLaunchReorderFlags() {
|
|
61
|
+
val flags = MediaNotificationPolicy.sessionLaunchIntentFlags()
|
|
62
|
+
assertEquals(
|
|
63
|
+
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP,
|
|
64
|
+
flags
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun handoffRetain_skipsEndForeground() {
|
|
70
|
+
assertTrue(MediaNotificationPolicy.shouldSkipEndForeground(true))
|
|
71
|
+
assertFalse(MediaNotificationPolicy.shouldSkipEndForeground(false))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
fun prepareForVideoHandoff_pausesRegardlessOfIsPlaying() {
|
|
76
|
+
assertTrue(MediaNotificationPolicy.shouldPauseForVideoHandoff(true))
|
|
77
|
+
assertTrue(MediaNotificationPolicy.shouldPauseForVideoHandoff(false))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test
|
|
81
|
+
fun foregroundStartFailure_securityExceptionIsRecoverable() {
|
|
82
|
+
assertTrue(MediaNotificationPolicy.isForegroundStartSecurityBlocked(SecurityException("denied")))
|
|
83
|
+
assertFalse(MediaNotificationPolicy.isForegroundStartSecurityBlocked(IllegalStateException()))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Test
|
|
87
|
+
fun prepareForVideoHandoff_retainsForeground() {
|
|
88
|
+
val retainCalls = mutableListOf<Boolean>()
|
|
89
|
+
MediaNotificationPolicy.applyForegroundRetainOnPrepare { retainCalls.add(true) }
|
|
90
|
+
assertEquals(listOf(true), retainCalls)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
fun retain_forcesNotificationForegroundPastMedia3Timeout() {
|
|
95
|
+
assertTrue(MediaNotificationPolicy.shouldForceNotificationForeground(true))
|
|
96
|
+
assertFalse(MediaNotificationPolicy.shouldForceNotificationForeground(false))
|
|
97
|
+
assertTrue(
|
|
98
|
+
MediaNotificationPolicy.startInForegroundRequired(
|
|
99
|
+
videoHandoffForegroundRetain = true,
|
|
100
|
+
media3Requested = false
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
assertTrue(
|
|
104
|
+
MediaNotificationPolicy.startInForegroundRequired(
|
|
105
|
+
videoHandoffForegroundRetain = true,
|
|
106
|
+
media3Requested = true
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
assertFalse(
|
|
110
|
+
MediaNotificationPolicy.startInForegroundRequired(
|
|
111
|
+
videoHandoffForegroundRetain = false,
|
|
112
|
+
media3Requested = false
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
assertTrue(
|
|
116
|
+
MediaNotificationPolicy.startInForegroundRequired(
|
|
117
|
+
videoHandoffForegroundRetain = false,
|
|
118
|
+
media3Requested = true
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
assertTrue(MediaService.resolveHandoffNotificationForeground(true, false))
|
|
122
|
+
assertFalse(MediaService.resolveHandoffNotificationForeground(false, false))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Test
|
|
126
|
+
fun retain_playWhenReadyTrue_forcesPause() {
|
|
127
|
+
val pauses = mutableListOf<Boolean>()
|
|
128
|
+
MediaNotificationPolicy.applyRetainPlayWhenReadyGuard(
|
|
129
|
+
playWhenReady = true,
|
|
130
|
+
videoHandoffForegroundRetain = true,
|
|
131
|
+
videoHandoffPlayerAttached = false
|
|
132
|
+
) { pauses.add(true) }
|
|
133
|
+
assertEquals(listOf(true), pauses)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
fun retain_playWhenReadyFalse_doesNotForcePause() {
|
|
138
|
+
val pauses = mutableListOf<Boolean>()
|
|
139
|
+
MediaNotificationPolicy.applyRetainPlayWhenReadyGuard(
|
|
140
|
+
playWhenReady = false,
|
|
141
|
+
videoHandoffForegroundRetain = true,
|
|
142
|
+
videoHandoffPlayerAttached = false
|
|
143
|
+
) { pauses.add(true) }
|
|
144
|
+
assertTrue(pauses.isEmpty())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@Test
|
|
148
|
+
fun retainWithVideoAttached_playWhenReadyTrue_doesNotForcePause() {
|
|
149
|
+
val pauses = mutableListOf<Boolean>()
|
|
150
|
+
MediaNotificationPolicy.applyRetainPlayWhenReadyGuard(
|
|
151
|
+
playWhenReady = true,
|
|
152
|
+
videoHandoffForegroundRetain = true,
|
|
153
|
+
videoHandoffPlayerAttached = true
|
|
154
|
+
) { pauses.add(true) }
|
|
155
|
+
assertTrue(pauses.isEmpty())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Test
|
|
159
|
+
fun retain_ignoresSessionPlay() {
|
|
160
|
+
assertTrue(MediaNotificationPolicy.shouldIgnoreSessionPlay(true))
|
|
161
|
+
assertFalse(MediaNotificationPolicy.shouldIgnoreSessionPlay(false))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Test
|
|
165
|
+
fun audibleResume_playsThenSeeks() {
|
|
166
|
+
assertTrue(MediaNotificationPolicy.shouldPlayThenSeekOnResume())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
fun lastKnownPosition_isHandoffSnapshot() {
|
|
171
|
+
assertEquals(42.5f, MediaNotificationPolicy.lastKnownPositionSec(42.5f), 0f)
|
|
172
|
+
assertEquals(0f, MediaNotificationPolicy.lastKnownPositionSec(0f), 0f)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Test
|
|
176
|
+
fun resume_reportsInPlaceOnlyWhenForegroundUp() {
|
|
177
|
+
assertTrue(
|
|
178
|
+
MediaNotificationPolicy.shouldReportInPlaceResumed(
|
|
179
|
+
prewarm = false,
|
|
180
|
+
play = true,
|
|
181
|
+
retain = true,
|
|
182
|
+
serviceInForeground = true
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
assertFalse(
|
|
186
|
+
MediaNotificationPolicy.shouldReportInPlaceResumed(
|
|
187
|
+
prewarm = true,
|
|
188
|
+
play = true,
|
|
189
|
+
retain = true,
|
|
190
|
+
serviceInForeground = true
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
assertFalse(
|
|
194
|
+
MediaNotificationPolicy.shouldReportInPlaceResumed(
|
|
195
|
+
prewarm = false,
|
|
196
|
+
play = true,
|
|
197
|
+
retain = true,
|
|
198
|
+
serviceInForeground = false
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
assertFalse(
|
|
202
|
+
MediaNotificationPolicy.shouldReportInPlaceResumed(
|
|
203
|
+
prewarm = false,
|
|
204
|
+
play = false,
|
|
205
|
+
retain = true,
|
|
206
|
+
serviceInForeground = true
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Test
|
|
212
|
+
fun failedInPlaceResume_doesNotBeginPlayback() {
|
|
213
|
+
val beginPlaybackCalls = mutableListOf<String>()
|
|
214
|
+
MediaNotificationPolicy.applyBeginPlaybackWhenInPlaceUnavailable {
|
|
215
|
+
beginPlaybackCalls.add("beginPlayback")
|
|
216
|
+
}
|
|
217
|
+
assertTrue(beginPlaybackCalls.isEmpty())
|
|
218
|
+
assertFalse(MediaNotificationPolicy.shouldBeginPlaybackWhenInPlaceUnavailable())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
fun pausedVideoExit_doesNotClaimInPlaceResume() {
|
|
223
|
+
assertFalse(
|
|
224
|
+
MediaNotificationPolicy.shouldReportInPlaceResumed(
|
|
225
|
+
prewarm = false,
|
|
226
|
+
play = false,
|
|
227
|
+
retain = true,
|
|
228
|
+
serviceInForeground = true
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
assertFalse(MediaNotificationPolicy.shouldBeginPlaybackWhenInPlaceUnavailable())
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Test
|
|
235
|
+
fun retainMustClearBeforeAudiblePlay() {
|
|
236
|
+
assertTrue(MediaNotificationPolicy.shouldIgnoreSessionPlay(true))
|
|
237
|
+
assertFalse(MediaNotificationPolicy.shouldIgnoreSessionPlay(false))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Test
|
|
241
|
+
fun mediaSessionId_isDistinctFromEmptyDefault() {
|
|
242
|
+
assertEquals("org.dwbn.playlist", MediaNotificationPolicy.MEDIA_SESSION_ID)
|
|
243
|
+
assertFalse(MediaNotificationPolicy.MEDIA_SESSION_ID.isEmpty())
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Test
|
|
247
|
+
fun ensureService_skipsWhenAlreadyCreated() {
|
|
248
|
+
assertFalse(MediaNotificationPolicy.shouldStartForegroundService(true))
|
|
249
|
+
assertTrue(MediaNotificationPolicy.shouldStartForegroundService(false))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@Test
|
|
253
|
+
fun mediaNotificationSkipFlags_followPlaylistAvailability() {
|
|
254
|
+
assertFalse(MediaNotificationPolicy.mediaNotificationSkipPreviousEnabled(false))
|
|
255
|
+
assertTrue(MediaNotificationPolicy.mediaNotificationSkipPreviousEnabled(true))
|
|
256
|
+
assertFalse(MediaNotificationPolicy.mediaNotificationSkipNextEnabled(false))
|
|
257
|
+
assertTrue(MediaNotificationPolicy.mediaNotificationSkipNextEnabled(true))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@Test
|
|
261
|
+
fun onStartCommand_doesNotOverwriteMedia3Notification() {
|
|
262
|
+
assertFalse(MediaNotificationPolicy.shouldOverwriteMedia3NotificationOnStartCommand())
|
|
263
|
+
assertFalse(MediaNotificationPolicy.shouldAllowTitleOnlyNotificationAfterSession())
|
|
264
|
+
val methodNames = MediaService::class.java.declaredMethods.map { it.name }
|
|
265
|
+
assertTrue(methodNames.contains("startForegroundImmediately"))
|
|
266
|
+
assertTrue(methodNames.contains("startForegroundWithMedia3Notification"))
|
|
267
|
+
assertTrue(methodNames.contains("onUpdateNotificationAsync"))
|
|
268
|
+
assertTrue(methodNames.contains("onStartCommand"))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Test
|
|
272
|
+
fun playback_holdsNetworkWakeLockLikePlaylistCore() {
|
|
273
|
+
assertEquals(androidx.media3.common.C.WAKE_MODE_NETWORK, MediaNotificationPolicy.WAKE_MODE)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@Test
|
|
277
|
+
fun smallIcon_usesAppDrawableFromOptions() {
|
|
278
|
+
assertEquals(0x7f080001, MediaNotificationPolicy.smallIconRes(0x7f080001))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@Test
|
|
282
|
+
fun smallIcon_missingDrawable_fallsBackToPlatformPlayGlyph() {
|
|
283
|
+
assertEquals(android.R.drawable.ic_media_play, MediaNotificationPolicy.smallIconRes(0))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.service
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.Player
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.runner.RunWith
|
|
8
|
+
import org.robolectric.RobolectricTestRunner
|
|
9
|
+
import org.robolectric.annotation.Config
|
|
10
|
+
|
|
11
|
+
@RunWith(RobolectricTestRunner::class)
|
|
12
|
+
@Config(sdk = [28])
|
|
13
|
+
class VideoHandoffNotificationCommandsTest {
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun videoHandoff_playerCommandsHideSkipButKeepPlayPause() {
|
|
17
|
+
val commands = MediaNotificationPolicy.playerCommandsForMediaNotification(
|
|
18
|
+
Player.Commands.EMPTY,
|
|
19
|
+
MediaNotificationPolicy.mediaNotificationSkipPreviousEnabled(
|
|
20
|
+
previousAvailable = true,
|
|
21
|
+
videoHandoffForegroundRetain = true,
|
|
22
|
+
videoHandoffPlayerAttached = true
|
|
23
|
+
),
|
|
24
|
+
MediaNotificationPolicy.mediaNotificationSkipNextEnabled(
|
|
25
|
+
nextAvailable = true,
|
|
26
|
+
videoHandoffForegroundRetain = true,
|
|
27
|
+
videoHandoffPlayerAttached = true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
assertTrue(commands.contains(Player.COMMAND_PLAY_PAUSE))
|
|
31
|
+
assertFalse(commands.contains(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM))
|
|
32
|
+
assertFalse(commands.contains(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun retainWithoutVideo_playerCommandsExposeSkipWhenAvailable() {
|
|
37
|
+
val commands = MediaNotificationPolicy.playerCommandsForMediaNotification(
|
|
38
|
+
Player.Commands.EMPTY,
|
|
39
|
+
MediaNotificationPolicy.mediaNotificationSkipPreviousEnabled(
|
|
40
|
+
previousAvailable = true,
|
|
41
|
+
videoHandoffForegroundRetain = true,
|
|
42
|
+
videoHandoffPlayerAttached = false
|
|
43
|
+
),
|
|
44
|
+
MediaNotificationPolicy.mediaNotificationSkipNextEnabled(
|
|
45
|
+
nextAvailable = true,
|
|
46
|
+
videoHandoffForegroundRetain = true,
|
|
47
|
+
videoHandoffPlayerAttached = false
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
assertTrue(commands.contains(Player.COMMAND_PLAY_PAUSE))
|
|
51
|
+
assertTrue(commands.contains(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM))
|
|
52
|
+
assertTrue(commands.contains(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.service
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class VideoHandoffNotificationPolicyTest {
|
|
8
|
+
|
|
9
|
+
@Test
|
|
10
|
+
fun retainWithoutVideo_stillIgnoresSessionPlay() {
|
|
11
|
+
assertTrue(MediaNotificationPolicy.shouldIgnoreSessionPlay(true, videoHandoffPlayerAttached = false))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Test
|
|
15
|
+
fun retainWithVideo_doesNotIgnoreSessionPlay() {
|
|
16
|
+
assertFalse(MediaNotificationPolicy.shouldIgnoreSessionPlay(true, videoHandoffPlayerAttached = true))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun videoHandoff_hidesSkipCommandsEvenWhenPlaylistWouldAllow() {
|
|
21
|
+
assertFalse(
|
|
22
|
+
MediaNotificationPolicy.mediaNotificationSkipNextEnabled(
|
|
23
|
+
nextAvailable = true,
|
|
24
|
+
videoHandoffForegroundRetain = true,
|
|
25
|
+
videoHandoffPlayerAttached = true
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
assertFalse(
|
|
29
|
+
MediaNotificationPolicy.mediaNotificationSkipPreviousEnabled(
|
|
30
|
+
previousAvailable = true,
|
|
31
|
+
videoHandoffForegroundRetain = true,
|
|
32
|
+
videoHandoffPlayerAttached = true
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun playlistSkipRestoredWhenVideoDetached() {
|
|
39
|
+
assertTrue(
|
|
40
|
+
MediaNotificationPolicy.mediaNotificationSkipNextEnabled(
|
|
41
|
+
nextAvailable = true,
|
|
42
|
+
videoHandoffForegroundRetain = true,
|
|
43
|
+
videoHandoffPlayerAttached = false
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|