feat(android): output picker foundation - mediarouter + OutputRoute
android / Build + lint + test (push) Has been cancelled
android / Build + lint + test (push) Has been cancelled
Bluetooth slice (1/5). Adds the androidx.mediarouter 1.7.0 dep, declares BLUETOOTH_CONNECT (needed on Android 12+ to enumerate paired BT devices by name), and lays down the OutputRoute domain model. OutputRoute decouples the picker UI from MediaRouter.RouteInfo (framework class, can't be constructed in JVM tests - same constraint we hit with LikeMediaCallback). The Protocol enum includes UPNP/CAST/SONOS placeholders so the next slice slots in without a data-model rename - see docs/superpowers/specs/2026-06-03-android-output-picker-upnp-scope.md for the deferred work. Controller + ViewModel + Compose UI land in follow-up commits.
This commit is contained in:
@@ -173,6 +173,7 @@ dependencies {
|
||||
implementation(libs.media3.exoplayer)
|
||||
implementation(libs.media3.session)
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.mediarouter)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<application
|
||||
android:name=".MinstrelApplication"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.fabledsword.minstrel.player.output
|
||||
|
||||
import androidx.mediarouter.media.MediaRouter
|
||||
|
||||
/**
|
||||
* Narrow domain model for an audio output route. Independent of
|
||||
* [MediaRouter.RouteInfo] so the picker UI can preview without an
|
||||
* Android framework presence (RouteInfo can't be constructed in
|
||||
* JVM tests, mirroring the LikeMediaCallback constraint).
|
||||
*
|
||||
* The [protocol] field is a forward-compatibility hook for the
|
||||
* UPnP / DLNA / Sonos / Cast slice (scope captured in
|
||||
* docs/superpowers/specs/2026-06-03-android-output-picker-upnp-scope.md).
|
||||
* THIS slice always sets `protocol = SYSTEM` — the system-route
|
||||
* categories MediaRouter surfaces (built-in / wired / Bluetooth).
|
||||
*/
|
||||
data class OutputRoute(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val kind: Kind,
|
||||
val protocol: Protocol,
|
||||
val isConnected: Boolean,
|
||||
) {
|
||||
enum class Kind { BuiltIn, Wired, Bluetooth, Cast, Other }
|
||||
|
||||
enum class Protocol {
|
||||
/** System-managed routes — built-in speaker, wired, Bluetooth. */
|
||||
SYSTEM,
|
||||
|
||||
/** Generic UPnP / DLNA renderers — reserved for the next slice. */
|
||||
UPNP,
|
||||
|
||||
/** Chromecast via the Cast SDK — reserved for a later slice. */
|
||||
CAST,
|
||||
|
||||
/** Sonos via the Sonos extension on top of UPnP — reserved. */
|
||||
SONOS,
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Lift a MediaRouter [route] into the domain model. Kind is
|
||||
* inferred from [MediaRouter.RouteInfo.getDeviceType]; unknown
|
||||
* device types fall through to [Kind.Other]. The
|
||||
* `connectionState` proxy is good enough for the chip's
|
||||
* "Connected"/"Available" subtitle.
|
||||
*/
|
||||
fun fromRouteInfo(route: MediaRouter.RouteInfo): OutputRoute {
|
||||
val kind = when (route.deviceType) {
|
||||
MediaRouter.RouteInfo.DEVICE_TYPE_BUILTIN_SPEAKER -> Kind.BuiltIn
|
||||
MediaRouter.RouteInfo.DEVICE_TYPE_WIRED_HEADSET,
|
||||
MediaRouter.RouteInfo.DEVICE_TYPE_WIRED_HEADPHONES,
|
||||
-> Kind.Wired
|
||||
MediaRouter.RouteInfo.DEVICE_TYPE_BLUETOOTH_A2DP -> Kind.Bluetooth
|
||||
MediaRouter.RouteInfo.DEVICE_TYPE_TV -> Kind.Other
|
||||
MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER -> Kind.Other
|
||||
else -> Kind.Other
|
||||
}
|
||||
val connected =
|
||||
route.connectionState == MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED
|
||||
return OutputRoute(
|
||||
id = route.id,
|
||||
name = route.name,
|
||||
description = route.description,
|
||||
kind = kind,
|
||||
protocol = Protocol.SYSTEM,
|
||||
isConnected = connected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ kotlinx-coroutines = "1.9.0"
|
||||
kotlinx-datetime = "0.6.1"
|
||||
kotlinx-serialization-converter = "1.0.0"
|
||||
media3 = "1.10.1"
|
||||
mediarouter = "1.7.0"
|
||||
coil = "3.0.0-rc02"
|
||||
palette = "1.0.0"
|
||||
timber = "5.0.1"
|
||||
@@ -71,6 +72,7 @@ media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref =
|
||||
media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }
|
||||
media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
|
||||
media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
|
||||
mediarouter = { module = "androidx.mediarouter:mediarouter", version.ref = "mediarouter" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||
androidx-palette = { module = "androidx.palette:palette-ktx", version.ref = "palette" }
|
||||
|
||||
Reference in New Issue
Block a user