9940bc375dab7709a3b0238132d8124b06995b1c
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9940bc375d |
feat(android): notifications when the app is closed, a specialUse delivery service (#5347)
release / govulncheck (push) Successful in 37s
release / web (push) Successful in 1m8s
release / go (push) Successful in 1m31s
release / integration (push) Successful in 5m6s
release / android (push) Successful in 6m17s
release / Build signed APK (releases and dev) (push) Successful in 6m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m13s
release / Verify release artifacts (tag releases only) (push) Skipped
Roundtable's shape: no FCM. A specialUse foreground service keeps the process alive; EventsStream stays connected; DeliveryLauncher runs the catch-up on every notification.created nudge and every reconnect, and is the single owner of the service's lifetime (signed in AND the device's "Notifications when the app is closed", on by default). - NotificationSync pulls the newest page and announces unread notices past a high-water mark (auth_session.notifiedUpTo, read and written through the DAO), honouring the per-kind phone pref. A first look sets the mark without announcing; more than three collapse to one line; nothing is posted while the app is on screen. - Two channels: "Your requests" and "Library health"; the ongoing notice sits on a MIN "Background connection" channel. - EventsStream: a connected flow, 2s→5min backoff with ±25% jitter, and an immediate reconnect when a network comes up (a hint, not VALIDATED) or the app comes to the foreground. - BootReceiver restarts delivery after a reboot or a self-update. - A tap opens what the notice links to (routeForLink), a pile the inbox. - POST_NOTIFICATIONS is asked for on Android 13+ once delivery is wanted, and again when the toggle is turned on. - Room v12: auth_session.backgroundDelivery and notifiedUpTo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b28cbe0600 |
feat(android): refuse plain http:// to a public server address (#5111)
release / go (push) Successful in 1m47s
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m15s
release / integration (push) Successful in 4m44s
release / android (push) Successful in 5m37s
release / Build signed APK (releases and dev) (push) Successful in 5m43s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m13s
release / Verify release artifacts (tag releases only) (push) Skipped
Cleartext stays permitted app-wide for LAN servers and UPnP (#2439), but a password or session cookie sent over plain HTTP to a public address can be read by anyone on the path. A network interceptor now refuses a cleartext request to the Minstrel server when the connection lands on a public address, before any request byte is written. Checked per connection, on the address actually reached, rather than when the URL is typed: a name that resolved to the home network at entry resolves to a public address once the phone leaves home. Allowed: loopback, 10/8, 172.16/12, 192.168/16, link-local, 100.64/10 (Tailscale and other overlay VPNs) and fc00::/7. Only requests BaseUrlInterceptor tagged as server-bound are checked; external fetches and UPnP are untouched. The refusal has its own message. Family baseline #5105, practice 13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b52a00df66 |
fix(android): bundle the typefaces instead of fetching them at runtime
android / Build + lint + test (push) Successful in 4m19s
Typography.kt resolved Fraunces, Inter and JetBrains Mono through the Play Services font provider, which fetches them over the network on first use. Same rule-164 problem the web client had, with a second failure mode on top: the provider is absent entirely on devices without Play Services, so the app fell back to the platform default and stopped looking like Minstrel — quietly, with no error. The five static instances now live in res/font, vendored by the same tools/vendor-fonts.py that produces the web bundle. Both clients draw from one list of faces so they cannot drift apart. Cost is ~0.86 MB of APK; the runtime path is removed rather than kept as a fallback — the ui-text-google-fonts dependency, its version-catalog entry and the provider certificate hashes in font_certs.xml are all gone. Two things about fetching TTFs that are worth writing down, because both fail by succeeding: Google Fonts picks the format from the User-Agent, and there is no parameter to ask for one. A modern UA gets woff2, which res/font cannot load. The obvious "use an old UA" fix gets EOT — an IE-only format that downloads happily, has a plausible size, and is entirely useless here. An Android 4.4 UA is what actually yields TrueType. css2 also collapses a multi-weight request to 400 for legacy clients, so asking for Medium silently returns Regular: a valid TrueType file that renders at the wrong weight everywhere. Each weight is therefore fetched on its own URL, and the script now asserts OS/2 usWeightClass on every download — that field is the only thing distinguishing the two files. Verified before wiring: all five carry TrueType magic, the 400/500 pairs differ, and their usWeightClass reads 400/400/500/500/400 as declared beside them in the FontFamily. Not covered: there is no guard for this on the Android side. The web equivalent is asserted by no-external-assets.test.ts, but the Android tree has no source-inspection test pattern to follow and no way to falsify one without a local Gradle run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH |
||
|
|
5593f7ce17 |
feat(brand): replace the M mark with the traced bard-hat logo
The mark is now a feathered hat with an arc of eighth notes, traced from the operator's reference artwork at 99.74% IoU. The hat takes the text colour and the note arc holds the accent — the same construction the M used, and for the same reason: parchment on a light surface is invisible, so the silhouette has to flip with its background while the accent stays constant. This reverses the subject-neutrality argument recorded in Minstrel's design system, which held that depicting a bard would tell a new user the app is for renaissance-faire music and had twice rejected a hat. The operator commissioned this artwork and chose it with that objection on the table; the record is updated rather than silently contradicted. Both accent-filled alternatives were measured and rejected: #4A6B5C is 3.04:1 on obsidian and 2.80:1 on the raised iron, so an accent hat drops under the 3:1 graphics floor as soon as it sits on a card. tools/gen-brand-assets.py is the single source for the four copies, which cannot share a file because each needs a different colour mechanism — currentColor inlined, a prefers-color-scheme swap in the favicon, literal fills in mark.svg, flat pixels in the rasters. Hand-copying 20KB of path data four ways is how a silhouette change lands in three of them. Two notes on the trace, both non-obvious: it runs on the original antialiased greyscale rather than a binary mask, because tracing a supersampled mask scores ~100% IoU by reproducing the pixel staircase exactly — a perfect number for jagged art at 120KB of path, versus 99.74% at 20KB. And potrace reads PBM where bit 1 is black, so the ink mask is inverted going in; backwards, it traces the background and still emits a plausible-looking SVG. The header lockup moves 20px → 28px: the hat carries far more detail than the M and does not resolve below ~32px. The 16px browser-tab favicon is still a blob at that size and is not addressed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH |
||
|
|
2e1a8a62d8 |
refactor(android): move cleartext opt-out into a networkSecurityConfig — #2439
android / Build + lint + test (push) Successful in 3m46s
`android:usesCleartextTraffic="true"` sat on <application> as a bare opt-out of the platform's network-security default, with nothing recorded about why. It's now a res/xml/network_security_config.xml carrying the same permission and the reasoning behind it. Behaviour is unchanged. networkSecurityConfig supersedes the attribute on API 24+ and our minSdk is 26, so the attribute is removed rather than kept alongside. Cleartext stays permitted because two independent things need it, and neither can be narrowed to a domain list: - The Minstrel server's host is user-entered at runtime, and plenty of self-hosters run plain HTTP on a LAN. - UPnP/DLNA/Sonos — device-description and SOAP control URLs arrive in SSDP responses at runtime and are plain HTTP essentially always. This one wasn't in the original ticket, which only considered the server; it independently rules out the "tighten it later to RFC1918" idea, since <domain-config> matches literal hostnames, not CIDR ranges, and renderer IPs are unknowable ahead of time. Trust anchors deliberately left at the platform default. Adding <certificates src="user" /> would let self-hosters use HTTPS with a private CA — which Mihon does, and which suits this product — but it also trusts every CA on the device including a corporate MITM proxy. Raised separately rather than assumed as a default. tools:ignore="InsecureBaseConfiguration" mirrors Mihon's config and keeps lintVitalRelease quiet about a choice that is deliberate and now documented. |
||
|
|
a4b6f22d86 |
feat(update): silent self-update via PackageInstaller session — #2438
android / Build + lint + test (push) Successful in 3m54s
Replaces the ACTION_VIEW + application/vnd.android.package-archive handoff with a PackageInstaller session, and declares UPDATE_PACKAGES_WITHOUT_USER_ACTION so the update can land with no confirm dialog at all. The platform grants the silent path when the installer opts in via setRequireUserAction(USER_ACTION_NOT_REQUIRED), the installed app targets API 29+, the installer holds that permission, and the target is the installer itself. Minstrel updating Minstrel satisfies all four. Where it can't be granted — anything pre-S — the platform returns STATUS_PENDING_USER_ACTION and we show its dialog instead, so this degrades rather than failing. Prior art: Mihon, which is out-of-store and self-updating and whose updates are quiet for exactly this reason. It also confirmed REQUEST_INSTALL_PACKAGES is not what draws install warnings — Mihon declares it too. No setRequestUpdateOwnership(true), despite it reading like the obvious declaration for a self-updater. Ownership can only be claimed on initial installation (a no-op on update) and additionally wants the privileged ENFORCE_UPDATE_OWNERSHIP permission. It's an API for app stores claiming the apps they install. Also: the install now has an outcome. The old path fired an intent and assumed, so a failure and a user declining were indistinguishable. Sessions report back, so InstallOutcome distinguishes Installed / Cancelled / Failed, and cancelling returns to IDLE rather than showing an error — the user chose it. DOWNLOADING and INSTALLING became separate stages because the install half now genuinely waits, and "Downloading…" through a confirm dialog is a lie. The FileProvider and res/xml/file_paths.xml are gone. They existed only to expose the cached APK as a content:// URI for the old intent; a session takes a stream. Nothing else used that authority. Two judgement calls worth naming: - The pending-user-action intent is only launched if it resolves to a system component. Below API 34 a dynamically registered receiver can't declare itself unexported, so another app can broadcast at us, and an unchecked startActivity on an attacker-supplied extra would be an escalation primitive. The real confirm activity is a system app, so the check costs the legitimate path nothing. - Cancellation unregisters the receiver but deliberately does NOT abandon the session. By then it's committed, and killing an install because the user navigated away from the banner misreads their intent. Untestable here: no androidTest source set and no Robolectric, so the gesture-level behaviour is operator on-device verification. |
||
|
|
e1e591b520 |
feat(brand): Minstrel mark — favicon, header lockup, Android adaptive icon
A Didone M whose right leg is an eighth note: stem, flag and notehead in the
accent, the letter in parchment. Traced from the operator's reference at
99.74% IoU (potrace, 26 + 22 segments), so the geometry is theirs, not an
approximation of it.
Subject-neutral on purpose. "Minstrel" pulls toward a lute or a bard, which
would tell a new user this is a renaissance-faire player rather than one for
all music. A geometric letter plus universal notation says "music" without
saying which music. The family look arrives through palette and drawing
style instead of through the subject — see the design-system discussion.
Starting state: web/static/favicon.png was a 1x1 PIXEL placeholder, so there
was effectively no favicon at all; Android had legacy bitmaps only, so modern
launchers letterboxed the square instead of masking it.
## The colour problem, and why each surface differs
Parchment on white is invisible — the operator caught this. The M therefore
has to flip with its background, while the accent note holds in both:
- mark.svg / MinstrelMark.svelte use currentColor, so the letter takes the
surrounding text colour and one asset covers both palettes.
- favicon.svg bakes colours with a prefers-color-scheme swap, because a
favicon sits on browser chrome and has no cascade to inherit from.
- PNG fallback, apple-touch-icon and Android are PLATED. A PNG can't
respond to scheme and iOS composites onto white regardless.
MinstrelMark is inlined rather than <img src>, because an <img> cannot
inherit currentColor and inheriting it is the entire point.
## Plate colour chosen by measurement
Obsidian (#14171A), not the raised-surface iron. The accent note only clears
the 3:1 non-text contrast threshold against the darker value: 3.04:1 vs iron's
2.70:1. My own earlier suggestion — lighten the plate — is WRONG and the
numbers say so: slate scores 2.21:1, worse, because the note is a dark colour
and lifting the plate closes the gap. Recorded in colors.xml so the reasoning
sits with the value.
## Construction
Traced as a full ink silhouette with the note painted OVER it, rather than as
two separate shapes. Separate shapes needed either a 2px seam where letter and
note touch, or an anti-aliasing fringe (2,430 misclassified pixels) around the
note. Painting over avoids both and yields a monochrome version for free — the
base layer alone is the whole mark in one colour, which is what
mipmap-anydpi-v26's <monochrome> uses for themed icons.
Android foreground sits at 61% of the 108dp canvas so it stays inside the
66dp safe zone and no launcher mask can clip it.
Paths are duplicated between the component and the two static SVGs, since one
needs currentColor and the others need literals. A comment in each names the
others.
Verified by render at 16/20/32/64/180 on obsidian, white, parchment and
plated; one optical size holds across the whole range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
f2f6fa06a2 |
feat(android): FabledSword theme — Material 3 + Google Fonts
M8 phase 1.4. Mirrors flutter_client/lib/theme/. Source of truth for hex values is flutter_client/shared/fabledsword.tokens.json (manual sync until cross-language codegen lands; ports the dark-surface + flat cohort). Material 3 ColorScheme takes accent as primary; action colors (Moss/Bronze/Oxblood) live in LocalActionColors as semantic roles per the project_design_system rule "NEVER use accent for action buttons". Typography uses androidx.compose.ui.text.googlefonts to fetch Fraunces / Inter / JetBrains Mono at runtime via Play Services Fonts — matches the Flutter client's `google_fonts` package (no bundled .ttf files in either tree). Weights restricted to 400/500. Fraunces is reserved for ≥18sp display/headline slots per the design-system rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |