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:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user