diff --git a/docs/research/2026-10-19-modernization-research.md b/docs/research/2026-10-19-modernization-research.md new file mode 100644 index 0000000..299ad65 --- /dev/null +++ b/docs/research/2026-10-19-modernization-research.md @@ -0,0 +1,85 @@ +# AceTimeTracker modernization research + +- Checked: 19 October 2026. +- Purpose: inform the proposed [Android specification](../superpowers/specs/2026-10-19-android-modernization-design.md). + +## Scope and evidence + +The owner confirmed that the new audience is anyone building personal goals and habits. The app's older student positioning is historical context, not a constraint on the redesign. + +This is research from public first-party product documentation, engineering writing, a first-hand interview, official platform references, dependency metadata, and a read-only code audit. It does not include running competitor binaries, private traffic inspection, private telemetry schemas, or a live Firebase inventory. No installed skill matching the mentioned reverse-engineering name was found. These sources are sufficient for the current product proposal; the exact skill/link would be needed to reproduce that particular workflow. + +Historical articles establish what was published at their date. They do not prove identical behavior across today's accounts, experiments, countries, and app versions. Recommendations below are proposed AceTimeTracker adaptations, not claims about competitor internals. + +## Product comparison + +| Product | Publicly documented pattern | Proposed lesson for AceTimeTracker | +| --- | --- | --- | +| Duolingo | Guided next actions, visible streaks, reminder settings and flexibility. [Guide](https://blog.duolingo.com/duolingo-101-how-to-learn-a-language-on-duolingo/) | Make the next manageable action obvious and keep actual goal completion distinct from consistency | +| Forest | Focus sessions contribute to an accumulating visual forest; strict modes have consequences for abandoning focus. [Product page](https://www.forestapp.cc/) | Make effort visible over time; preserve earned effort when a person stops early | +| Focus To-Do | Combines tasks, Pomodoro timing, reminders and completion/time reporting. [Features](https://www.focustodo.cn/) | Give time a purpose and place relevant actions together | +| TickTick | Stopwatch/Pomodoro options, linked focus sessions and flexible habit frequency. [Focus](https://help.ticktick.com/articles/7055782010496745472), [habits](https://help.ticktick.com/articles/7055781878401335296) | Offer flexible routines and a direct action from the reminder into the relevant goal | +| Toggl Track | Editable entries, reminder windows/weekdays, snooze and explicit idle-time choices in its desktop product. [macOS documentation](https://support.toggl.com/en-us/article/toggl-track-desktop-app-for-macos-1669b8x/) | Prioritize trustworthy, correctable history; phone inactivity does not prove that a reading/exercise session stopped | + +The resulting direction is a goal-first personal companion, not a billing system or a full task-management suite. Start with duration-based daily/weekly goals and optional routine reminders. A focus countdown, richer outcome/check-in measures, or count-based habits can be evaluated after the core flow is reliable. + +## What Duolingo's public evidence supports + +- **Message selection:** A 2020 engineering account describes experiments with prewritten notifications and reducing recent repetition, evaluated against lesson completion. It is a useful example of testing helpful behavior rather than optimizing clicks alone. [Engineering account](https://blog.duolingo.com/hi-its-duo-the-ai-behind-the-meme/) +- **Timing:** Retention lead Jackson Shuttleworth described a reminder based on 23.5 hours after the previous day's first practice and reducing reminders after inactivity. This is a reported strategy to test, not a universal best time or a verified description of every current user. [First-hand interview](https://subclub.com/episode/how-to-time-reactivation-campaigns-for-maximum-impact-jackson-shuttleworth-duolingo) +- **Control:** The public product guide documents reminder settings and smart scheduling. A person's selected routine should remain the starting point. [Guide](https://blog.duolingo.com/duolingo-101-how-to-learn-a-language-on-duolingo/) +- **Streak tradeoffs:** A historical experiment made maintaining a streak easier and improved retention while fewer learners reached daily goals. Consistency and achievement must therefore be reported separately. [Experiment](https://blog.duolingo.com/improving-the-streak/) +- **Quality:** Its published Time Spent Learning Well metric reflects attention to the quality of activity. For AceTimeTracker, completed planned actions and the person's reported usefulness are stronger product signals than accumulated screen time alone. [Metric discussion](https://blog.duolingo.com/time-spent-learning-well/) + +## Proposed engagement sequence + +1. Make the goal/session/progress loop correct and easy to use before adding personalization. +2. Introduce opt-in scheduled reminders with local quiet hours, one daily nudge cap, a direct goal action, snooze/disable, completion suppression and an inactivity pause. +3. Keep scheduled, posting-attempt, opened and completed-session events distinct; Android does not provide universal delivery certainty. +4. If the person opts in, use a simple local history-based suggestion before considering an ML model. A suggested time requires acceptance and respects quiet hours and manual choices. +5. Evaluate complete goal periods and useful returns, with opt-out/frequency pressure as guardrails. Use credible controls before claiming reminder-driven improvement. + +The integrated spec fixes its own defaults: 21:00–08:00 quiet hours, at most one automatic nudge per account/device/local day, and one explicitly requested 20-minute snooze in the same day's reminder chain. Pause after three scheduled-day automatic posting attempts without a subsequent completed session; snoozes do not add to this counter. These are proposed AceTimeTracker policies; they are not copied competitor settings. Validate denied permission, time-zone changes, daylight saving, reboot, canceled goals and delayed background execution. [Android notifications](https://developer.android.com/develop/ui/compose/notifications/notification-permission), [alarm guidance](https://developer.android.com/develop/background-work/services/alarms). + +## Data audit: verified source contracts + +Baseline commit: `340958c1ccbc9f6b57cf9e1055ae671b8da7d27a`. + +| Source | Evidence | Consequence | +| --- | --- | --- | +| `users/{uid}/categories/{id}` | [Categories.kt](../../app/src/main/java/com/example/acetimetracker/Categories.kt#L57) writes names, descriptions, goals and zero initial `timeSpent` | Keep source document identities; category names are not reliable unique keys | +| `users/{uid}.dailyTimesheetEntries` | [Timesheet.kt](../../app/src/main/java/com/example/acetimetracker/Timesheet.kt#L213) stores date, total hours, category and user ID | Description, image and interval details are not saved by this path | +| `timesheetEntries` | [Myprogress.kt](../../app/src/main/java/com/example/acetimetracker/Myprogress.kt#L224) reads user-filtered entries; no app writer exists | Progress and the manual-entry/pie-chart path cannot be assumed consistent | +| Global `Category` | [Timer.kt](../../app/src/main/java/com/example/acetimetracker/Timer.kt#L175) adds elapsed time to a name-addressed aggregate | No per-session evidence or recorded user ownership for this write | +| User profile | [Registration.kt](../../app/src/main/java/com/example/acetimetracker/Registration.kt#L49) creates Auth only; [profile editing](../../app/src/main/java/com/example/acetimetracker/EditProfileActivity.kt#L69) can access an uninitialized photo URL | Bootstrap a profile idempotently and test text-only editing | + +Other confirmed defects: repeated timer Stop/Save/Reset can recount the full elapsed time; per-entry integer-hour conversion drops sub-hour progress; manual duration ignores calendar dates; upload/save ordering can lose image association; theme is not restored at app startup; notification preference has no implementation. The generated tests cover arithmetic and package identity only. + +## Non-destructive migration requirements + +- Inventory an allowlisted pilot in staging before live migration. No live data, deployed rules, production bucket ownership or signing setup was verified by this audit. +- Import into one per-user goals/session contract; preserve the original source and a deterministic provenance mapping. +- Imported categories require routine setup before scheduled completion or reminders, because the old source supplies no cadence, routine days or historical time zone. +- Keep summary-only records explicitly incomplete. Do not manufacture endpoints, historical time zones, descriptions, image ownership or individual sessions from aggregates. +- Preserve unowned global `Category.totalTime` untouched. Similar names do not establish user ownership. +- Repeated imports must not duplicate records. Preserve multiplicity actually present in a source; do not merge records across sources just because date/category/duration match. +- Keep malformed or ambiguous history visible in a review report and separate from verified totals. +- Reconcile late queued writes from older offline clients. A successful snapshot/import does not prove that every older device has synchronized. +- Do not delete original data or deploy live rules as part of documentation. Stage code, rules, indexes, dry-run evidence and a rollback/recovery plan before cutover. + +Firestore's documented [offline cache](https://firebase.google.com/docs/firestore/manage-data/enable-offline), [array operations](https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array), [transaction behavior](https://firebase.google.com/docs/firestore/manage-data/transactions), [ownership rules](https://firebase.google.com/docs/firestore/security/rules-conditions) and [indexes](https://firebase.google.com/docs/firestore/query-data/indexing) constrain the migration. Server-side import code must use its own explicit user/write limits because administrative access bypasses normal client rules. + +## Android foundation evidence + +Use a current compatible release set, with exact pins and build evidence in P1. The integrated spec proposes AGP 9.3.3, Gradle 9.5.0, Kotlin/Compose compiler 2.4.21, JBR 21, compile SDK 37, initial target SDK 36 and minimum SDK 27. Its compatibility qualification is explicit: Kotlin's published tested AGP upper bound is 9.3.1; the newer patch choice must be demonstrated by a real build. [AGP 9.3](https://developer.android.com/build/releases/agp-9-3-0-release-notes), [Kotlin](https://kotlinlang.org/docs/gradle-configure-project.html). + +Current Android dependencies cannot be upgraded independently of SDK/AGP metadata. The [Compose BOM 2026.09.00](https://dl.google.com/dl/android/maven2/androidx/compose/compose-bom/2026.09.00/compose-bom-2026.09.00.pom) and [stable Navigation 3](https://developer.android.com/jetpack/androidx/releases/navigation3) are proposed for the new UI. Use [built-in Kotlin migration](https://developer.android.com/build/migrate-to-built-in-kotlin) and [Firebase main-module Kotlin APIs](https://firebase.google.com/docs/android/kotlin-migration), rather than carrying obsolete configuration forward. + +Compose migration is incremental and screen-based. Repositories and local persistence isolate UI from Firebase and make timer/progress correctness testable. [Compose migration](https://developer.android.com/develop/ui/compose/migrate/strategy), [architecture recommendations](https://developer.android.com/topic/architecture/recommendations), [offline-first data layer](https://developer.android.com/topic/architecture/data-layer/offline-first). + +## Research chats + +- Competitor/engagement research: `01a15363-d945-78c1-82ca-eff30cfed112`. +- Data/migration audit: `01a15363-dcba-7311-bbb0-f29566dbb9c3`. + +Both chats completed read-only research. Their findings were reconciled into the broader personal-goals scope in the integrated specification. Android implementation and iOS work have not started. diff --git a/docs/superpowers/plans/2026-10-19-p1-android-foundation.md b/docs/superpowers/plans/2026-10-19-p1-android-foundation.md new file mode 100644 index 0000000..2b5d2c2 --- /dev/null +++ b/docs/superpowers/plans/2026-10-19-p1-android-foundation.md @@ -0,0 +1,151 @@ +# P1 Android Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans in the dedicated implementation chat. Steps use checkbox (`- [ ]`) syntax for tracking. The owner selected separate chats and one PR per implementation task; preserve that method. Use a fresh reviewer before each implementation PR is handed back. + +**Goal:** Establish a reproducible Compose-ready Android build and CI, then raise the target SDK to 36 with the existing screen navigation and system-bar behavior working. + +**Architecture:** Keep the existing single app module and Firebase data contracts. First replace the build/dependency configuration while retaining target 34; then add a small temporary compatibility boundary for legacy View activities and raise the target to 36. P5 later replaces those screens with the Compose application shell, and P11 removes the temporary legacy boundary. + +**Tech Stack:** AGP 9.3.3, Gradle 9.5.0, JDK 21, built-in Kotlin/Compose compiler 2.4.21, compile SDK 37, minimum SDK 27, Compose BOM 2026.09.00, Firebase BOM 35.0.2. + +**Spec:** [Approved Android design](../specs/2026-10-19-android-modernization-design.md), especially sections 4, 7 and 8. + +**Status:** Approved by the owner in this chat on 19 October 2026 with “Approve P1 and begin implementation.” The specification is also approved. Execute Task 1 first, then Task 2; no further design/plan approval is pending for P1. + +**Execution evidence:** P1's assigned deliverables are complete. Task 1 is verified in [PR #4](https://github.com/MphoCodes/AceTimeTracker/pull/4), head `570b8dd0875f75a3985c4d0eb05470cfa0a116d5`, with local/clean-source host checks, local API 27 and submitted-head host/API 27 CI passing ([run](https://github.com/MphoCodes/AceTimeTracker/actions/runs/39234042052)). Task 2 is verified in [PR #5](https://github.com/MphoCodes/AceTimeTracker/pull/5), head `2626b7c0cb974b2aa271fcdd2652302ed6cf7428`: all five host checks, local API 27/36/37 regressions, modern gesture/three-button/expanded-window walkthroughs and submitted-head host/API 27/36 CI pass ([run](https://github.com/MphoCodes/AceTimeTracker/actions/runs/39237635970)). Both PRs are ready for review and remain unmerged. PR descriptions hold exact runtime/review evidence. The full Android goal continues with P2–P11. + +**Practical limits:** Lint retains 204 warnings, zero errors, including legacy GIF native-alignment warnings for release-stage review. The generated arithmetic test is build smoke only. Authenticated Timesheet description-field visibility was not exercised without credentials; no predecessor comparison establishes a regression. P2's isolated Firebase harness explicitly owns focused Timesheet/profile verification. These results do not claim account, timer, migration, full adaptive layout or product redesign complete. Expanded-window evidence uses display/density overrides, not freeform/split-screen. + +## Global Constraints + +- Retain application ID and namespace `com.example.acetimetracker`, `minSdk 27`, and the existing Firebase account/project configuration. +- Use `compileSdk 37`; P1 finishes at `targetSdk 36`. Task 1 retains target 34 solely to isolate build changes from runtime behavior changes in Task 2. +- Run Gradle on JDK/JBR 21 and compile Java/Kotlin to bytecode 17. Do not commit a machine-specific Java/SDK path. +- Pin exact stable artifact/plugin versions in `gradle/libs.versions.toml`; use the same Kotlin version for the built-in KGP runtime and Compose compiler plugin. The root buildscript may repeat that single version where required by plugin bootstrap ordering; verify they match. +- No permanent built-in-Kotlin/AGP DSL opt-out flags, dynamic dependency versions, preview ConstraintLayout dependencies, disabled lint errors, or broad new lint baselines. +- Preserve current product/data behavior. No auth redesign, data migration, timer correction, reminder implementation, production Firebase writes, store publication, PR merging or iOS work belongs to P1. +- One implementation task produces one PR. Task 2 waits for Task 1. P2 waits for both. Use a dedicated implementation chat per task and avoid simultaneous writers to a shared checkout. + +## Review Focus + +1. **Fresh checkout without Java/SDK defaults:** document and prove local SDK/JDK selection; a warm IDE cache is not evidence of reproducibility. Task 1 steps 1, 2 and 7 own this. +2. **Published versions with incompatible metadata:** fail on SDK/AGP/compiler mismatch rather than hiding it; artifact publication alone is not a passing build. Task 1 steps 3–7 own this. +3. **Oldest supported device and existing resource/library loading:** compile the test APK and run device smoke checks at API 27; keep the app ID, launcher and existing layouts. Task 1 step 8 and Task 2 step 6 own this. +4. **System bars, cutouts and keyboard transitions:** content must not gain cumulative padding, receive double insets, or stay raised after the keyboard hides. Task 2 steps 1–4 and 6 own this. +5. **Drawer Back versus leaving the app:** Back closes an open drawer first; both system Back and visible Back buttons still navigate after targeting 36. Task 2 steps 1, 4 and 6 own this. + +## Version and environment decisions + +| Item | Exact value | +| --- | --- | +| Android Gradle Plugin | `9.3.3` | +| Gradle | `9.5.0-bin` | +| Gradle ZIP SHA-256 | `553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746` | +| Kotlin/KGP and Compose compiler | `2.4.21` | +| Google Services plugin | `4.5.0` | +| Compose BOM | `2026.09.00`, imported for production and Android-test configurations | +| Activity Compose | `1.13.0` | +| AndroidX Core | `androidx.core:core:1.19.1`; its KTX APIs are merged into the main artifact | +| AppCompat / XML ConstraintLayout | `1.8.0` / `2.2.2` | +| Firebase BOM | `35.0.2`; use unversioned main Auth/Firestore/Database/Storage modules under the BOM | +| Android test runner / JUnit extension / Espresso | `1.7.0` / `1.3.0` / `3.7.0` | +| Local unit JUnit | Retain `4.13.2` | +| SDK Manager platform package | `platforms;android-37.0`, revision 2 in the checked official manifest; do not assume `platforms;android-37` exists | +| Build Tools | `36.0.0`, the documented AGP 9.3 default | +| Command-line tools | Revision `23.0.0`, official archive build `16111833` | + +Keep existing Material Components `1.12.0`, GIF drawable `1.2.28`, Glide `4.16.0`, MPAndroidChart `v3.1.0`, Lottie `5.2.0`, and existing independently versioned legacy navigation/lifecycle dependencies pinned during Task 1 unless a real compatibility failure requires a separately documented change. Remove only the duplicate/alpha ConstraintLayout declarations and unused alpha Compose ConstraintLayout dependency in this task. Do not install Room, DataStore, WorkManager or Navigation 3 ahead of their implementation stages. + +Published artifact checks were performed on 19 October 2026. Kotlin's tested AGP range ends at 9.3.1; the selected newer 9.3.3 patch has now passed this repository's Task 1 real host, clean-source and API 27 checks. This is repository evidence, not an expanded vendor certification. Sources: [AGP](https://developer.android.com/build/releases/agp-9-3-0-release-notes), [Kotlin](https://kotlinlang.org/docs/gradle-configure-project.html), [Gradle checksum](https://services.gradle.org/distributions/gradle-9.5.0-bin.zip.sha256), [Google Services](https://firebase.google.com/docs/android/google-services-plugin-and-file), [Firebase Kotlin migration](https://firebase.google.com/docs/android/kotlin-migration), [SDK manifest](https://dl.google.com/android/repository/repository2-4.xml). + +### Local and CI bootstrap + +Use an available JDK 21; Android Studio's bundled JBR was verified locally. Set it only in the task shell. The old Gradle 8.0 wrapper cannot be assumed to run on that runtime. Bootstrap Gradle 9.5.0 from its official ZIP, verify the SHA-256 above, and generate the four wrapper files in a temporary empty project before copying them into this checkout. Restore executable permission on `gradlew` and include the distribution checksum in wrapper properties. + +Use a task-specific SDK directory if no local SDK exists. Install the matching official command-line-tools archive into `cmdline-tools/23.0`; expose that SDK using the standard Android environment variables. Install platform `37.0`, Build Tools `36.0.0`, and platform-tools through that SDK Manager. Verify that AGP actually resolves compile SDK 37 from the installed platform before declaring setup complete. A package-name mismatch is a diagnosed failure, not permission to lower the approved compile SDK. + +Official archive names and checksums recorded from Google's repository manifest: + +| Host | Archive below `https://dl.google.com/android/repository/` | Published SHA-1 integrity value | +| --- | --- | --- | +| Linux x86-64 CI | `commandlinetools-linux-16111833_latest.zip` | `e025545c62a8e64c7559119566a569fb1dec5f60` | +| macOS x86-64 | `commandlinetools-mac_x86_64-16111833_latest.zip` | `112cf9618794a997ff273537d55bee02c22abffe` | +| macOS arm64 | `commandlinetools-mac_arm64-16111833_latest.zip` | `ad03dc49bfacfd52c110b14104ea548b8a07e830` | + +For CI, use Ubuntu 24.04 with a clean task-specific SDK instead of relying on the runner's preinstalled CLI/platform layout. Pins resolved from the publishers' Git refs on 19 October 2026: + +- `actions/checkout` v6: `d23441a48e516b6c34aea4fa41551a30e30af803`. +- `actions/setup-java` v5: `b6effb05e454b25005698d916606bdc6ffcbf961`, distribution `temurin`, Java `21`. +- `gradle/actions/setup-gradle` v5: `0723195856401067f7a2779048b490ace7a47d7c`; let it own Gradle caching/wrapper validation. +- For the device-test job, `ReactiveCircus/android-emulator-runner` v2: `a421e43855164a8197daf9d8d40fe71c6996bb0d`; inspect its pinned action inputs before use. + +Execution corrected the original annotated tag-object SHAs to the publisher-verified commit targets above. Linux API 27 uses the published `default;x86_64` image; no `google_apis;x86_64` API 27 package is published. These rulings preserve the chosen version families and actual minimum-device coverage. + +The workflow runs on pull requests, pushes to `main`, and manual dispatch, with `contents: read`, a 30-minute host-build timeout, workflow/ref concurrency and cancellation of superseded runs. It needs no signing or production-service secrets. Do not use `pull_request_target`. + +## Branch and PR sequencing + +The approved docs are currently on `codex/android-modernization-spec` in PR #1. Recheck its head and merge state before execution. If PR #1 is still open, start Task 1 from that exact head on `codex/p1-toolchain-ci` and target the docs branch so the implementation PR contains only Task 1. If it has merged, branch from updated `main` and target `main`. Record the chosen base in the PR description. + +Task 2 uses `codex/p1-target36-compatibility`, based on the verified Task 1 head, and targets its predecessor until integration permits retargeting to `main`. Do not merge anything automatically. Attach every created PR to its implementation chat and report its URL to the coordinator through the available thread status. + +## Task 1: Reproducible Compose-ready toolchain and CI + +**Files:** +- Create: `gradle/libs.versions.toml`, `.github/workflows/android.yml`, `docs/development/android-setup.md`. +- Create: `app/src/debug/java/com/example/acetimetracker/FoundationPreview.kt` as a compile-only Material 3/Compose preview, with no launcher or production route. +- Modify: `build.gradle.kts`, `app/build.gradle.kts`, `gradle/wrapper/gradle-wrapper.properties`, `gradle/wrapper/gradle-wrapper.jar`, `gradlew`, `gradlew.bat`. +- Modify: `app/src/main/AndroidManifest.xml`, `app/src/main/java/com/example/acetimetracker/Login.kt`, `app/src/main/res/values/strings.xml` only for the specific build/lint compatibility fixes below. +- Conditional compiler-only compatibility edits: `TimeUtils.kt` and `Timesheet.kt` if the updated SDK/compiler reports nullable `SimpleDateFormat.parse` results; preserve their existing catch/error behavior. + +**Interfaces:** +- Consumes: the existing app ID, namespace, layouts, resource IDs, Firebase configuration, launcher and activity navigation. +- Produces: executable Gradle 9.5.0 wrapper; version catalog; Compose-enabled app configuration; `@Composable internal fun FoundationPreview(): Unit`; reproducible host CI checks. The preview is debug-only and is not the P5 design system. + +- [x] **Step 1: Record the starting state.** Confirm the approved spec/plan, PR base, clean checkout, JDK version, SDK availability and current wrapper permissions. Capture the current baseline command outcome. Do not represent the existing arithmetic/package tests as feature coverage. +- [x] **Step 2: Prepare the local bootstrap.** Provision the task SDK/JDK paths and verified temporary Gradle 9.5.0 distribution described above, then generate/copy the wrapper. Run `./gradlew --version`; it must report Gradle 9.5.0 and JVM 21. Keep cache/install directories outside tracked source. +- [x] **Step 3: Define the version catalog.** Add exact pins from the version table and aliases for existing retained dependencies. Define plugin aliases for AGP, Compose compiler and Google Services. Do not convert stable legacy libraries to unrequested new major lines. +- [x] **Step 4: Migrate build configuration.** Remove `org.jetbrains.kotlin.android`; use built-in Kotlin with an explicit root `kotlin-gradle-plugin:2.4.21` runtime override and matching Compose plugin. Set compile SDK 37, minimum 27, temporary target 34 and Java/Kotlin bytecode 17. Enable Compose alongside View Binding. Import the Compose and Firebase BOMs; add BOM-managed Compose UI, Material 3 and preview/tooling support. Keep Compose test artifacts BOM-managed. Flatten the nested dependency block and remove the named duplicate/alpha dependencies. +- [x] **Step 5: Make only identified source/resource compatibility changes.** Migrate Login's Firebase KTX imports to `com.google.firebase.auth.auth` and `com.google.firebase.Firebase`. Remove the manifest's obsolete package attribute and unused `.timetracker` activity declaration; the real `Timer` activity stays. Remove the stray raw sentence outside a string element in `strings.xml`, preserving the actual About string. If compiler nullability diagnostics require it, use `requireNotNull` inside the existing time-parsing `try` blocks so their existing error return remains intact. No new temporal behavior belongs here. +- [x] **Step 6: Compile a real Compose function.** Add a debug-only `FoundationPreview()` annotated with `@Preview` and `@Composable`, using Material 3. Build it with the same compiler/BOM as the app. Do not add a production screen, fake goal data, or tests that merely assert this preview's static text. +- [x] **Step 7: Run the host acceptance commands.** Run `./gradlew --no-daemon --stacktrace :app:checkDebugAarMetadata :app:assembleDebug :app:assembleDebugAndroidTest :app:lintDebug :app:testDebugUnitTest`. Require successful task exits; inspect both lint and test reports. Fix only introduced configuration/compilation failures within this task's contract. Report existing lint warnings accurately; do not bypass errors. If the pinned Kotlin/AGP patch combination fails, diagnose it and document a justified compatible version adjustment before proceeding. +- [x] **Step 8: Add CI and setup instructions.** Implement the host workflow with the pinned actions/SDK steps and the exact checks in step 7. Document local JDK/SDK selection, package IDs, wrapper checksum, setup commands and report locations. Run those instructions against a clean source checkout using the same task cache, so untracked project files do not mask a missing input. Run the existing package-name instrumentation smoke on a clean API 27 test emulator; record device/runtime and result. Credentials must not be entered and live Firebase records must not be changed. +- [x] **Step 9: Commit, review and open the Task 1 PR.** Commit with `build: establish Compose-ready Android toolchain and CI`. Have a fresh reviewer inspect the diff and actual check output, then push `codex/p1-toolchain-ci` and open/attach its PR. Wait for the submitted head's host CI result; document any unavailable device execution as unavailable rather than passing. Task 1 acceptance requires the checks above, not merely version-file edits. Leave target SDK 34 explicit in the PR; the target-36 behavior change is Task 2. + +## Task 2: Target SDK 36 with legacy screen compatibility + +**Files:** +- Create: `app/src/main/java/com/example/acetimetracker/LegacyActivity.kt`. +- Create: `app/src/main/java/com/example/acetimetracker/core/ui/LegacyWindowInsets.kt`. +- Create: `app/src/androidTest/java/com/example/acetimetracker/LegacyWindowInsetsTest.kt` and `MainActivityBackTest.kt`. +- Modify: `app/build.gradle.kts`, `app/src/main/res/values/ids.xml`, `app/src/main/res/layout/activity_main.xml`, `.github/workflows/android.yml`, `docs/development/android-setup.md`. Both `fitsSystemWindows` consumers are in `activity_main.xml`; preserve navigation-header design padding. +- Modify inheritance in all existing View activities: `Splash.kt`, `Registration.kt`, `Login.kt`, `MainActivity.kt`, `Categories.kt`, `Timer.kt`, `Timesheet.kt`, `Myprogress.kt`, `GraphActivity.kt`, `profile.kt`, `EditProfileActivity.kt`, `settings.kt`, `About.kt`. +- Modify Back handling in `MainActivity.kt` and the existing visible Back button callbacks in the activity files above. + +**Interfaces:** +- Consumes: Task 1's verified catalog, wrapper, Compose-ready build and existing resource/navigation contracts. +- Produces: `abstract class LegacyActivity : AppCompatActivity`; `internal fun installLegacyWindowInsets(content: ViewGroup): Unit`; all existing View activities use this temporary boundary; final P1 target SDK 36. P5's Compose shell will own its own insets/navigation and must not receive both boundaries. + +**Runtime rulings:** API 27 platform dispatch cannot preserve synthetic typed IME/cutout fixtures, so those assertions run on 30+ while real API 27 bars, keyboard bounds/focused fields and Back checks remain required. The keyboard check compares usable child bounds with the actual keyboard top after initial insets settle. Predecessor comparison proved the new boundary obscured Login's focused password; the minimal additional `activity_login.xml` ScrollView preserves that existing behavior. Registration's confirmation field passes without a layout change. These are compatibility corrections within the approved task, not a new auth design. + +- [x] **Step 1: Write meaningful regression tests before the compatibility code.** Define the following Android instrumentation tests, initially failing to compile/run because the new boundary does not exist. Use an actual `FrameLayout` and dispatched `WindowInsetsCompat` for helper tests; assert applied View padding rather than a duplicate implementation of the padding formula. Use `ActivityScenario` for drawer behavior, without entering credentials or saving records. Each helper test starts with padding `(3,5,7,11)`; system-bar fixture top is `24`, bottom is `16`, and left cutout is `30`. + +| Test | Action and required assertions | +| --- | --- | +| `appliesSystemBarsAndCutoutOnce` | Dispatch the fixture; `assertEquals(33, view.paddingLeft)`, `assertEquals(29, view.paddingTop)`, `assertEquals(7, view.paddingRight)`, `assertEquals(27, view.paddingBottom)` | +| `imeUsesMaximumBottomInsetAndResets` | Add IME bottom `250`; `assertEquals(261, view.paddingBottom)`; hide IME and `assertEquals(27, view.paddingBottom)` | +| `redispatchAndReinstallDoNotAccumulate` | Dispatch twice and install the listener twice; assert padding remains `(33,29,7,27)` using the four assertions above | +| `hiddenBarsRestoreInitialPadding` | Remove bar/cutout/IME insets; assert padding returns to `(3,5,7,11)` using four per-side assertions | +| `backClosesDrawerBeforeFinishing` | Open drawer without animation, invoke Back dispatcher; `assertFalse(drawer.isDrawerOpen(GravityCompat.START))` and `assertFalse(activity.isFinishing)`; invoke Back again and assert the scenario reaches the normal destroyed/exit state | +- [x] **Step 2: Demonstrate the failing regression checks.** Run `./gradlew :app:assembleDebugAndroidTest` and the targeted connected tests where available; record the missing boundary or old drawer behavior. An unrelated SDK/download error is not the expected regression failure. +- [x] **Step 3: Implement the temporary legacy boundary.** In `LegacyActivity.onCreate`, use supported edge-to-edge configuration. Override the existing layout-resource `setContentView(Int)` path to install `installLegacyWindowInsets` on `android.R.id.content` after inflation. The helper stores original padding once using a keyed ID, applies system-bar/display-cutout insets plus the maximum of bottom-bar/IME insets, and forwards only unhandled inset types to children. Request initial dispatch. Do not add an edge-to-edge opt-out. Remove the two existing `fitsSystemWindows` consumers so legacy content has one inset owner. +- [x] **Step 4: Wire activities and Back handling.** Change the thirteen View activity base classes to `LegacyActivity`. Replace `MainActivity.onBackPressed` interception with an enabled `OnBackPressedDispatcher` callback that closes an open drawer first and delegates normal navigation with the callback temporarily disabled otherwise. Visible Back buttons invoke `onBackPressedDispatcher.onBackPressed()`. Preserve their destinations and account/data behavior; do not add an auth rewrite. +- [x] **Step 5: Raise the target and rerun host checks.** Set target SDK 36, leaving compile 37/minimum 27. Run all Task 1 host checks again; require successful results. No manifest Back opt-out, orientation restriction, disabled lint error or system-bar opt-out is an acceptable substitute for the compatibility change. +- [x] **Step 6: Verify on supported runtimes.** Extend CI with targeted instrumentation on clean API 27 and API 36 x86-64 emulators using the pinned emulator action and JDK/SDK setup. Run `LegacyWindowInsetsTest`, `MainActivityBackTest`, and the existing package-name smoke. Use a known supported system-image package from SDK Manager rather than guessing a minor-version package name. Also run an API 37 device/emulator walkthrough: launcher, registration/login form, open/close drawer, visible Back buttons, gesture and three-button navigation, keyboard show/hide, and an expanded window. Record runtime, navigation mode and evidence; these checks do not submit credentials or write production Firebase data. Broader legacy UX defects remain visible for P5 rather than being declared fixed by this check. +- [x] **Step 7: Commit, review and open the Task 2 PR.** Commit with `fix: preserve legacy navigation and insets when targeting Android 16`. Obtain a fresh review of the regression tests, changed behavior and real reports; then push/open/attach `codex/p1-target36-compatibility` against its verified predecessor. Require the submitted head's host and device CI to pass. P1 is complete only when both PR deliverables and their evidence are ready for coherent integration. + +## Completion and next handoff + +Record each PR URL, exact head SHA, host/device results and any remaining limitations in its description. Do not call P1 complete when dependency publication is the only evidence or a required runtime check is still missing. P2's account/profile and app-session behavior is a separate plan and PR; timer/data-model and Compose product-screen changes remain in their approved later stages. + +Both implementation chats completed their bounded deliverables. The coordinator independently inspected reports, submitted-head CI, representative screenshots and source boundaries. A Git merge-tree simulation combined the current docs with Task 2 (which includes Task 1) without conflict; its diff from the verified code head contains only documentation. No branch/checkout/index or PR was merged. Integration order is documentation PR #1, toolchain PR #4, then compatibility PR #5, with bases rechecked at integration time. The next action is review of the proposed P2 plan; P2 code has not started. diff --git a/docs/superpowers/plans/2026-10-19-p2-accounts-and-profiles.md b/docs/superpowers/plans/2026-10-19-p2-accounts-and-profiles.md new file mode 100644 index 0000000..c24a80f --- /dev/null +++ b/docs/superpowers/plans/2026-10-19-p2-accounts-and-profiles.md @@ -0,0 +1,138 @@ +# P2 Accounts and Profiles Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans in a dedicated implementation chat for each task. Steps use checkbox (`- [ ]`) syntax for tracking. Preserve the owner's separate-chat, one-PR-per-task method and obtain a fresh independent review before handing back each PR. + +**Goal:** Make existing accounts, profile editing and authenticated routing correct and testable before migrating the product screens to Compose. + +**Architecture:** Keep P1's legacy activity/inset boundary and XML controls. Introduce a small application container, Firebase adapters and lifecycle-aware ViewModels with immutable StateFlow state. A shared session coordinator binds operations to a UID and session generation; profile operations preserve the legacy root document and remain separate from authentication credentials. + +**Tech Stack:** P1's verified toolchain and Firebase BOM 35.0.2; Lifecycle runtime/ViewModel 2.11.0; kotlinx-coroutines Android/Play Services/test 1.11.0; JUnit 4.13.2 and existing Android test libraries. Isolated Firebase tests use Node 24.22.0, firebase-tools 15.34.0, firebase-admin 14.5.0 and JDK 21. + +**Spec:** [Approved Android design](../specs/2026-10-19-android-modernization-design.md), especially Onboarding and accounts, Architecture, P2 and the verification contract. + +**Status:** Proposed for owner review. P1 Task 1 is verified in [PR #4](https://github.com/MphoCodes/AceTimeTracker/pull/4) at `570b8dd0875f75a3985c4d0eb05470cfa0a116d5`; Task 2 is verified in [PR #5](https://github.com/MphoCodes/AceTimeTracker/pull/5) at `2626b7c0cb974b2aa271fcdd2652302ed6cf7428`, with all required runtime/CI checks complete. Recheck both heads/merge states before implementation. This document does not authorize P2 implementation before plan review. + +## Global Constraints + +- Retain application ID/namespace `com.example.acetimetracker`, `minSdk 27`, `compileSdk 37` and P1's final `targetSdk 36`. +- Preserve existing Firebase accounts, email/password registration/sign-in/reset and existing profile URLs. Keep production Firebase client configuration unchanged. +- `users/{uid}` contains profile fields and legacy `dailyTimesheetEntries`, `minGoal`, `maxGoal` and possibly unknown fields. Profile bootstrap and editing must preserve unrelated data; P2 must not mark canonical migration complete. +- Existing `email` is the profile's contact/display email. FirebaseAuth is authoritative for sign-in email. Name/photo/contact edits never update sign-in credentials. +- Empty UID fails before constructing document or Storage paths. Operations capture their originating UID/session generation and reject stale callbacks after sign-out/account replacement. +- Use the existing XML controls with bounded correctness/loading/error changes. Compose navigation/design tokens, DataStore preferences, canonical goals/sessions, Room/outbox, durable timer checkpoints and reminders remain P3–P10 work. +- Keep the existing `app_settings` preference keys `theme` and `notifications`. Do not implement guest accounts, social login or new account deletion flows. +- All automated account/data tests use the isolated demo project. No production service writes, rules deployment, PR merge, publication or iOS work. + +## Review Focus + +1. Auth may succeed before profile setup fails: retry profile setup under that UID, without repeating account creation. Task 1 owns the test. +2. Registration's auth listener can fire before its create-user task returns: bind the submitted name to the returned UID before profile initialization; never give it to another account. Task 1 owns the test. +3. A late callback from account A may arrive while account B is active: it cannot route, render or start a write using B. Both tasks own stale-result tests. +4. Existing root data and missing/malformed profile fields must survive bootstrap/edit; two devices may bootstrap concurrently. Task 1 owns preservation/concurrency tests; Task 2 owns patch tests. +5. Photo upload, download-URL lookup, profile acknowledgement and email verification are distinct outcomes. Task 2 owns failure/retry/recreation tests and truthful UI state. + +Pending authentication also has its own attempt generation. A late sign-in/create-user SDK success after explicit sign-out must not reestablish an abandoned session; existing UID/token checks alone cannot prove that. Task 1 owns this regression and startup reconciliation. + +## Shared contracts and file responsibilities + +Types belong in `app/src/main/java/com/example/acetimetracker/core/model/AccountModels.kt`: + +- `AuthIdentity(uid: String, signInEmail: String?, emailVerified: Boolean, displayName: String?)`. +- `AuthState`: `Restoring`, `SignedOut`, `SignedIn(identity: AuthIdentity)`. +- `SessionToken(uid: String, generation: Long)`. Generation changes on sign-out/account replacement, including signing back into the same UID. An ordinary identity refresh for the same session retains it. +- `UserProfile(uid: String, displayName: String?, contactEmail: String?, photoUrl: String?)`. Safe reads tolerate absent/non-string legacy profile values without overwriting them. +- `SessionState`: `Restoring`, `SignedOut`, `LoadingProfile(token, identity)`, `ProfileError(token, identity, message)`, `Ready(token, identity, profile)`; the named parameters use the types above and `message: String`. + +`core/data/account/AuthRepository.kt` produces `val state: StateFlow`, `fun currentUid(): String?`, `suspend fun register(email: String, password: String): AuthIdentity`, `suspend fun signIn(email: String, password: String): AuthIdentity`, `suspend fun sendPasswordReset(email: String): Unit`, and `fun signOut(): Unit`. `FirebaseAuthRepository.kt` implements it with Firebase callbacks/tasks. `currentUid()` reads the SDK's current identity so a queued listener cannot validate a stale token. + +`core/data/profile/ProfileRepository.kt` produces `suspend fun bootstrap(token: SessionToken, identity: AuthIdentity, registrationName: String?): UserProfile` and `fun observe(token: SessionToken): Flow`. `FirebaseProfileRepository.kt` owns Firestore paths and transactions. Bootstrap adds only absent profile keys for which an actual value is available: registration name, otherwise Auth display name; Auth email for missing contact `email`. Never fill an unknown name with invented identity. Existing explicit null/blank values and unknown fields remain unchanged. No photo is required. + +`app/SessionGuard.kt` owns token validity independently of UI state: `class SessionGuard(currentUid: () -> String?)` produces `fun bind(uid: String): SessionToken`, `fun invalidate(): Unit`, `fun currentToken(): SessionToken?` and `fun isCurrent(token: SessionToken): Boolean`. Bind requires a nonblank UID matching the SDK; it reuses the token for the same active identity, otherwise increments generation. Invalidate clears the token and advances generation. The container constructs SDK instances, this guard, then adapters and the coordinator; all use the same guard, avoiding a repository/coordinator dependency cycle. + +`app/SessionCoordinator.kt` produces `val state: StateFlow`, `fun currentToken(): SessionToken?`, `fun isCurrent(token: SessionToken): Boolean`, `suspend fun register(displayName: String, email: String, password: String): Unit`, `suspend fun signIn(email: String, password: String): Unit`, `suspend fun retryProfileBootstrap(): Unit`, and `fun signOut(): Unit`. Token methods delegate to SessionGuard. It serializes credential mutations and defers registration profile setup until the create-user result binds the submitted name to its UID. Restored sessions use the initial AuthStateListener result. Invalidate a session before SDK sign-out; cancel old profile observation and reject results whose token no longer matches the SDK UID/generation. + +`app/AuthAttemptRegistry.kt` produces `fun begin(): Long`, `fun invalidate(): Unit` and `fun isCurrent(attempt: Long): Boolean`, independently of session tokens. The coordinator owns SDK credential operations in its application scope and serializes them until their underlying Tasks are terminal, even if a UI waiter cancels. During an attempt, buffer auth listener events until its result is reconciled. Sign-out invalidates the attempt immediately. A late abandoned success triggers SDK sign-out before another credential mutation starts; do not bootstrap, navigate or publish Ready from it. Reject listener payloads that no longer match the SDK's current UID. + +`app/PendingAuthCancellationStore.kt` produces `fun markAbandoned(): Unit`, `fun needsReconciliation(): Boolean` and `fun clear(): Unit`, storing only a cancellation marker in private `account_session` preferences, never credentials or a replacement login boolean. Persist the marker before sign-out abandons an unresolved SDK credential operation. At startup reconcile that marker by clearing any SDK user before accepting restoration; remove it only after reconciliation. Normal restoration still comes from Firebase Auth. This closes the process-recreation window between a late SDK success and cleanup; it does not implement P4's tracking outbox. + +`app/AppContainer.kt` constructs these adapters/coordinator and ViewModel factories without adding a DI framework. `app/AceTimeTrackerApplication.kt` owns the application scope/container and Firebase initialization. `app/SessionRouter.kt` produces `fun openSignedOut(activity: Activity): Unit` (Login) and `fun openReady(activity: Activity): Unit` (MainActivity), using `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK` for account-boundary transitions. Login gains a `Create account` link to Registration; Registration retains its Login link. Guard navigation against repeated observations of the same destination/session. + +`app/AccountActivity.kt` extends `LegacyActivity`, exposes `protected fun requireSessionOrRoute(): SessionToken?` and `protected fun isCurrentSession(token: SessionToken): Boolean`, and watches session invalidation while started. Every protected activity calls the first function immediately after `super.onCreate`, before inflation/data access, and returns if it yields null. A lost session clears sensitive views/listeners and routes through the session gate. Account-specific writes use the captured token UID; callbacks validate it before rendering or chaining another request. + +## Isolated test environment + +Task 1 introduces `firebase.test.json`, `test-support/firebase/firestore.profile.rules`, `test-support/firebase/storage.profile.rules`, `test-support/firebase/package.json`, its lockfile, `seed.mjs`, and `run-tests.sh`. The fixed test project is `demo-acetimetracker-p2`; Auth/Firestore/Storage ports are `9099`/`8080`/`9199`. Android emulators connect through `10.0.2.2`; host tooling uses `127.0.0.1`. + +Enable the Android test environment only with debug property `-PfirebaseEmulators=true`, compiled to a debug BuildConfig boolean; release is always literal `false`. Remove `FirebaseInitProvider` only in `app/src/debug/AndroidManifest.xml`. The Application manually initializes the default FirebaseApp using demo FirebaseOptions with bucket `demo-acetimetracker-p2.appspot.com` in emulator mode, then configures all used SDK endpoints before constructing the container or starting activities. Use memory-only Firestore cache in this test mode and server readback for acknowledgement assertions. Normal debug initializes from existing resources; release retains normal initialization. Debug network configuration permits only the emulator hosts' cleartext traffic. Fail startup if emulator mode sees a preinitialized non-demo default app, mismatched project/bucket/hosts/ports or incomplete configuration; never fall back to production. A named demo app alone is insufficient because legacy code still accesses the default SDK instances. + +Host fixture tooling checks the exact demo project/bucket and all three emulator environment variables before importing/initializing firebase-admin. It seeds deterministic users A/B and legacy root documents using Admin only for fixtures. Authorization assertions use authenticated Android clients. The fixture rules allow each account to read its own root and create/update only `displayName`, `email`, `profilePictureUrl`; updates preserve every other field. Storage allows user-owned image paths introduced in Task 2. These are versioned test fixtures, not the P3 production rules contract. Never deploy them. + +Pin package versions and commit the npm lockfile. Use `npm ci`, JDK 21 and `firebase emulators:exec --project demo-acetimetracker-p2 --config firebase.test.json --only auth,firestore,storage` to seed and run the device suite. `run-tests.sh` owns the complete command, fails on missing demo guards/emulators, and accepts an optional Android serial. Reuse P1's external SDK/Gradle installations and run builds/device tasks sequentially per checkout. + +Default Firestore database only; explicitly load the checked-in rules. Seed tooling sets `FIREBASE_AUTH_EMULATOR_HOST`, `FIRESTORE_EMULATOR_HOST`, `FIREBASE_STORAGE_EMULATOR_HOST` and `GCLOUD_PROJECT` before Admin SDK construction. Cancelling `Task.await()` cancels waiting and does not guarantee remote operation cancellation, so token checks remain mandatory after every asynchronous boundary. + +## Branch and PR sequence + +P2 waits for both P1 tasks. Start `codex/p2-account-routing` at the verified P1 Task 2 head; start `codex/p2-profile-editing` only after Task 1 verification, at that exact head. While predecessors remain open, target their branches; if integrated, verify updated main and use it instead. Never merge automatically. The coordinator's approved plan may be newer than the code predecessor: read it from the coordinator checkout or its exact docs commit, record that docs SHA, and do not silently execute an older plan inherited by the code branch. + +## Task 1: Correct authentication, profile bootstrap and session routing + +**Files:** +- Create the shared model/repository/container/coordinator/router/guard files above and `feature/auth/AuthViewModel.kt`. +- Modify `AndroidManifest.xml`, debug manifest/network resources, `app/build.gradle.kts`, `gradle/libs.versions.toml`, `.github/workflows/android.yml` and `docs/development/android-setup.md` for required dependencies, initialization and the isolated test environment. +- Modify `Splash.kt`, `Login.kt`, `Registration.kt`; add `activity_session_gate.xml`; update `activity_login.xml`, `activity_registration.xml`, `dialog_forgot_password.xml` and `strings.xml` for loading/retry/field errors. +- Modify protected activity guards and captured-UID callbacks in `MainActivity.kt`, `Categories.kt`, `Timer.kt`, `Timesheet.kt`, `Myprogress.kt`, `GraphActivity.kt`, `profile.kt`, `EditProfileActivity.kt`, `settings.kt` and `About.kt`. Retain existing business calculations/storage paths here. +- Create `SessionCoordinatorTest.kt` and `AuthViewModelTest.kt` under `app/src/test/java/com/example/acetimetracker/feature/auth/`; create `AccountRoutingTest.kt` and `ProfileBootstrapEmulatorTest.kt` under `app/src/androidTest/java/com/example/acetimetracker/`. +- Create `AuthenticatedFormInsetsTest.kt` in the same Android test package. Conditional compatibility-only fixes may touch `activity_timesheet.xml` and `activity_edit_profile.xml` if the focused-field checks reveal obstruction; retain their controls/data behavior. +- Adapt `MainActivityBackTest.kt` and `LegacyWindowRuntimeTest.kt` to explicitly establish signed-in/signed-out demo sessions so P1 regressions continue to run with the new guard. + +**Interfaces:** Consumes P1's final `LegacyActivity`/Back/inset behavior and produces the shared contracts above. Auth ViewModel methods are `register(displayName, email, password, confirmation)`, `signIn(email, password)`, `resetPassword(email)` and `retryProfile()` with String inputs and Unit returns. Its immutable state exposes pending action, field/error feedback and profile retry state; secrets are never stored in SavedStateHandle or logs. + +- [ ] **Step 1: Pin the verified predecessor and dependencies.** Check P1 heads/CI/merge states; create isolated `codex/p2-account-routing` from the verified Task 2 head, stacked against its predecessor while unmerged. Pin the listed dependencies only as used by this task. Record the chosen base and environment. +- [ ] **Step 2: Write failing coordinator/ViewModel tests with fake adapters.** Pin these outcomes: restored A reaches `Ready` with UID A; signed-out restoration reaches `SignedOut`; listener-before-register-result initializes A with the submitted name exactly once; create success/bootstrap failure remains `ProfileError(A)` and Retry never calls register again; duplicate submit makes one SDK request; switching A→B drops A's delayed profile result; logout→login A invalidates the old generation; a stale SDK UID makes `isCurrent(oldToken)` false even before the listener runs. Sign-out during unresolved sign-in and registration followed by late SDK success/listener events leaves SignedOut with zero bootstrap/navigation; queued B credentials start only after abandoned A is reconciled. Recreating the coordinator with the cancellation marker and SDK user A clears A before restoration. Invalid/empty fields make no SDK call; confirmation mismatch is a field error. Reset failure retains the entered email and permits retry. +- [ ] **Step 3: Demonstrate failures.** Run `./gradlew :app:testDebugUnitTest --tests '*SessionCoordinatorTest' --tests '*AuthViewModelTest'`; require missing-contract/behavior failures, not SDK installation failures. Use coroutine test dispatchers and controllable deferred completions, not sleeps or copied implementation logic. +- [ ] **Step 4: Implement the adapters, bootstrap and coordinator.** Use an online Firestore transaction for fill-missing bootstrap, preserving all unknown/root values; an offline/permission failure exposes Retry without recreating the Auth account. Collect Firebase's initial/current auth callback rather than a persisted login boolean. Stop old profile listeners, validate tokens and serialize auth mutations, including the registration-listener race. +- [ ] **Step 5: Wire the existing auth/session UI.** Remove Splash's artificial four-second delay. Signed-out launch opens Login with a Create account link to Registration; restored signed-in launch passes profile setup then opens MainActivity. Show recoverable profile initialization failure with Retry and Sign out. Keep email/name drafts across recreation, disable duplicate submits, and show pending/error feedback. Password reset dismisses the displayed dialog only on success. Registration uses the shared flow and does not send an already authenticated person back to Login. +- [ ] **Step 6: Install account boundaries in existing activities.** Apply guard-before-data-access to the listed protected screens. Logout clears the authenticated task; Back cannot reveal account A after signing out or entering B. Capture tokens in reads/uploads/writes and check before callbacks render or start another request. Stop invalidated legacy timer callbacks and disable their writes; durable pause/checkpoint recovery remains P4. Preserve existing calculation/data contracts instead of inventing canonical history now. +- [ ] **Step 7: Implement the isolated Firebase harness and integration tests.** Seed A with `displayName="Existing name"`, contact email, existing photo, `dailyTimesheetEntries`, `minGoal=1`, `maxGoal=3` and `unknownField="keep"`. Concurrent bootstrap calls and a later retry must leave every existing value equal and only fill genuinely absent keys. A cannot read/write B and signed-out access fails; a unit adapter test proves empty UID constructs no SDK path. Test missing/partial/non-string profile data without destroying it. Exercise registration, login, signed-in/out launch, invalidated auth, logout/Back and switching A→B. Retrieve the demo Auth emulator's `PASSWORD_RESET` OOB code, confirm the reset and verify old-password rejection/new-password sign-in. With demo A authenticated, focus Timesheet's `editTextDescription` and profile editing's `editEmailEditText`, assert each is reachable above the actual IME and hide restores bounds; do not Save/upload. This closes P1's authenticated-form coverage lead without production credentials. Fix measured compatibility-only obstruction before passing the check. No call may use production endpoints or send real email. +- [ ] **Step 8: Verify host and devices.** Run P1's five host acceptance checks. Run `test-support/firebase/run-tests.sh` on clean API 27 and 36 devices, including all retained P1 tests with explicit account setup. Inspect XML/lint/device reports; CI must run the pinned isolated Firebase harness and retain reports. Test app recreation during submit/profile retry and removal of password-reset dialog after success. Record exactly what is real SDK/emulator coverage versus fake state coverage. +- [ ] **Step 9: Review, commit and submit.** Obtain a fresh independent review of auth races, token checks, fixture isolation and actual reports; fix findings and rerun affected checks. Commit `fix: restore authenticated routing and bootstrap profiles safely`, push/open/attach one PR, and require submitted-head host and device CI to pass. Record exact head/base and remaining limitations; no merge. + +## Task 2: Reliable profile editing and explicit sign-in email changes + +**Files:** +- Extend `AccountModels.kt`, `AuthRepository.kt`, `FirebaseAuthRepository.kt`, `ProfileRepository.kt`, `FirebaseProfileRepository.kt` and `AppContainer.kt` with the interfaces below. +- Create `core/data/profile/ProfilePhotoRepository.kt`, `FirebaseProfilePhotoRepository.kt`, `feature/profile/ProfileViewModel.kt`, `ProfileEditViewModel.kt` and `SignInEmailViewModel.kt`. +- Modify `EditProfileActivity.kt`, `profile.kt`, `MainActivity.kt`, `activity_edit_profile.xml`, `activity_profile.xml`, `nav_header_timetracker.xml` and `strings.xml`; create `dialog_sign_in_email.xml` and `dialog_reauthenticate.xml`. +- Add `ProfileEditViewModelTest.kt` and `SignInEmailViewModelTest.kt` under `app/src/test/java/com/example/acetimetracker/feature/profile/`; add `ProfileEditingTest.kt` and `ProfileAuthorizationEmulatorTest.kt` under `app/src/androidTest/java/com/example/acetimetracker/`. Extend fixture Storage rules/seed data, CI and setup documentation with this task's checks. + +**Interfaces:** +- `ProfileFieldPatch` is sealed with `Keep` and `Set(value: String)`; `ProfilePatch(displayName: ProfileFieldPatch, contactEmail: ProfileFieldPatch, photoUrl: ProfileFieldPatch)` defaults all fields to Keep. Only explicitly edited fields become Set. Keep preserves the raw existing field, including absent/null/non-string values; Set writes the supplied string. No incidental field deletion. +- ProfileRepository adds `suspend fun update(token: SessionToken, patch: ProfilePatch): Unit`; success means server acknowledgement, not a queued offline write. Failures retain the draft/retry state. +- ProfileRepository maps model/patch `displayName` → legacy `displayName`, `contactEmail` → `email`, and `photoUrl` → `profilePictureUrl`. It never replaces the entire root document. +- ProfilePhotoRepository produces `suspend fun upload(token: SessionToken, selectionId: String, uri: Uri): String`, returning the acknowledged download URL. New images use `users/{uid}/profilePictures/{selectionId}`; never relocate/delete legacy images. Selection IDs are stable UUIDs for retry. Separate upload and URL-fetch failure state. +- AuthRepository adds `suspend fun requestSignInEmailChange(token: SessionToken, email: String): Unit`, `suspend fun reauthenticate(token: SessionToken, password: String): Unit`, and `suspend fun reloadIdentity(token: SessionToken): AuthIdentity`. The adapter validates the token before using its current FirebaseUser and after each asynchronous boundary. +- `ProfileEditViewModel` exposes immutable state and `editName(value: String)`, `editContactEmail(value: String)`, `selectPhoto(uri: Uri)`, `save()` and `retry()`, each returning Unit. Track loaded profile, edited draft, selected photo/selection ID, busy stage, error and acknowledged completion separately. +- `SignInEmailViewModel` exposes immutable `Idle`, `Sending`, `NeedsReauthentication`, `VerificationPending(email: String)` and `Error(message: String)` state, with `request(email: String)`, `reauthenticate(password: String)` and `refresh()` Unit-returning actions. VerificationPending is not a credential-change success. + +- [ ] **Step 1: Write failing profile state tests.** Text-only Save with an existing URL retains that exact URL; Save with no photo does not invent one. Photo-only and name-only saves preserve untouched absent/null/non-string sibling fields and all legacy root data. Initial loading/programmatic binding marks no field edited. Repeated Save produces one mutation; upload, download-URL and profile-write failures retain draft/retry and restore controls; a successful upload followed by failed profile acknowledgement retries the same selection without choosing another account/path. Replacing selection X with Y ignores X's delayed completion. Account A's delayed save/upload cannot render, finish or chain a write under B. UI completion occurs only after acknowledged profile update. +- [ ] **Step 2: Write failing credential tests.** Name/photo/contact edits leave Auth email unchanged. Requesting a new sign-in email calls verification-before-update and yields `VerificationPending`, while the current Auth email remains visible. Recent-login failure requests reauthentication; wrong password/cancel retains a recoverable state; successful reauth retries once. Reload determines actual completion and handles invalidated authentication. Duplicate requests and a session change cannot send under another UID. Pending UI state is keyed to its UID; passwords never enter saved state. +- [ ] **Step 3: Demonstrate failures and implement repositories/ViewModels.** Run `./gradlew :app:testDebugUnitTest --tests '*ProfileEditViewModelTest' --tests '*SignInEmailViewModelTest'` before implementation, then after. Use token-bound callbacks, an explicit loaded/draft distinction and acknowledged updates. For profile writes, use an online transaction so an offline failure remains retryable and does not create a second hidden queued mutation. Cancel local upload work on invalidation; a remote result already in flight can only belong to its original UID/path. +- [ ] **Step 4: Wire profile editing.** Remove the uninitialized photo URL and shadowing variable. Track explicit field edits separately from loaded/displayed values so binding does not replace malformed or absent legacy siblings. Selecting an image stages a selection; Save coordinates any required upload/URL lookup and profile acknowledgement. Persist document permission and UID-bound draft/selection ID for recreation; never persist a password. Replace whole-window touch blocking with relevant busy controls and an accessible error/retry state. Finish only on confirmed success. Preserve the original photo and text on failure/cancel. Do not delete existing remote images as an incidental cleanup. +- [ ] **Step 5: Clarify email and refresh profile surfaces.** Label editable legacy email `Contact email` with `Does not change your sign-in email`. Show authoritative `Sign-in email` separately and provide `Change sign-in email`, recent-login recovery, `Verification sent`/pending and Refresh feedback. Use `verifyBeforeUpdateEmail`, not a Firestore update pretending to change credentials. Observe profile state in the profile screen and drawer header while started; edits update both. Remove the hardcoded student identity and show explicit loading/missing/error states. +- [ ] **Step 6: Verify SDK authorization and image failure behavior.** Extend demo rules for owner-only `users/{uid}/profilePictures/{selectionId}` image uploads with a 10 MiB maximum. Authenticated A may use its path; B/anonymous clients and oversized/non-image writes fail. Seed old URLs and verify text-only edit preserves all root legacy values. Exercise picker cancel, recreation, upload failure, URL-fetch failure, write denial, duplicate Save and A→B callback invalidation. Existing photos remain readable by their old URL; no migration/deletion is implied. +- [ ] **Step 7: Verify the authenticated email flow.** Request verification-before-update, retrieve the matching `VERIFY_AND_CHANGE_EMAIL` record from `GET /emulator/v1/projects/demo-acetimetracker-p2/oobCodes`, redeem its code with `applyActionCode` or the emulator link, reload and assert the authoritative Auth email changed while Firestore contact email stayed unchanged. Before redemption, assert the old Auth email and pending UI. The pinned emulator supports this flow. Exercise recent-login enforcement errors with typed fakes; emulator completion does not prove production email delivery, provider abuse protection or recent-login enforcement. No real email is sent. Check actual pending/re-auth/error UI and that name/photo changes never trigger credential APIs. +- [ ] **Step 8: Run full acceptance and submit.** Run P1's five host checks, Task 1's account suite, retained P1 regressions and this task's profile/authorization tests on API 27/36. Inspect reports; perform a modern-runtime text-only/profile-photo/pending-email walkthrough. Obtain a fresh independent review, fix findings, commit `fix: make profile saves reliable and separate sign-in email changes`, push/open/attach a separate stacked PR and require submitted-head host/device CI success. Record exact base/head, endpoint isolation and limitations. Do not start P3 or merge. + +## Handoff and completion + +Recheck the final P1 head before executing this plan; adapt only mechanically to its verified form/Back/inset changes. If a new behavior decision falls outside the approved spec, state the concrete decision before implementation. A check unavailable on a runtime/provider remains unverified, never passed. + +P2 is ready when both bounded PRs, meaningful fake/SDK/emulator evidence and account-switch/profile UI checks are coherent with P1, with no unresolved important review finding. P3 then defines canonical goals/sessions and migration; it must preserve P2's root profile keys and replace test-only rules with the reviewed full data-access contract. Full offline tracking, durable timer sign-out checkpoints and sync isolation remain P4 acceptance requirements. + +## Primary references + +- [Firebase Auth state listener](https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuth.AuthStateListener), [manage users and email changes](https://firebase.google.com/docs/auth/android/manage-users). +- [Firestore transactions](https://firebase.google.com/docs/firestore/manage-data/transactions), [Auth emulator](https://firebase.google.com/docs/emulator-suite/connect_auth), [Firestore emulator](https://firebase.google.com/docs/emulator-suite/connect_firestore), [Storage emulator](https://firebase.google.com/docs/emulator-suite/connect_storage). +- [Lifecycle releases](https://developer.android.com/jetpack/androidx/releases/lifecycle), [coroutines releases](https://github.com/Kotlin/kotlinx.coroutines/releases), [Firebase CLI](https://github.com/firebase/firebase-tools), [Admin SDK](https://github.com/firebase/firebase-admin-node), [Node release index](https://nodejs.org/dist/index.json). +- Exact inputs: [Lifecycle runtime 2.11.0](https://dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime-android/2.11.0/lifecycle-runtime-android-2.11.0.aar), [coroutines 1.11.0 POM](https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.11.0/kotlinx-coroutines-android-1.11.0.pom), [CLI 15.34.0](https://registry.npmjs.org/firebase-tools/15.34.0), [Admin 14.5.0](https://registry.npmjs.org/firebase-admin/14.5.0), [CLI JDK check](https://github.com/firebase/firebase-tools/blob/v15.34.0/src/emulator/commandUtils.ts#L599), [Auth emulator email flow](https://github.com/firebase/firebase-tools/blob/v15.34.0/src/emulator/auth/operations.ts#L937), [Task.await cancellation](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/await.html). diff --git a/docs/superpowers/specs/2026-10-19-android-modernization-design.md b/docs/superpowers/specs/2026-10-19-android-modernization-design.md new file mode 100644 index 0000000..c616832 --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-android-modernization-design.md @@ -0,0 +1,267 @@ +# AceTimeTracker Android modernization: approved product and technical specification + +- Date: 19 October 2026 +- Status: Approved by the owner on 19 October 2026. P1's two PR deliverables and required checks are complete and ready for review/integration. P2's implementation plan awaits owner review; the Android modernization goal remains in progress. +- Baseline: `main` at `340958c1ccbc9f6b57cf9e1055ae671b8da7d27a`. +- Audience confirmed by the owner: **anyone building personal goals and habits**. + +## 1. Outcome and boundaries + +Make AceTimeTracker a beautiful, useful daily companion for turning personal goals into small, repeatable actions. A person should understand what to do next, start tracking quickly, trust their history, and see how that effort contributes to their chosen goals. + +The owner requested native Android modernization with Compose, competitor research including Duolingo's engagement/reminder practices, code cleanup, separate work chats, and one pull request per implementation task. Android comes first. iOS work, its implementation chat, and a detailed iOS specification are deferred until the Android goal is met. + +The owner approved this scope and these decisions on 19 October 2026. Approval is not evidence that any new behavior has been implemented. The delivery sequence is a roadmap; each stage receives its own executable implementation plan before code is written. + +### Proposed success criteria + +- A new person can create a simple goal and begin a session within 60 seconds in a usability walkthrough, excluding time they spend choosing credentials. This is a design target to test, not an observed result. +- Every new saved session appears consistently in history, goal progress, and summaries; repeated Save/Stop actions never add it twice. +- Tracking and saving an already configured goal work without connectivity, including after activity recreation and process death. +- Upgrading preserves all recoverable user-owned records and clearly distinguishes incomplete legacy summaries from precise sessions. +- Daily use is possible with notifications and optional analytics disabled. +- All primary screens use Compose; the build, behavior tests, accessibility checks, and representative device walkthroughs pass. + +### Deliberate scope choices + +The first version supports **time-based personal goals**: reading, exercise, learning, creative work, or other pursuits. A habit is a recurring plan attached to a goal. Daily and weekly duration targets are included. Arbitrary count-based habits, financial goals, public leaderboards, social networks, subscriptions, AI coaching, wearable apps, and iOS are later product decisions. + +Existing Firebase accounts remain usable. Retain the Android application ID `com.example.acetimetracker` so the project can support an upgrade rather than silently creating a different installed app. Publishing, production Firebase changes, and merging PRs require their own explicit release/integration decision; opening PRs is already authorized. + +## 2. Migration approaches considered + +| Approach | Benefit | Cost/risk | Decision | +| --- | --- | --- | --- | +| Incremental replacement in the existing Android app | Preserves identity and historical access; yields small PRs and a runnable app throughout | Requires temporary adapters between old activities and new screens | Recommended | +| Replace the entire Android app in one rewrite | Clean starting point | Delays usable results and concentrates data, lifecycle, and UI risk into one change | Reject for this modernization | +| Introduce shared Android/iOS architecture now | Could share some future work | Adds a second platform before product behavior and data contracts are stable | Defer with iOS | + +Compose and Views can coexist during migration. New screens use Compose, reusable UI components accumulate, and legacy screen layouts are removed only after their replacements are verified. This follows the [official Compose migration strategy](https://developer.android.com/develop/ui/compose/migrate/strategy). The Android manifest, string resources, and other appropriate XML resources remain valid project files. + +## 3. Product structure and interaction + +### Core loop + +Choose a meaningful goal → commit to a manageable action → start a session → save/refine the result → see progress → choose the next action. + +### Main destinations + +| Destination | Primary information | Primary action | +| --- | --- | --- | +| Today | One suggested next action, due goals, current session, today's effort | Start/resume a session | +| Goals | Active/archived goals, weekly or daily targets, progress and schedules | Create or adjust a goal | +| Progress | Session history, goal comparisons, weekly trends, optional consistency view | Understand progress or correct an entry | +| You | Profile, sync/account state, theme, reminder controls, data controls, About | Adjust preferences | + +Use bottom navigation on compact windows and an adaptive rail/list-detail arrangement when space permits. A running-session control remains reachable across destinations. Avoid navigation-click Toasts, forced waiting screens, and separate screens for redundant chart variants. + +### Onboarding and accounts + +1. Briefly communicate the benefit with one clear action. +2. Offer example personal goals and a custom goal; avoid student-only language. +3. Ask for a manageable daily or weekly time target, with editable suggestions. +4. Make the first session action immediately visible. +5. Offer reminders after the person has chosen a goal/time, with a skip option. + +Preserve email/password sign-in, registration, and password reset. Existing signed-in users resume their app state rather than always visiting registration. Initialize the Firestore profile idempotently after successful authentication. Authentication is the initial account model; guest mode is deferred so it does not introduce a second account-ownership migration during the first release. + +Profile display email and authentication email must have clear semantics. A display-name/photo edit cannot accidentally change sign-in credentials. A sign-in email change uses the supported authenticated verification flow and communicates its outcome. Saving text-only profile changes must work without uploading a new photo. + +### Goals and routines + +- A goal has a name, optional description, icon/color, daily or weekly target minutes, selected routine days, and a time zone. +- A target is positive and editable. Prefer one clear target over the old goal/minimum/maximum input form; retain old minimum/maximum values in migration provenance. +- Archive a goal without deleting its sessions. Confirm destructive history deletion and offer undo where practical. +- Goal details show progress, recent sessions, a prominent Start action, and the next scheduled opportunity. +- Consistency rewards completing the chosen schedule. Rest days are legitimate; missing a day does not erase historical effort. +- Daily periods begin at midnight in the goal's stored time zone; weekly periods begin Monday at midnight there. Retain effective-dated revisions of the target, cadence, routine and period time zone. Changes take effect at the next boundary of the existing period, recorded as an instant, and cannot rewrite completed historical periods. New goals begin immediately with the full chosen target for the first partial period, clearly labeled. +- Imported categories start in a visible **Finish setup** state. Preserve their original hour targets and provenance, but require the person to choose cadence, routine days and a time zone before calculating scheduled completion or enabling reminders. Historical duration remains visible; do not invent a previous routine or retroactively mark it complete. + +### Tracking and manual sessions + +- Support start, pause, resume, finish, and discard, with one active timer per local app installation. +- Finish produces one stable session ID; Stop/Save/retry cannot create multiple records for the same timer. +- Persist timer state and checkpoints outside the activity. Use monotonic elapsed time while a boot session remains valid; store wall-clock anchors for recovery. +- Persist each active interval between Start/Resume and Pause/Finish, including its UTC endpoints and measured duration. A saved session retains these intervals and sums their durations; paused time earns no progress. A new uninterrupted manual entry has one interval. Complete new sessions require a positive total duration. +- Rotation, navigation, backgrounding, screen lock, and process death do not discard a session. A reboot or implausible time change leads to an editable recovered draft rather than an invented duration. +- Use an appropriate user-visible ongoing notification for an active session and verify platform/background rules during the tracking stage. Do not use WorkManager as a per-second clock. +- Manual entry accepts a goal, start/end date-times, a note, and an optional image. Validate real calendar times and positive duration, including midnight, time zones, and daylight-saving ambiguity. +- Keep sub-hour precision. Do not truncate each entry to integer hours before summing. +- Stage selected images in app-private storage, show pending/retry state, and associate uploaded files only with the intended saved session. Saving must not race an incomplete upload or retain a temporary picker URI as a remote image URL. +- Preserve existing legacy image references when available; allow private user-owned storage paths for new uploads. + +### Progress and visual design + +Use one trustworthy progress model for Today, Goals, and Progress. Calculate totals from canonical sessions; show minutes/hours with readable labels. Provide period filters and a text equivalent for every chart. For goal progress, split active intervals at the boundaries defined by that goal's effective period/time-zone revision. Recording time zone is history-display metadata, not an alternative completion rule. Pauses earn no duration on either day. An all-goal report uses one explicitly displayed reporting zone, initially the device zone, rather than mixing incompatible day boundaries. Retain summary-only legacy records on their recorded date, labeled as summaries without fabricated interval allocations or historical target completion. + +The proposed visual direction is calm, confident, and encouraging: warm neutral surfaces, a refined coral/red brand accent, restrained success colors, generous spacing, clear typography, and a prominent time/progress display. A brief completion animation should reinforce a saved action. Animation is decorative and respects system settings; it must not obscure state or delay interaction. Specific tokens and representative screen previews are delivered with the Compose design-system stage before broad screen migration. + +Support light, dark, and system themes from startup. Use meaningful loading, empty, offline, failed, and pending-sync states. Make controls at least 48dp, support TalkBack and large text, and avoid conveying progress by color alone. See [Android accessibility guidance](https://support.google.com/accessibility/android/answer/7101858). + +## 4. Architecture and source layout + +Keep one app module initially, organized by feature with small shared packages. Do not introduce a large module graph to modernize 19 source files. + +```text +app/src/main/java/com/example/acetimetracker/ + app/ Application container, activity, navigation/session routing + core/model/ Goal, session, profile, legacy provenance, duration/time policy + core/data/ Repositories, persistence, Firebase adapters, migration and sync + core/designsystem/ Compose theme, shared controls, previews + core/reminders/ Reminder policy and platform scheduling adapters + feature/auth/ Sign-in, registration and recovery + feature/today/ Today and onboarding + feature/goals/ Goals and routine editing + feature/tracking/ Timer state machine and active-session UI + feature/history/ Entry editing, attachments and history + feature/progress/ Period aggregation and accessible summaries + feature/profile/ Profile, preferences and data controls +``` + +Screen composables render immutable UI state and emit actions. ViewModels expose lifecycle-aware `StateFlow`; repositories own data access. Duration math, aggregation, migration decisions, reminder eligibility, and timer transitions are testable Kotlin logic with injectable clocks. Firebase SDK calls do not live in composables. + +A small application-scoped dependency container is sufficient initially. Introduce additional dependency-injection machinery only when its lifecycle or testing benefit is concrete. + +### Local persistence and remote sync + +Use Room for the local goal/session/timer state that screens observe, DataStore for device preferences, and Firebase Authentication/Firestore/Storage for account and remote synchronization. Room is the UI's local source of truth; Firestore is the remote account store, not a second independent UI model. This follows Android's [offline-first guidance](https://developer.android.com/topic/architecture/data-layer/offline-first). + +Record local writes and an outbox operation in one local transaction. Stable entity/operation IDs make retries idempotent. A connectivity-aware worker drains pending operations with bounded backoff and exposes pending/error state. Server-confirmed revisions and tombstones prevent a delayed offline edit from silently resurrecting deleted data; conflicting edits become a resolution state. New session identities do not depend on a mutable category name. + +Firestore already has offline caching and queued writes, with last-write-wins behavior for changes to a document. Room's outbox is the sole durable queue for new canonical goal/session mutations. Its remote adapter uses online transactions that validate the expected entity revision and atomically record an immutable operation receipt. A retry first checks that receipt; a committed operation with a lost acknowledgement is acknowledged locally without applying it twice. An offline failure remains pending in Room; do not issue a separate ordinary/batched Firestore write as a fallback. Legacy cached writes remain subject to the migration reconciliation policy. See [Firestore offline behavior](https://firebase.google.com/docs/firestore/manage-data/enable-offline) and [transaction semantics](https://firebase.google.com/docs/firestore/manage-data/transactions). + +Keep local records partitioned by account ID. On sign-out/account replacement, first pause and checkpoint the originating account's active timer, cancel its notification/actions, then stop its listeners/workers and reminders and remove its data from active UI access. The paused timer does not block a different account from starting its own session; its owner must explicitly resume it after returning. Notification actions and in-flight async callbacks validate both the originating UID and timer/operation identity before acting. Warn about unsynced local work before a destructive local clear. Account A's drafts, attachments, and pending writes cannot appear or upload under account B. + +### Approved compatible toolchain snapshot + +Research date: 19 October 2026. P1 Task 1 built this combination locally, from a clean tracked-source export, and in submitted-head CI in [PR #4](https://github.com/MphoCodes/AceTimeTracker/pull/4). The catalog and wrapper checksum pin the resolved versions. Later-stage libraries remain unimplemented decisions until their owning stages validate them; publication alone is not a passing build. + +| Component | Proposed pin/decision | +| --- | --- | +| Android Gradle Plugin | `9.3.3` | +| Gradle wrapper | `9.5.0` | +| Gradle runtime | JBR/JDK 21; keep machine-specific paths untracked | +| Kotlin and Compose compiler plugin | `2.4.21`, same compiler/plugin version | +| Java/Kotlin bytecode | 17, independently of the Gradle runtime | +| Android SDK | `compileSdk 37`, `targetSdk 36` by P1 completion, `minSdk 27` retained; the toolchain-only task temporarily retains target 34 until its paired compatibility task passes | +| Compose BOM | `2026.09.00` | +| [Activity Compose](https://developer.android.com/jetpack/androidx/releases/activity) / [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) | `1.13.0` / `2.11.0` | +| [Navigation 3](https://developer.android.com/jetpack/androidx/releases/navigation3) / [Material 3 Adaptive](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive) | `1.2.0` / `1.3.0` | +| [Room](https://developer.android.com/jetpack/androidx/releases/room) / [DataStore](https://developer.android.com/jetpack/androidx/releases/datastore) / [WorkManager](https://developer.android.com/jetpack/androidx/releases/work) | `2.8.5` / `1.2.1` / `2.12.0`, introduced with the stage that uses them | +| [Firebase Android BOM](https://firebase.google.com/support/release-notes/android#2026-10-15) | `35.0.2`; SDK migration is validated explicitly | + +AGP 9.3 documents API 37 support and Gradle 9.5.0. Its 9.3.3 patch includes relevant D8/R8 fixes. Kotlin's published fully tested range for 2.4.21 ends at AGP 9.3.1; choosing 9.3.3 is an engineering inference within the patch line, and P1 must prove it through dependency metadata checks, assembly, lint, and tests. Do not silently present the combination as certified or fall back to a known-regression patch without review. See [AGP release notes](https://developer.android.com/build/releases/agp-9-3-0-release-notes), [Kotlin compatibility](https://kotlinlang.org/docs/gradle-configure-project.html), and [Gradle Java compatibility](https://docs.gradle.org/current/userguide/compatibility.html). + +Use AGP's built-in Kotlin support: remove `org.jetbrains.kotlin.android`, explicitly align the built-in Kotlin Gradle Plugin runtime to the chosen Kotlin version, apply the matching Compose compiler plugin, and migrate `kotlinOptions` to `kotlin.compilerOptions`. Do not hide incompatibilities behind permanent built-in-Kotlin/DSL opt-out flags. See [built-in Kotlin migration](https://developer.android.com/build/migrate-to-built-in-kotlin) and [documented KGP override](https://developer.android.com/build/releases/agp-9-0-0-release-notes#upgrade-to-a-higher-kgp-version). + +Current Compose/navigation artifacts impose higher SDK/AGP metadata requirements than the repository's old toolchain. Check `:app:checkDebugAarMetadata` instead of upgrading isolated version numbers. See the [official Compose BOM](https://dl.google.com/dl/android/maven2/androidx/compose/compose-bom/2026.09.00/compose-bom-2026.09.00.pom) and [Navigation 3 releases](https://developer.android.com/jetpack/androidx/releases/navigation3). + +Migrate obsolete Firebase KTX imports to the main modules during dependency modernization; the current `Login.kt` still imports old KTX packages. The BOM update is not just formatting. See [Firebase Kotlin migration](https://firebase.google.com/docs/android/kotlin-migration) and [Android SDK release notes](https://firebase.google.com/support/release-notes/android#2026-10-15). + +Compile against API 37 for current dependencies; start the foundation at target API 36, then complete the API 37 behavior audit and target update in P11. Reverify Play requirements before a release; the snapshot requirement for new apps/updates is API 36. See [target SDK requirements](https://developer.android.com/google/play/requirements/target-sdk). This spec does not authorize store publication. + +## 5. Canonical data contract and legacy migration + +### Remote collections + +| Path | Purpose | +| --- | --- | +| `users/{uid}` | Profile and account schema metadata | +| `users/{uid}/goals/{goalId}` | Goal identity, target/cadence, routine, archive state | +| `users/{uid}/sessions/{sessionId}` | One canonical saved duration/session with optional note and attachment | +| `users/{uid}/migrationRecords/{sourceKey}` | Provenance, deterministic mappings, imported/skipped/review status | +| `users/{uid}/syncReceipts/{operationId}` | Immutable acknowledgement written atomically with a canonical mutation to make retries safe | + +Session duration is a `Long` count of milliseconds. Complete new sessions have a positive total, UTC outer start/end instants, recording time zone and ordered non-overlapping active intervals; their total duration is the sum of active interval durations. The outer interval alone must never be used to allocate paused-session progress. A legacy summary may have only its original local date and nonnegative duration. Missing start/end times or intervals remain missing: no synthetic midnight timestamps or fabricated notes/images. + +Each entity has a stable ID, schema version, revision/update metadata, and explicit deletion/archive semantics. A session preserves its goal-label snapshot for history even if the goal is renamed or later unavailable. Legacy goal mapping uses source document identity where available; ambiguous duplicate names require review rather than an arbitrary first match. + +### Current sources and decisions + +| Existing source | Existing usage | Migration policy | +| --- | --- | --- | +| `users/{uid}/categories` | Category management and manual selection | Import owned categories into goals requiring routine setup; retain original fields/provenance without inventing cadence/zone | +| `users/{uid}.dailyTimesheetEntries` | Written by manual entry, read by pie chart | Import as summary-quality sessions using only fields actually stored | +| `timesheetEntries` filtered by `userId` | Read by My Progress | Import only records with verified ownership; preserve available detail | +| Global `Category` and `totalTime` | Timer selection/aggregate writes | Preserve untouched; do not assign aggregate values to a person or fabricate individual sessions | + +Migration must support dry-run reporting, incremental batches, restart after interruption, and deterministic source keys. Use an allowlisted pilot, explicit write limits, create-only canonical writes, a created-document manifest, and resumable checkpoints. Protect migration provenance from ordinary client edits. Deduplicate repeated imports by source identity while preserving any duplicate multiplicity actually present in source arrays. Similar records from different sources are not automatically the same session; place ambiguous cross-source matches in a review report. Previously collapsed identical array entries and unstored notes/images cannot be recovered from this repository alone. Quarantine malformed, negative, contradictory, or uncertain records; do not silently count them as verified progress. + +Do not delete legacy source records during the modernization. Use a migration version and per-source reconciliation so that delayed writes from older/offline clients can be discovered after initial import. A live production inventory and explicit cutover plan are prerequisites to a production data migration; a timestamp in the new client alone cannot prove that old clients have stopped writing. + +Rollback must retain sessions created after cutover. Revert a screen through the compatibility adapter or disable the new surface while keeping canonical writes accessible; restoring an old reader/writer alone is not a complete rollback. Verify reconciliation and rollback against mixed legacy/canonical fixtures before changing a live client cutoff. + +Ship versioned Firestore/Storage rules and required indexes alongside code. Emulator tests must prove account isolation and permitted writes before any live deployment. Production backups, actual rule state, release signing, store access, and live data volume are not known from this repository and must not be assumed. + +## 6. Reminder and engagement policy + +Duolingo's published engineering describes selecting reminder copy using experiment outcomes and avoiding over-repeated messages. That is evidence about one published system, not a complete description of today's internal telemetry or notification scheduler. Our proposed adaptations must be labeled as our own. See [Duolingo's engineering article](https://blog.duolingo.com/hi-its-duo-the-ai-behind-the-meme/). + +### First reminder release + +- Reminders are opt-in, attached to the person's selected routine and local time, with an editable schedule. +- Default quiet hours are 21:00–08:00 in the device's current time zone. Settings clearly show the effective time. A reminder that falls inside quiet hours is skipped, not accumulated for a burst later. +- Limit automatic goal-engagement reminders to one per account per device per local day across all goals. A person can start, snooze, or disable reminders; ongoing active-timer status is a separate notification channel. +- Allow one explicit 20-minute snooze of that day's original reminder. It replaces the notification within the same stable reminder chain; it is the sole exception to the automatic posting cap. Repeated Snooze taps cannot schedule additional follow-ups. If the follow-up would land in quiet hours or on another local day, skip it. +- Recheck eligibility when work actually executes: permissions and channel enabled, correct account, configured/active goal, due day, no current session, no goal completion in the relevant period, and no quiet hours. An automatic nudge requires unused daily quota; a snoozed follow-up requires the original chain and its unused single snooze instead. +- Suppress while the app is being used. Reschedule after routine, permission, time-zone, or device-time changes. +- Pause engagement nudges after three scheduled-day automatic posting attempts without a subsequent completed session; snoozed follow-ups do not increment that counter. Show the paused state in settings; resume only when the person explicitly enables reminders again. +- Use inexact scheduling for engagement nudges and communicate that Android may delay delivery. Do not request exact-alarm privileges for ordinary habit nudges. WorkManager coordinates durable deferrable work; AlarmManager is considered only for the appropriate scheduling need. See [Android alarm guidance](https://developer.android.com/develop/background-work/services/alarms) and [notification permission behavior](https://developer.android.com/develop/ui/compose/notifications/notification-permission). +- No cloud push service or paid messaging infrastructure is required for the first local reminder implementation. + +### Behavior signals and later personalization + +Record only the app behavior needed to evaluate the goal loop: goal creation, session start/pause/finish, goal-period completion, reminder scheduling/posting attempts, taps, snoozes, and opt-out. Do not label an API posting success as confirmed delivery or an absent tap as a known dismissal. Optional product analytics excludes goal names, notes, images, email addresses, and raw text. + +The person can disable optional analytics without disabling tracking or reminders. A local learning mode, if enabled, can propose a reminder window after at least five completed sessions on three distinct days in a rolling 14-day window. The proposed window is shown for approval; it never silently overrides a chosen schedule or quiet hours. Use simple deterministic scoring first. Machine learning and bandit selection are deferred until event quality, sample size, and an evaluation plan justify them. + +Evaluate goal-period completion and completed-session return behavior, with reminder opt-out/frequency as guardrails. Notification click-through alone is not success. Any comparison of timing/copy needs randomized assignment or another credible control; an observational rise after a reminder is not proof of incremental benefit. + +## 7. Delivery sequence: one coherent task per PR + +Stage IDs below are roadmap identifiers, not GitHub PR numbers. Each implementation stage has a bounded plan, branch, validation evidence, review, and PR. Dependent stages execute in order; independent read-only research runs in parallel. Later code work uses isolated checkouts when parallel work would otherwise touch shared files. A PR should be reviewable without accepting its neighbors. + +| Stage | Deliverable | Depends on | Minimum acceptance evidence | +| --- | --- | --- | --- | +| P0 | Research, proposed specification and roadmap | Current audit | Source links, legacy-source mapping, explicit assumptions and scope review | +| P1 | Compatible build/toolchain and CI foundation, then target-36 runtime compatibility in a separate PR | P0 approved | Reproducible clean checkout; debug assembly, lint and unit tests; dependency metadata/compatibility check; system-bar/back compatibility; no credentials in tracked config | +| P2 | Auth/profile correctness and app-session routing | P1 | Existing login/registration/reset work; profile bootstrap is idempotent; text-only edit works; persisted session routes correctly; account-switch tests | +| P3 | Canonical goals/sessions repositories and migration | P2 | Precision, ownership and idempotency tests; migration dry-run/fixtures; imported routine-setup state; emulator rules/index tests; old data retained | +| P4 | Durable local tracking data and synchronization | P3 | Offline writes/readback; process-death persistence; lost-ack receipt retries; active-timer sign-out/account separation; conflict and tombstone tests | +| P5 | Compose design system, navigation and preferences | P2–P4 | Representative Today/Goal/Timer previews; auth/back navigation; light/dark/system themes; large text, TalkBack and adaptive-window checks | +| P6 | Goal creation, editing, archive and routine UI | P5 | Create/edit/archive flows; daily/weekly boundaries; target-change history; historical sessions retained | +| P7 | Reliable active timer | P6 | Monotonic-time state-machine tests; paused overnight interval allocation; repeated finish; rotation, lock, background, process death and reboot recovery walkthroughs | +| P8 | Manual sessions, history editing and attachments | P7 | Overnight/DST validation; note/image preservation; pending/retry upload; edit/delete recalculates summaries; no duplicate saves | +| P9 | Today and Progress summaries | P8 | All screens agree on totals; minute precision; period/zone boundaries; legacy quality labels; accessible chart/text equivalence | +| P10 | Onboarding, reminders and optional measurement | P9 | Goal-first onboarding; denied permission; daily cap/single snooze/quiet-hours tests; completed-goal suppression; rescheduling, preference and event payload tests | +| P11 | Legacy removal and release-quality verification | P10 | Remove replaced activities/layouts/dependencies; current target behavior audit; min/latest representative device tests; critical flow, accessibility and performance evidence | + +Research chats completed for competitor/engagement research and data/migration audit. Their initial student-oriented assumption is superseded by the owner's confirmed general personal-goals audience. The integrated findings are recorded in [the research brief](../../research/2026-10-19-modernization-research.md). This coordinating chat owns the integrated specification, dependency order, review outcomes, and the active modernization goal. Implementation chats receive the approved stage contract and its predecessor state; do not launch every stage simultaneously. + +## 8. Verification and completion contract + +### Baseline facts + +This is the pre-implementation snapshot. Current build/runtime evidence is recorded in the P1 plan and implementation PRs. + +- Nineteen Kotlin source files; activity-based XML screens; direct Firebase calls; no implemented repository/ViewModel separation. +- Existing tests are generated arithmetic and package-name examples, not behavior coverage. +- All 55 resource XML files parsed successfully during repository familiarization; this is not an Android compilation check. +- The shell's current `bash gradlew :app:testDebugUnitTest --console=plain` attempt fails before Gradle because no Java runtime is configured for the shell. Android Studio bundles JBR 21, while tracked IDE metadata references JBR 17. A local Android SDK path has not been established. +- The repository includes Firebase client configuration but no versioned Firebase rules/indexes or CI workflow. The live project state has not been inspected. + +### Required validation + +Unit tests own time math, timer transitions, aggregation, migration mapping, reminder eligibility and event payloads. Repository/emulator tests own account isolation, offline/sync semantics, rules and migration restart. Compose tests own primary navigation and editing flows. Device checks own permissions, background behavior, notification actions, image selection, process recovery, adaptive layout and accessibility. Add meaningful tests with the stage that changes behavior; avoid tests that merely restate implementation details. + +Record exact commands/results in each implementation PR. CI should run `:app:assembleDebug`, `:app:lintDebug`, and `:app:testDebugUnitTest`; instrumented/emulator tests run where the environment supports them. A blocked check is reported as blocked, never as passed. + +The Android goal is met when the agreed P1–P11 behavior is implemented, its validation evidence passes, every implementation task is available in its own reviewable PR, the resulting branches can be integrated coherently, and the final Android review has no unresolved required work. Open PRs alone do not prove the complete product works. Store publication and iOS are separate goals. + +## 9. Review decisions and follow-through + +Confirmed: general personal goals/habits audience; native Android/Compose modernization; stepwise PR delivery; separate chats; iOS deferred. + +Approved: time-based goals first, incremental migration, the four main destinations, offline local persistence with Firebase synchronization, the visual direction, opt-in reminders and simple personalization before ML, and the staged delivery sequence. + +The owner approved the [P1 implementation plan](../plans/2026-10-19-p1-android-foundation.md); its toolchain task is verified in PR #4 and its compatibility task is verified in [PR #5](https://github.com/MphoCodes/AceTimeTracker/pull/5). P1 ends at target 36, with host checks, API 27/36 CI and the required API 37 walkthrough complete. The PRs remain unmerged. A [proposed P2 plan](../plans/2026-10-19-p2-accounts-and-profiles.md) covers account/session routing followed by reliable profile editing and sign-in email changes. P2 implementation waits for its plan review. Later plans refine their own interfaces against this specification and the verified predecessor.