skip to content
stevengates.io
back to home
case study·lead·since 2026

OpenLoop

Open-source Boomerang camera, live on Google Play.

visit live publicAndroidKotlinJetpack ComposeCameraXMedia3Play Store
OpenLoop — open-source Boomerang camera, live on Google Play

the project

OpenLoopis a free, open-source Boomerang camera for Android — point, tap, and get a seamless speed-controlled video loop. Every other loop app on the Play Store either charges money, runs ads, or ships your video off to a server you don't control. OpenLoop is the honest alternative: 100% on-device, zero network calls, no accounts, no tracking, Apache 2.0.

It is live on Google Playand approved — the full pipeline ships: capture a 1.5s burst, trim it, then dial in direction, playback speed, and a color look in a tabbed editor before exporting. Every frame is rendered locally with Media3 Transformer. Built solo, with AI assistance, against Google's own Android guidance.

the stack

  • Kotlin + Jetpack Compose — declarative UI, no XML layouts, no fragments. A single ViewModel drives a sealed-interface state machine via StateFlow (unidirectional data flow).
  • CameraX for the viewfinder, burst recording, and lens toggle — device-agnostic across the long tail of Android hardware.
  • Media3 (ExoPlayer + Transformer) for looping playback and the export pipeline. Reverse is a two-pass MediaCodec dance (Media3 has no native reverse effect), fed into a Transformer Composition with speed and filter effects.
  • Jetpack DataStore for the onboarding flag — coroutine-based, replacing SharedPreferences.
  • Baseline Profiles to pre-compile hot paths and kill Compose jank on first launch. API 36 (Android 16) target, minSdk 26, clearing Google Play's target-API floor.
  • JUnit + MockK + Compose UI Test — JVM unit tests for the pure sequence/seam math, instrumented tests for layout-critical screens.

the hard problems

A Boomerang looks trivial. The on-device media pipeline is where it stops being trivial — the bugs that ate the most time were all in the seams between decoder, encoder, and muxer:

  • Reversing through a Surface.Pushing frames decoder → encoder Surface re-applied the camera's rotation metadata twice. Fix: strip KEY_ROTATION on the decoder and re-stamp it on the muxer.
  • Imported clips exercise codecs the camera never produces — HDR, odd resolutions, software encoders. Those needed tone-mapping, a HW-encoder preference, and a resolution cap, plus a hard rule that a failed reverse can never wedge the editor.
  • A Samsung S23 shipped zero-frame exports that exited “cleanly.” The lesson: every pipeline stage must count its own output samples — a stage that emits nothing must fail, not return success.
  • Green corruption on the SW path from sizing the encoder Surface smaller than the decoder output. Downscaling belongs in the Media3 render, not the Surface dimensions.
  • A Galaxy A55 / Android 14 crash caught in Crashlytics traced to a foreground-service type gated one API level too low — it must be gated on the level that added the constant.

lessons learned (the discipline)

Every PR review and production bug that surfaced a reusable pattern gets written down — there are 24 numbered lessons in the repo, each with the mistake, the rule going forward, and a grep to catch it again. A few that generalize beyond Android:

  • A change is not done because it compiles.“Ready for PR” means clean debug and release build, zero test failures, zero new lint errors, the app actually run on an emulator, and a screenshot as proof.
  • Never trust a piped exit code. BUILD SUCCESSFUL through | tail reports the exit code of tail, not Gradle — a failed build can look green.
  • Keep the state router honest. The UI-state when is exhaustive with no else, so adding a state is a compile error until it is routed — extracted into a testable NavHost.
  • Release resources on composition exit, not just Activity stop — a CameraX leak only shows up when the preview leaves the tree but the Activity lives on.

how it ships (the quality gate)

OpenLoop is live in production and reachable by anyone on Google Play, so the bar is a zero-error rule: a PR opens only from a fully green state — any failing test, compile error, or lint error gets fixed first, even pre-existing ones. On top of the human work, every PR is audited by an autonomous review agent that web-searches developer.android.comfor current guidance (no stale rules) and posts a PASS/FAIL report against 11 categories of Google standards, plus a headless Android Lint gate mirroring Android Studio's Inspect Code. No merge without an APPROVE verdict and zero new lint errors.

what this demonstrates

  • I ship to production. Concept → Play Store, approved and live — including the unglamorous parts: signing, data-safety disclosures, target-API compliance, Crashlytics triage, OEM regression testing on real Samsung and Pixel hardware.
  • I respect the platform.Latest Jetpack libraries, Google's recommended architecture, and a standards doc that the code is forced to agree with.
  • I build durable process, not just features.A lessons-learned corpus and an automated review gate mean the same mistake doesn't ship twice.
  • Privacy by construction.On-device processing and zero network calls aren't a setting — they're the architecture.