Native Android (Kotlin / Jetpack Compose) + Wear OS companion + iOS (Swift / SwiftUI) stretching app focused on lower back, legs, hips, and hamstrings.
100% local persistence by default; cloud sync only kicks in if you sign in.
content/ Shared JSON (stretches, programs, glossary) consumed by both apps
android/ Gradle project — three modules: :core (pure JVM), :app, :wear
ios/ iOS sources — LowerBackStretching/ (phone) + WatchApp/ (watch)
firebase/ Firestore rules + cloud-functions stubs (backend, optional)
.github/ Actions workflows (unit tests on every push, emulator nightly)
The same stretches.json / programs.json / glossary.json files live in
android/app/src/main/assets/ and ios/LowerBackStretching/Resources/ —
keep them in sync (or symlink) when editing content.
:core — pure-JVM Kotlin library. Models, the generic PlayerEngine<T :
Timed> state machine, calendar math, gamification calculations, audio
enums, formatting helpers. No Android dependency, no Room, no DataStore.:app — the phone Android app. Depends on :core. Owns Room
persistence, DataStore prefs, Compose screens, Health Connect /
notifications / cloud-sync plumbing.:wear — Wear OS companion. Depends on :core. Reuses the same
PlayerEngine; ships a slim WatchStretch model.Open android/ in Android Studio (Hedgehog or newer). Sync Gradle, plug in
a device or start an emulator, and Run. Min SDK 26, Target SDK 36.
./gradlew :app:installDebug # phone
./gradlew :wear:installDebug # watch
./gradlew :core:test :app:testDebugUnitTest :wear:testDebugUnitTest
Open Xcode 15+, create a new iOS App named LowerBackStretching, drag in
ios/LowerBackStretching/. Add a Watch App target named WatchApp, drag
in ios/WatchApp/. iOS 17 deployment target. See ios/README.md for
the full step-by-step.
Stretch entries carry an animation field — a set of normalized joint
keyframes interpolated at runtime by StretchAnimationView (2D) and
StretchAnimation3DView (3D). All 26 stretches have one. Keyframe data is
generated by scripts/inject_animations.py; see content/README.md.
The legacy youtubeId field is still present in the JSON but is no longer
used for playback (video embeds were removed — see “Health Connect &
platform notes”). 12 of 26 entries have it empty.
.github/workflows/android-tests.yml — JVM unit tests on every push to
main and on PRs that touch android/**. Per-module diagnostic captures
Kotlin compile errors into the run summary on failure..github/workflows/android-instrumented-tests.yml — emulator tests
(Pixel 6 + Pixel Tablet) on a nightly schedule and manual dispatch.
Includes Room migration smoke tests..github/workflows/android-release.yml — push a v* tag to build signed
AABs for both modules. Re-runs the full verify set first, restores the
upload keystore from repository secrets, and fails loudly if the result
came out unsigned. It does not upload to Play; submission stays manual.The brainstorm plan shipped Waves 1–10:
| # | Wave | Status |
|---|---|---|
| 1 | Foundation polish (dark mode, units, haptics, lifecycle save-state) | ✓ |
| 2 | Audio (music + ambient + chime + ducking) | ✓ |
| 3 | Routine management (reorder, duplicate, soft-delete, share-via-link) | ✓ |
| 4 | Gamification (streaks, goals, XP, levels, achievements) | ✓ |
| 5 | Health integration (Health Connect session write) | ✓ |
| 6 | Education (glossary, “why this stretch”, what-you-should-feel) | ✓ |
| 7 | Anatomy & body diagram (tap silhouette → routine) | ✓ |
| 8 | Backend & social (sync interface; Firebase impl is opt-in) | ✓ |
| 9 | Watch companion (Wear OS app sharing PlayerEngine) | ✓ |
| 10 | Polish (PiP, share QR, system-calendar integration) | ✓ |
Android is write-only by design. The app declares exactly one Health
Connect permission — WRITE_EXERCISE — and reads nothing. An earlier
release also requested READ_STEPS to power a “you walked 5k today, try a
cooldown stretch” card on Home. That was removed in 1.0.8 (versionCode 11)
rather than justified against Google Play’s Minimum Scope policy for
Health Connect permissions, which requires each requested data type to be
essential to a core, user-facing feature. The cooldown card was a nice-to-
have, so deleting it was cheaper than defending it.
PRIVACY.md is the policy Google Play links to and reviewers read against
the manifest — if you ever change the declared permissions, update that
file in the same commit.
iOS mirrors this: the HealthKit step read and its cooldown card were
removed for parity, and NSHealthShareUsageDescription dropped from
Info.plist. HealthKit has no Minimum Scope equivalent, so this was a
consistency choice rather than a compliance requirement — recorded here so
the removal doesn’t later read as an accidental regression.
android/app/AUDIO_FILES.md) — currently
the audio system silently no-ops when files are missingFirebaseSyncBackend stub already
exists; only the google-services.json and a few config swaps remain)values-<locale>/ translations. UI copy is fully externalized to
strings.xml now, but the content JSON (stretches, programs, glossary)
is still English-only with no locale mechanism.