Commit Graph

7 Commits

Author SHA1 Message Date
bvandeusen 5f3905f2c7 feat(android): UPnP picker foundation (UPnP slice 3/6)
android / Build + lint + test (push) Successful in 4m11s
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>
2026-06-03 11:51:11 -04:00
bvandeusen 0662c9d5cc feat(android): output picker foundation - mediarouter + OutputRoute
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.
2026-06-03 10:27:02 -04:00
bvandeusen b9186937b3 chore(android): lock MainActivity to portrait
android / Build + lint + test (push) Successful in 3m39s
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.
2026-06-02 08:12:58 -04:00
bvandeusen f080afc38c fix(android): remove WorkManager auto-initializer (lintVitalRelease)
android / Build + lint + test (push) Successful in 4m26s
android / Build signed release APK (push) Has been skipped
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.
2026-06-01 16:55:29 -04:00
bvandeusen 8f5635c489 feat(android): in-app APK update install (audit v3 #25)
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>
2026-05-28 01:18:49 -04:00
bvandeusen 85b8452b78 feat(android): MinstrelPlayerService — Media3 MediaSessionService (M8 phase 6.2)
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>
2026-05-23 12:26:47 -04:00
bvandeusen 34e54f29e9 feat(android): :app module skeleton — Compose Activity + manifest
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>
2026-05-21 21:17:40 -04:00