UpnpRoute - narrow domain model for a discovered UPnP / DLNA
renderer. Carries the AVTransport + RenderingControl control URLs
the SOAP client uses.
CastApi - Retrofit interface for the new POST /api/cast/stream-token
endpoint (UPnP slice 2/6). Returns {token, exp, url} for the
selection path.
OutputRoute.fromUpnpRoute - companion factory that tags the route
with Protocol.UPNP. Subtitle is 'Manufacturer Model' or falls back
to 'Network speaker' when description fields are blank.
CHANGE_WIFI_MULTICAST_STATE manifest permission - install-time on
all API levels, no runtime prompt. Required for SSDP multicast
discovery.
Discovery + SOAP + integration land in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Operator feedback: landscape just stretches the phone-portrait
Compose layout awkwardly — every screen was sized for one column
of cards, so rotation produces wide rows of unrelated content with
big dead bands top and bottom. Until a dedicated tablet/landscape
layout exists, lock the activity to portrait via screenOrientation.
Revisit when a sw600dp resource set + multi-pane layouts land.
The android.yml release build (run 110, job 282) failed at
lintVitalRelease with:
AndroidManifest.xml:13: Error: Remove androidx.work.WorkManagerInitializer
from your AndroidManifest.xml when using on-demand initialization.
[RemoveWorkManagerInitializer from androidx.work]
MinstrelApplication implements androidx.work.Configuration.Provider
and supplies the HiltWorkerFactory, so WorkManager initializes
on-demand at first WorkManager.getInstance(...) call. The androidx-
startup InitializationProvider that ships with the work-runtime
library still auto-registers WorkManagerInitializer, racing the
on-demand path. Lint catches this as a release blocker.
Fix: merge the InitializationProvider entry and remove the nested
WorkManagerInitializer meta-data with tools:node='remove'. This is
the AOSP-recommended fix when an Application is its own
Configuration.Provider.
Wires the "Install vX.Y.Z" action the About card's update-check
teed up.
* ApkInstaller @Singleton — downloads the server APK via the shared
OkHttpClient (inherits auth cookie + BaseUrlInterceptor host
rewrite; apkUrl is server-relative) into cacheDir, then launches
the system installer via a FileProvider content:// URI. canInstall()
gates on PackageManager.canRequestPackageInstalls() on O+, and
requestInstallPermission() opens the "install unknown apps" settings
page when not yet granted.
* Manifest: REQUEST_INSTALL_PACKAGES permission + FileProvider
(${applicationId}.fileprovider) + res/xml/file_paths.xml exposing
the cache dir.
* AboutCardViewModel.install(info): permission check → download →
launch, with isInstalling + installMessage state. Errors routed
through ErrorCopy.
* About card shows an "Install vX.Y.Z" button under the check button
when an update is available, "Downloading…" while in flight, and
the install message line. Extracted UpdateControls / InstallButton /
ButtonSpinner helpers to keep AboutCard under the length cap;
added @file:Suppress(TooManyFunctions) for the settings-card density.
Closes audit v3 #25 — the last open parity item from the v3 sweep
aside from the offline-pool Home cards (#28).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The actual replacement for everything audio_service plugin wrapped.
Media3 owns the foreground-service lifecycle, MediaSession token,
notification card, lock-screen surface, Bluetooth/AVRCP routing,
Pixel Watch tile, and Android Auto adapter natively — no plugin
layer between us and the platform.
Service shape (~25 LOC):
- @AndroidEntryPoint MediaSessionService
- @Inject PlayerFactory builds ExoPlayer in onCreate
- onGetSession returns the live MediaSession to any binding
controller (system UI, Wear OS companion, MediaController3 clients)
- onTaskRemoved keeps playing while audio is active (standard
media-app behavior); otherwise stopSelf so notification clears
- onDestroy releases session + player
Compare with flutter_client/lib/player/audio_handler.dart's 1000+ LOC
across MinstrelAudioHandler + the soft-teardown / stall-watchdog /
recovery machinery. Media3 owns most of that natively; we'll get to
the small portions we still need (queue management, position
reporting facade) in 6.3.
Manifest registration: foregroundServiceType="mediaPlayback" +
MediaSessionService intent-filter. MediaButtonReceiver is registered
by the Media3 library; no manual receiver class needed (Flutter's
manifest had to declare audio_service's receiver explicitly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 1.2. AndroidManifest declares FGS mediaPlayback +
POST_NOTIFICATIONS permissions ahead of the player phase. Activity
hosts a single Compose Scaffold for now; nav graph lands in phase 5.
Launcher icons reused from flutter_client/ (same applicationId means
same brand at cutover). MinstrelApplication referenced in manifest
but the class itself lands in Task 1.3 — manifest class names are
resolved at install time, not build time, so the intermediate commit
still builds.
@AndroidEntryPoint deferred to Task 1.3 alongside @HiltAndroidApp on
MinstrelApplication (Hilt KSP errors without an annotated Application).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>