Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PyrycodeMobile">
android:theme="@style/Theme.PyrycodeMobile.SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/de/pyryco/mobile/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
Expand Down Expand Up @@ -48,6 +49,7 @@ import org.koin.compose.koinInject

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_splash_logo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="288dp"
android:height="288dp"
android:viewportWidth="288"
android:viewportHeight="288">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M80,144a64,64 0 1,0 128,0a64,64 0 1,0 -128,0Z" />
</vector>
6 changes: 6 additions & 0 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
<resources>

<style name="Theme.PyrycodeMobile" parent="android:Theme.Material.Light.NoActionBar" />

<style name="Theme.PyrycodeMobile.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash_logo</item>
<item name="postSplashScreenTheme">@style/Theme.PyrycodeMobile</item>
</style>
</resources>
1 change: 1 addition & 0 deletions docs/knowledge/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ One-line pointers to evergreen docs. Per-ticket implementation notes live under

## Features

- [Splash screen](features/splash-screen.md) — system-managed cold-launch splash via `androidx.core:core-splashscreen:1.0.1` (#80); launcher `<activity android:theme="@style/Theme.PyrycodeMobile.SplashScreen">` resolves a `Theme.SplashScreen`-parented style (compat library, no `android:` namespace) with `windowSplashScreenBackground = @color/ic_launcher_background` (#FF32628D, reused from the launcher icon — no parallel color name) + `windowSplashScreenAnimatedIcon = @drawable/ic_splash_logo` (new 288×288dp viewport with all paths inside the inner 192dp safe area; single white-filled r=64 circle centred at 144,144 — placeholder, not brand identity) + `postSplashScreenTheme = @style/Theme.PyrycodeMobile`; `MainActivity.onCreate` calls `installSplashScreen()` as its **first** statement before `super.onCreate(...)` (AndroidX swaps the theme to `postSplashScreenTheme` before the window is realised — calling after `super` leaks the splash theme into the first app frame on some OEMs) with the returned `SplashScreen` handle intentionally discarded — no `setKeepOnScreenCondition { ... }`, no exit-animation listener (the [Navigation](features/navigation.md) #13 conditional start destination + `produceState`+empty-`Surface` placeholder already covers the only flash window). Application-level `android:theme` stays at `Theme.PyrycodeMobile` so any future non-launcher activity inherits the normal app theme. Splash drawables resolve **outside** the app theme so fills must be literal hex (`#FFFFFFFF`), not `?attr/...` references. No automated tests — splash is a system-window concern that `androidx.compose.ui.test` attaches after and `ActivityScenario` bypasses; verification surface is the build + one manual Android 13+ cold launch (Android 11/12 compat fallback is solid-background-no-animation-envelope, expected not a regression). Branded artwork + animated transition + any `windowSplashScreenAnimationDuration` / `windowSplashScreenIconBackgroundColor` items are a deliberate Phase 5 follow-up.
- [Navigation](features/navigation.md) — single-activity Compose NavHost in `MainActivity`; start destination gated on `AppPreferences.pairedServerExists` via `produceState` (`welcome` fresh, `channel_list` once paired); destinations `welcome`, `scanner`, `channel_list`, `discussions` (#24 — reached from the channel-list inline Recent-discussions section's "See all discussions (N) →" link since #69, which replaced the #26 pill), `conversation_thread/{conversationId}`, `settings`, `license` (#91 — reached from the Settings About-section License row); route constants in colocated `Routes` object.
- [Welcome screen](features/welcome-screen.md) — first onboarding screen; stateless Composable with two navigation callbacks, mounted at the `welcome` route. Body locked to Figma node `6:32` since #57 — layered `Box` (solid `surface` + `Modifier.drawBehind` radial-gradient atmospheric overlay + content `Column(SpaceBetween)`); hero pairs `ic_pyry_logo` over `headlineLarge` title + `titleMedium` tagline + `bodyLarge` paragraph; CTA stack is filled `Button` w/ `ic_qr_scan_frame` leading icon over `TextButton` over centred `labelSmall` open-source footer. Instrumented Compose tests since #100 — four-method `WelcomeScreenTest` at `app/src/androidTest/.../onboarding/WelcomeScreenTest.kt` covers per-CTA `assertIsDisplayed()` plus per-CTA click-wiring with local `var pairedCount = 0` / `var setupCount = 0` counters (raw-callback shape — the screen's `(onPaired, onSetup)` signature predates the project-wide `(state, onEvent)` MVI shape, so the click-capture pattern degrades from `mutableListOf<Event>::add` to plain counters). CTA labels asserted as English literals (`"I already have pyrycode"` / `"Set up pyrycode first"`) because the production source carries them as literals too — no `strings.xml` entry to route through.
- [Scanner screen](features/scanner-screen.md) — QR-pairing screen at the `scanner` route; locked to Figma node `13:2` since #60. M3 `TopAppBar("Pair with pyrycode")` over a viewport `Box(weight(1f))` (`surfaceContainerLowest` base + dual radial gradients painted in one `Modifier.drawBehind` + `Canvas` stripe overlay every 7dp + 248dp four-corner `Reticle` with a `Modifier.blur(12.dp, BlurredEdgeTreatment.Unbounded)` halo under a crisp scan line + `HintCard` AnnotatedString with `FontFamily.Monospace` `pyry pair` accent) over a `Trouble scanning?` `TextButton`. AC6 contradiction: visible back-arrow and text-button are both wired to `onTap` — tap anywhere flips `AppPreferences.setPairedServerExists(true)` + navigates to `channel_list` with the scanner popped. Phase 4 replaces with real CameraX + ML Kit. Instrumented Compose tests since #101 — three-method `ScannerScreenTest` at `app/src/androidTest/.../onboarding/ScannerScreenTest.kt` asserts `"Pair with pyrycode"` TopAppBar title (exact), the `buildAnnotatedString` hint card body via `hasText("pyry pair", substring = true)` (substring covers the `AnnotatedString` concatenation without depending on the full sentence), and the `"Trouble scanning?"` fallback `TextButton` carries `hasClickAction()`. Structure only — the `onTap`-fires-from-every-affordance wiring is deliberately unasserted; click-callback coverage is deferred to the Phase 4 rewrite.
Expand Down
42 changes: 42 additions & 0 deletions docs/knowledge/codebase/80.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# #80 — Splash Screen API plumbing (Android 12+)

Wires `androidx.core:core-splashscreen` into the cold-launch path so the activity stops flashing the bare app theme on first frame. Plumbing only — the launcher activity now resolves a `Theme.SplashScreen`-rooted style with brand-colour background + a placeholder white-circle vector, then `installSplashScreen()` swaps to `Theme.PyrycodeMobile` before the Compose hierarchy is realised. Branded artwork + animated transition are an explicit follow-up.

## Summary

- Dependency `androidx.core:core-splashscreen:1.0.1` added through the version catalog (alias `androidx-core-splashscreen`, version key `coreSplashscreen`, adjacent to `androidx-core-ktx`). Referenced as `implementation(libs.androidx.core.splashscreen)` in `app/build.gradle.kts`.
- New `Theme.PyrycodeMobile.SplashScreen` style appended to `res/values/themes.xml`, parented at the compat library's `Theme.SplashScreen` (no `android:` namespace prefix — the parent ships in `core-splashscreen`, not the platform). Three items only: `windowSplashScreenBackground = @color/ic_launcher_background` (reuses the brand color from `colors.xml`, no parallel name introduced), `windowSplashScreenAnimatedIcon = @drawable/ic_splash_logo`, `postSplashScreenTheme = @style/Theme.PyrycodeMobile`.
- New placeholder vector at `res/drawable/ic_splash_logo.xml` — 288×288dp viewport per the Android 12+ splash icon spec, single white-filled `<path>` describing a r=64 circle centred at (144,144) (`M80,144a64,64 0 1,0 128,0a64,64 0 1,0 -128,0Z`). All path coordinates stay inside the inner 192dp safe area (48–240 on both axes).
- `AndroidManifest.xml` flips the launcher `<activity android:name=".MainActivity">`'s `android:theme` from `@style/Theme.PyrycodeMobile` to `@style/Theme.PyrycodeMobile.SplashScreen`. The application-level `android:theme` stays untouched at `Theme.PyrycodeMobile`, so any future non-launcher activity inherits the normal app theme by default.
- `MainActivity.onCreate` adds `installSplashScreen()` as the **first** statement, before `super.onCreate(savedInstanceState)`. Return value is intentionally discarded — no `setKeepOnScreenCondition { ... }`, no exit-animation listener (out of scope per spec; the #13 conditional NavHost start destination already prevents the only flash that would warrant gating).

## Files touched

- `gradle/libs.versions.toml:4,23` — `coreSplashscreen = "1.0.1"` + the `androidx-core-splashscreen` library alias.
- `app/build.gradle.kts:59` — `implementation(libs.androidx.core.splashscreen)` line in the existing `dependencies { ... }` block, alphabetically slotted after `core-ktx`.
- `app/src/main/AndroidManifest.xml:19` — launcher activity `android:theme` swapped to `@style/Theme.PyrycodeMobile.SplashScreen`.
- `app/src/main/res/values/themes.xml:6-10` — new `Theme.PyrycodeMobile.SplashScreen` style.
- `app/src/main/res/drawable/ic_splash_logo.xml` — new 288dp placeholder vector (10 lines).
- `app/src/main/java/de/pyryco/mobile/MainActivity.kt:22,52` — added `import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen` + the `installSplashScreen()` call ahead of `super.onCreate(...)`.

## Patterns established

- **Splash theme is additive — never modify `Theme.PyrycodeMobile`.** The post-splash theme is the existing app theme; the splash theme is a sibling style with the splash-specific items only. Branded-splash follow-up should add `windowSplashScreenAnimationDuration` / `windowSplashScreenIconBackgroundColor` to `Theme.PyrycodeMobile.SplashScreen`, not to the parent app theme.
- **Splash drawables resolve outside the app theme** — fills must be literal colors (`#FFFFFFFF`), not `?attr/...` theme references. The 288×288dp viewport with a 192×192dp inner safe area is the contract; anything that needs to live in the visible mark stays in coords 48–240.
- **`installSplashScreen()` runs before `super.onCreate(...)`.** AndroidX swaps the activity theme from the splash theme to `postSplashScreenTheme` before the window is realised; invoking the install call after `super.onCreate(...)` leaks the splash theme into the first app frame on some OEMs. If a future ticket reorders `onCreate` (e.g. inserts side effects), keep `installSplashScreen()` as the first statement.
- **Launcher-activity-only theme swap.** Don't put the splash theme at the application level — that breaks every non-launcher activity in the future. The activity's own `android:theme` is what the OS reads when drawing the first frame before the process is alive, so the launcher activity is the only correct target.

## Lessons learned

- **The `Theme.SplashScreen` parent is not platform — it lives in `androidx.core:core-splashscreen`.** Writing `parent="android:Theme.SplashScreen"` in `themes.xml` will compile (the resource resolves) but render with the wrong attribute set on devices that don't have the platform style declared for the chosen `minSdk` slice. The compat library's `Theme.SplashScreen` is the only sanctioned parent for this codebase regardless of API level, because it normalises behaviour from API 23 through 34+.
- **`Conversation` and adjacent features aren't observable during the splash window.** The splash is a system window, not a Compose surface — `androidx.compose.ui.test` semantics only attach after dismissal. Activity-level `ActivityScenario` recreates the activity in-process and bypasses the cold-launch path entirely. The verification surface for this ticket is therefore the build (theme/drawable/manifest resolve) plus one manual cold-launch on an Android 13+ emulator, as the spec's "Testing strategy" section codifies. Future splash work that touches behaviour (timing, exit transition) should not invent a flaky instrumented test — file a manual-verification checklist or factor the testable state out of the splash window first.
- **`ic_pyry_logo.xml` is the wrong source for splash icons** even though it's the project's only existing brand-mark vector. Its viewport is 104dp and its paths fill the entire viewport — both wrong for the splash spec (288dp viewport, 192dp inner safe area). Reuse would have required a translation/scale rewrite of the path coordinates, which is more invasive than writing a one-`<path>` placeholder circle. The branded-splash follow-up will produce a purpose-built 288dp vector; do not try to retrofit `ic_pyry_logo` into that role.

## Links

- Issue: https://github.com/pyrycode/pyrycode-mobile/issues/80
- Spec: `docs/specs/architecture/80-splash-screen-api-plumbing.md`
- Feature doc: [Splash screen](../features/splash-screen.md) (new).
- Prior shape it builds on: #13 ([`./13.md`](./13.md)) — conditional NavHost start destination; the reason this ticket can skip `setKeepOnScreenCondition { ... }` entirely.
- Brand color reused: `R.color.ic_launcher_background` (#FF32628D) — single-source-of-truth from the launcher-icon ticket.
- Follow-up (not yet filed): branded splash icon + animated transition under Phase 5 polish. Will replace `ic_splash_logo.xml`, may add `windowSplashScreenAnimationDuration` / `windowSplashScreenIconBackgroundColor` / an exit listener; will not touch the manifest, the install call, or `Theme.PyrycodeMobile`.
Loading
Loading