v2026.06.03 — Media3 like button + Bluetooth/UPnP picker + system playlist daily rotation #77

Merged
bvandeusen merged 29 commits from dev into main 2026-06-03 14:09:23 -04:00
2 changed files with 0 additions and 116 deletions
Showing only changes of commit 43754d03c4 - Show all commits
-13
View File
@@ -80,19 +80,6 @@ android {
buildConfig = true
}
testOptions {
unitTests {
// JVM unit tests run against android.jar's stub methods,
// which throw `RuntimeException("Method ... not mocked")`
// when touched (including statics like Bundle.EMPTY).
// Return defaults instead so tests that pass Bundles
// through without inspecting them (e.g. SessionCommand
// construction in LikeMediaCallbackTest) just work,
// without dragging in Robolectric.
isReturnDefaultValues = true
}
}
packaging {
resources.excludes +=
setOf(
@@ -1,103 +0,0 @@
package com.fabledsword.minstrel.player
import android.os.Bundle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.likes.data.LikesRepository.Companion.ENTITY_TRACK
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class)
class LikeMediaCallbackTest {
private val scope = TestScope(UnconfinedTestDispatcher())
private val likes = mockk<LikesRepository>(relaxed = true)
private val session = mockk<MediaSession>()
private val controllerInfo = mockk<MediaSession.ControllerInfo>()
private val callback = LikeMediaCallback(likes, scope)
@Test
fun `onConnect adds CMD_TOGGLE_LIKE to available session commands`() {
val result = callback.onConnect(session, controllerInfo)
assertTrue(
result.availableSessionCommands.contains(
SessionCommand(LikeMediaCallback.CMD_TOGGLE_LIKE, Bundle.EMPTY),
),
)
}
@Test
fun `onCustomCommand routes toggle when current track has a mediaId`() {
val item = MediaItem.Builder().setMediaId("track-abc").build()
val player = mockk<Player> { every { currentMediaItem } returns item }
every { session.player } returns player
coEvery { likes.observeIsLiked(ENTITY_TRACK, "track-abc") } returns flowOf(false)
val future = callback.onCustomCommand(
session,
controllerInfo,
SessionCommand(LikeMediaCallback.CMD_TOGGLE_LIKE, Bundle.EMPTY),
Bundle.EMPTY,
)
assertEquals(SessionResult.RESULT_SUCCESS, future.get().resultCode)
coVerify(exactly = 1) { likes.toggleLike(ENTITY_TRACK, "track-abc", true) }
}
@Test
fun `onCustomCommand inverts current liked state`() {
val item = MediaItem.Builder().setMediaId("track-xyz").build()
val player = mockk<Player> { every { currentMediaItem } returns item }
every { session.player } returns player
coEvery { likes.observeIsLiked(ENTITY_TRACK, "track-xyz") } returns flowOf(true)
callback.onCustomCommand(
session,
controllerInfo,
SessionCommand(LikeMediaCallback.CMD_TOGGLE_LIKE, Bundle.EMPTY),
Bundle.EMPTY,
)
coVerify(exactly = 1) { likes.toggleLike(ENTITY_TRACK, "track-xyz", false) }
}
@Test
fun `onCustomCommand is no-op when no currentMediaItem`() {
val player = mockk<Player> { every { currentMediaItem } returns null }
every { session.player } returns player
val future = callback.onCustomCommand(
session,
controllerInfo,
SessionCommand(LikeMediaCallback.CMD_TOGGLE_LIKE, Bundle.EMPTY),
Bundle.EMPTY,
)
assertEquals(SessionResult.RESULT_INFO_SKIPPED, future.get().resultCode)
coVerify(exactly = 0) { likes.toggleLike(any(), any(), any()) }
}
@Test
fun `onCustomCommand rejects unknown actions`() {
val future = callback.onCustomCommand(
session,
controllerInfo,
SessionCommand("garbage", Bundle.EMPTY),
Bundle.EMPTY,
)
assertEquals(SessionResult.RESULT_ERROR_NOT_SUPPORTED, future.get().resultCode)
}
}