Skip to content

feat(quality): Make performance and accessibility a demonstrated feature — Android Vitals green, a11y enforced in CI #98

Description

@stozo04

Companion to stozo04/stevengates#31, which did this for the website (Lighthouse 100s, enforced via Lighthouse CI — see stozo04/stevengates#35 for how it landed). This issue is the Android translation: same philosophy (audit first, fix what the audit finds, then lock it in CI so it can't regress silently), different instruments. Written to be executable by an agent; follow the Process section strictly.

What

Make app quality a demonstrated feature, not a claimed one: green Android Vitals, a real accessibility pass (TalkBack-usable, Accessibility Scanner clean, accessibility checks running inside the existing Compose test suite), measured startup/jank numbers with budgets, and CI enforcement so future features pay their quality cost visibly in PR review.

Why

  1. The README's central claim is currently unverifiable. It says "follows every Jetpack best practice Google recommends" and "Google-first architecture." A reviewer who opens the repo finds no accessibility work and no CI gate proving the performance story. Same argument as stevengates#31: for this audience, unverified claims are worse than no claims.
  2. Accessibility appears to be genuinely untouched. GitHub code search finds zero occurrences of contentDescription, semantics, or AccessibilityChecks in the repo (verify locally with grep -rn "contentDescription" app/src/ — code search can lag). For a camera app whose primary UI is icon buttons (shutter, flip camera, gallery, speed slider, save/share), that likely means TalkBack users get an app full of unlabeled buttons. This is both the biggest gap and the cheapest differentiation — almost no indie camera app does this properly.
  3. Google grades this app whether we participate or not. Android Vitals (Play Console) tracks real-user crash rate, ANR rate, excessive battery drain, and slow cold starts on every certified device. Crossing the bad-behavior thresholds (~1.09% user-perceived crash rate, ~0.47% ANR rate) demotes Play Store discovery and can put a warning on the listing. We already triage crashes (.github/workflows/crashlytics-autotriage.yml); this issue extends that posture from "react to crashes" to "hold a quality bar."
  4. Half the machinery already exists — finish the story. A baselineprofile module with BaselineProfileGenerator.kt is already wired (app/build.gradle.kts line ~308: baselineProfile(project(":baselineprofile"))). There is a rich Compose androidTest suite (CameraScreenTest, GalleryScreenTest, BoomerangEditorScreenTest, OnboardingScreenTest, etc.) that accessibility checks can piggyback on with a few lines. What's missing is the accessibility layer, the measured budgets, and the CI gate.

Process — read first

  1. Read CLAUDE.md, docs/ANDROID_STANDARDS.md, docs/DEFINITION_OF_DONE.md, docs/STATIC_ANALYSIS.md, and docs/TEST_COVERAGE.md before writing anything. This repo has a deliberate tiered-gate design (advisory || true steps promoted to hard gates only per the documented procedure) and a convention that the pr-reviewer skill decides PR verdicts, not the build (app/build.gradle.kts lint block: abortOnError = false with an explanatory comment). Do not fight these conventions — extend them.
  2. Verify every file/line reference in this issue against current code and note drift in your plan.
  3. If any "Decision required from Steven" item below is unresolved (no answer in issue comments), STOP and post a comment listing exactly what you need, with a recommendation for each. Do not guess.
  4. Write a short plan (files, tests, verification) before implementing; include it in the PR description.
  5. One branch per issue, scope discipline, conventional commits, do not merge — same rules as always. If this issue turns out to be too large for one PR, propose a split in a comment (suggested seams: a11y semantics pass / a11y test enforcement / macrobenchmark + CI) and wait for approval.

How

Phase 0 — Audit first (establish the real baseline; no code changes)

  1. Vitals snapshot (Steven task — agent cannot access Play Console): record in this issue's comments the current Android Vitals numbers (user-perceived crash rate, ANR rate, slow cold start %) and the latest pre-launch report summary if one exists. If pre-launch reports have never been looked at, upload the current build to the internal testing track and let one generate.
  2. Accessibility audit (agent, on device/emulator):
    • grep -rn "contentDescription\|semantics(" app/src/main/java/ — inventory what exists (expected: nothing).
    • Walk every screen with TalkBack enabled (Onboarding → PermissionExplanation → Camera → Loopifying → BoomerangEditor/Trim → Gallery → Share). Record per screen: unlabeled controls, focus order problems, announcements that lie or say nothing. The screen composables live under app/src/main/java/io/github/stozo04/openloop/ui/.
    • Run Google's Accessibility Scanner app over the same screens; save its findings (touch-target sizes, contrast, missing labels) as screenshots/notes in the issue.
  3. Performance baseline (agent):
    • Cold-start: adb shell am start -W -n io.github.stozo04.openloop/.MainActivity x 10, record median TTID; note whether the baseline profile is actually installed/compiled (adb shell dumpsys package dexopt | grep -A2 openloop).
    • Verify the baselineprofile module actually generates and the profile ships in the release AAB (check for baseline-prof.txt in the merged artifacts). It existing in the build script is not the same as it working.
    • Jank: capture a Perfetto trace or dumpsys gfxinfo io.github.stozo04.openloop while scrolling the Gallery grid and while the Loopifying progress screen animates; record janky-frame %.
  4. Post the full baseline as a comment on this issue before changing anything. Fixes must map to findings.

Phase 1 — Accessibility (the substantive half)

  1. Semantics/labels pass over every screen in app/src/main/java/io/github/stozo04/openloop/ui/: every Icon/IconButton gets a real contentDescription (or explicit null ONLY when a sibling text label makes it decorative); sliders get stateDescription/semantics { } so TalkBack announces the actual speed value ("1.5x"), not "50 percent"; the capture button announces what it does and its state; progress screens announce progress. Strings go in strings.xml, not literals — they must be translatable.
  2. Touch targets >= 48dp everywhere Accessibility Scanner flagged; use Modifier.minimumInteractiveComponentSize() or padding, not layout rewrites.
  3. Contrast: fix any Scanner-flagged text/background pairs (< 4.5:1 normal, < 3:1 large text) in ui/theme/Theme.kt tokens, mirroring the token-level (not per-usage) approach that worked in stevengates#35.
  4. Reduced motion: respect the OS "remove animations" setting (Settings.Global.ANIMATOR_DURATION_SCALE == 0) for the loopifying/editor animations — verify Compose animations honor it; anything hand-rolled must check it.
  5. Enforce it in the existing test suite: the repo already has Compose UI tests in app/src/androidTest/.../ui/. Enable accessibility checks on the Compose test rule (Compose UI test 1.8+ integrates the Accessibility Test Framework; the API is composeTestRule.enableAccessibilityChecks() — verify the exact API against the Compose BOM version in gradle/libs.versions.toml and upgrade the BOM if needed). Turn them on for every existing screen test rather than writing new tests. Suppressions require a written justification comment, same spirit as lint-baseline.xml.

Phase 2 — Performance, measured

  1. New :macrobenchmark module (androidx.benchmark macro-junit4), sibling to the existing :baselineprofile module and reusing its target setup: StartupTimingMetric for cold start, FrameTimingMetric for Gallery scroll and the editor. This is the Android equivalent of the Lighthouse numbers — it turns "feels fast" into a number that can regress visibly.
  2. Set budgets from the Phase 0 baseline, not from wishes (the stevengates#31 lesson: the issue's example budget was unreachable and had to be renegotiated mid-PR — measure first, then commit): e.g. cold start median <= baseline + 10%, janky frames <= baseline + 2pp. Write the numbers and the rationale in docs/ANDROID_STANDARDS.md or a new docs/PERFORMANCE.md.
  3. Verify the baseline profile earns its keep: run the startup benchmark CompilationMode.None vs CompilationMode.Partial — if the profile isn't measurably helping, that's a finding to report, not hide.

Phase 3 — Lock it in CI

  1. There is currently no Gradle build/test workflow in .github/workflows/ (only markdown static-analysis, doc-layout, and crashlytics-autotriage). Add android-ci.yml: JDK setup + Gradle cache, ./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintDebug on every PR. Unit tests + build are hard gates; lint stays advisory per the repo's documented tier design (docs/STATIC_ANALYSIS.md) — surface the report, let the pr-reviewer skill judge it.
  2. Instrumented tests with accessibility checks in CI: run the androidTest suite (now a11y-enabled per item 9) on an emulator via reactivecircus/android-emulator-runner (API 34, x86_64) or Gradle-managed devices — whichever docs/ conventions prefer; note the camera tests may need the emulator's virtual camera or the synthetic fixtures in app/src/androidTest/assets/. If the full suite is too slow for PRs (> 15 min), split: a11y-critical screen tests on PR, full suite nightly via schedule: + workflow_dispatch. State the split explicitly in the workflow comments.
  3. Macrobenchmarks in CI are noisy on shared runners — run them on a schedule: (nightly/weekly) with results published as a workflow artifact + a tracked JSON in the repo (or Firebase Test Lab physical devices if the free quota suffices — decision gate below). PR-blocking on emulator benchmark numbers is a known flake generator; don't do it without hysteresis.
  4. Vitals watchdog (lightweight): extend the existing crashlytics-autotriage pattern with a scheduled job that pulls crash-free-users % from the Firebase/Play APIs and files an issue when it dips below the agreed floor — ONLY if Steven approves the API credential setup (decision gate below).
  5. Once green and stable: state the enforced gates and measured numbers in README.md (it already makes the quality claim — back it) and in the Play Store listing materials under docs/play-store/ if applicable.

Decision required from Steven (answer in comments before the dependent work starts)

  • D1 — Play Console data: paste current Android Vitals numbers + whether pre-launch reports exist (Phase 0.1). Agent has no Play Console access. Recommendation: also enable pre-launch reports on every internal-track upload if not already on — they're free.
  • D2 — CI device strategy: GitHub-hosted emulator (free, slower, camera emulation quirks) vs Firebase Test Lab (real devices, free daily quota, needs a service-account secret in repo settings). Recommendation: start with the GitHub emulator for the a11y test gate; revisit FTL for benchmarks.
  • D3 — Vitals API automation (item 16): requires creating a Google Cloud service account with Play Developer Reporting / Firebase access and adding it as a repo secret. Recommendation: defer — do the manual monthly check first; automate only if it proves annoying.
  • D4 — Budget sign-off: after Phase 0, the agent posts proposed budget numbers (item 11) as a comment; Steven approves before they become CI gates.

Dependencies / coordination

  • None blocking. stozo04/stevengates#38 (fresh OpenLoop screenshot for the website) is unrelated code-wise, but a fresh capture session for it could double as the Accessibility Scanner walk — batch them if convenient.
  • The existing e2e skills (.claude/skills/run-e2e*) drive real devices via PowerShell on Steven's machine; nothing here replaces them. CI gates are the floor, the skills remain the ceiling.

Verification — required, no exceptions

  • Baseline comment posted (Phase 0) BEFORE any fix lands; every fix in the PR maps to a baseline finding.
  • TalkBack walk video or step log per screen, before AND after, attached to the PR.
  • Accessibility Scanner: before/after screenshots per screen.
  • ./gradlew :app:testDebugUnitTest green; androidTest suite green WITH accessibility checks enabled (list any suppressions + justification in the PR body).
  • Startup + frame benchmarks: before/after numbers in the PR, with the compilation-mode comparison from item 12.
  • The new CI workflow(s) green on the PR itself.
  • Any deviation from this issue documented in the PR body with reasoning — do not silently diverge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions