Replaces the four KeychainUnavailable stubs in auth_tokens.rs with a
real Android Keystore implementation:
- Device-bound AES-256/GCM/NoPadding key under alias
'solitaire_quest_token_key'; generated on first use, survives
restarts, destroyed on uninstall.
- Tokens serialised as JSON, encrypted to
{data_dir}/auth_tokens.bin as [12-byte IV][ciphertext+GCM-tag];
writes are atomic (tmp → rename).
- Key invalidation (biometric/lock change) surfaces as
TokenError::KeychainUnavailable, matching desktop fallback semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the workspace through `cargo apk build`. After this commit
`cargo apk build -p solitaire_app --target x86_64-linux-android`
produces a debug-signed APK at `target/debug/apk/solitaire-quest.apk`
containing all assets and `lib/x86_64/libsolitaire_app.so` — runnable
on the AVD or a physical x86_64 device.
The five gating points discovered by iterating compile cycles:
1. solitaire_app split into bin + lib. cargo-apk needs a `cdylib`
to bundle as `libmain.so`; pure-bin crates panic with
"Bin is not compatible with Cdylib". `src/lib.rs` carries the
ECS bootstrap as `pub fn run`; `src/main.rs` is a 3-line shim
that delegates for the desktop `cargo run` path.
2. `[package.metadata.android]` pins target SDK 34 / min SDK 26
so cargo-apk doesn't probe for whatever default it ships
(which on this machine was an uninstalled API 30). `assets =
"../assets"` lets the same asset directory feed both desktop
and APK.
3. Workspace `bevy` features add `android-native-activity` (the
Bevy-side glue that pairs with cargo-apk's NativeActivity
wrapper). The feature is target-gated inside bevy_internal so
desktop builds compile it out.
4. `arboard` (clipboard, used by Stats's "Copy share link") has
no Android backend — `cargo apk build` fails with E0433 on
`platform::Clipboard` if unconditional. Target-gated to
`cfg(not(target_os = "android"))`; the system surfaces an
informational toast on Android until JNI ClipboardManager is
wired in the Phase-Android round.
5. `keyring` + `keyring-core` cannot compile for android — the
transitive `rpassword` uses `libc::__errno_location` which
bionic doesn't expose. Both crates target-gated; `auth_tokens`
ships a stub on Android that returns `KeychainUnavailable` for
every call, matching how callers already handle a Linux box
without Secret Service.
Cosmetic post-pass panic: cargo-apk panics AFTER the APK is signed
when it tries to also wrap the bin target. The APK on disk is
unaffected. Working around this with `cargo apk build --lib` is
the next small step.
What's verified:
- Desktop `cargo build`, `cargo clippy --workspace --all-targets`,
and `cargo test --workspace` all clean.
- `cargo apk build -p solitaire_app --target x86_64-linux-android`
produces 54 MB debug APK with libsolitaire_app.so + assets.
What's NOT yet verified:
- Whether the APK actually launches on the AVD / a phone (next
step: `adb install` + `adb logcat` against the bevy_test AVD).
- Whether `dirs::data_dir()` on Android returns a usable path
(sync / persistence will surface this if not).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conservative cleanup pass — applied only the high-signal pedantic
lints whose fixes either remove genuine waste or read more naturally,
skipping anything stylistic that would bloat the diff.
- map_unwrap_or: 29 .map(...).unwrap_or(...) sites collapsed to
.map_or / .is_some_and / .map_or_else equivalents
- uninlined_format_args: 7 production format!/write!/println! sites
rewritten to the inline-argument style; assert! sites in test code
intentionally untouched
- match_same_arms: 2 redundant arms collapsed where the bodies were
identical and the merger didn't obscure intent
Public API is unchanged. No dependencies added or removed. The
pedantic warning count dropped from 840 to 807 (-33). Out-of-scope
findings — needless_pass_by_value on Bevy Res params, false-positive
explicit_iter_loop on Bevy Query iterators, items_after_statements
inside test mods, and the "ask before changing" merge logic in
solitaire_sync — were intentionally deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Art pass (Phase 4):
- Generate placeholder PNG assets: face.png, back_0–4.png, bg_0–4.png via
solitaire_assetgen gen_art binary (16×16 RGBA, embedded via include_bytes!)
- Add FiraMono-Medium font (assets/fonts/main.ttf) embedded at compile time
- Add FontPlugin: loads font at startup, exposes FontResource; gracefully
falls back to default handle when Assets<Font> absent (MinimalPlugins tests)
- Wire CardImageSet into card_plugin: face/back PNGs replace solid-colour
sprites when available; tests continue using colour fallback via MinimalPlugins
- Wire BackgroundImageSet into table_plugin: bg PNGs replace solid-colour
background; empty set inserted when Assets<Image> absent in tests
- Fix hint highlight system (input_plugin): tint sprite.color directly instead
of replacing the whole Sprite (which would discard the image handle)
- Export FontPlugin, FontResource, CardImageSet from solitaire_engine::lib
- Register FontPlugin in solitaire_app before other plugins
Dependency upgrades (latest releases):
- keyring "2" → keyring "4" + keyring-core "1" (v4 split architecture into
separate core library crate)
- auth_tokens.rs: Entry::new now returns Result; delete_password →
delete_credential; NoDefaultStore error variant handled
- solitaire_app: add keyring::use_native_store(true) at startup for Linux
Secret Service / macOS Keychain / Windows Credential Store selection
ARCHITECTURE.md: fix Edition 2025→2021, update asset pipeline section,
add FontPlugin/CardImageSet/BackgroundImageSet to plugin and resource tables,
update Section 14 to reflect actual include_bytes!() rendering approach,
add Decision Log entries for embedded PNG and font decisions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
challenge, account deletion, JWT middleware, rate limiting via
tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
SolitaireServerClient, auth_tokens keyring integration, blanket
Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
connections share the same schema
All 191 tests pass; cargo clippy -D warnings clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>