Files
samplez/rippr-src/docs/DEVELOPMENT.md
uhryniuk 247b9cdb3f Refresh Rippr snapshot and bundle with full project documentation
Re-exported at 46a0726, which adds README.md plus docs/ARCHITECTURE,
DEVELOPMENT, TESTING, v1 history including the original brief, and a v3
backlog. 112 files, and the bundle now carries 19 commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:44:48 -05:00

6.1 KiB
Raw Permalink Blame History

Development

Everything needed to build, test, and drive Rippr without Android Studio. This was all done from the command line; the GUI is not required at any point.


Toolchain

JDK — must be 17–21

Not 25. AGP 8.7 does not support it, and the failure is confusing. This machine runs JDK 25 by default via mise, so builds need an explicit JAVA_HOME:

export JAVA_HOME=~/.local/share/mise/installs/java/temurin-21.0.12+8.0.LTS
export PATH="$JAVA_HOME/bin:$PATH"

Install with mise install java@temurin-21 if it is missing.

Android SDK

export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"

Needs platform 35 and build-tools 35. sdkmanager and avdmanager come from brew install android-commandlinetools, but note the brew binaries resolve their own SDK root and will not see ~/Library/Android/sdk — avdmanager create fails with "Package path is not valid" even when the image is installed. Hand-editing ~/.android/avd/*.ini is more reliable than fighting it.

Disk space — the surprise blocker

The emulator enforces a fixed ~7.4 GB free-space minimum before it will boot. This is not tunable: shrinking the AVD's disk.dataPartition.size and passing -partition-size both leave the requirement unchanged.

A full build needs roughly 3–5 GB on top of that. Reclaimable without touching source:

brew cleanup -s
rm -rf ~/Library/Caches/Homebrew/* ~/Library/Caches/ms-playwright
pip cache purge; go clean -cache

Build and test

./gradlew assembleDebug                 # debug APK
./gradlew assembleRelease               # release APK (unsigned — will not install)
./gradlew testDebugUnitTest             # 84 tests, no device needed
./gradlew lintDebug
./gradlew connectedDebugAndroidTest     # 46 tests, needs a device

Only the debug APK installs directly; release is unsigned.

Reading results without the HTML report

python3 -c "
import re,glob
t=f=0
for p in glob.glob('app/build/test-results/testDebugUnitTest/*.xml'):
    m=re.search(r'tests=\"(\d+)\".*?failures=\"(\d+)\".*?errors=\"(\d+)\"',open(p).read())
    t+=int(m.group(1)); f+=int(m.group(2))+int(m.group(3))
print(f'{t} run, {f} failed')
"

Room schema export

Schemas land in app/schemas/com.rippr.data.AppDatabase/. The directory name follows the @Database class package — when the class moved from com.rippr to com.rippr.data, the export path moved with it, which initially looked like the schema had not generated at all.


Emulator

emulator -avd Medium_Phone_API_35 -no-window -no-audio -no-boot-anim \
         -gpu swiftshader_indirect
adb emu kill        # shut down

Grant permissions up front so the runtime dialog does not eat your taps:

for p in ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION POST_NOTIFICATIONS; do
  adb shell pm grant com.rippr android.permission.$p
done

Feeding a synthetic ride

adb emu geo fix <lon> <lat> <altitude>

Coordinate order is longitude first. See TESTING.md for what this cannot simulate — it matters more than it sounds.

Driving the UI

The service is exported=false, so adb shell am start-service is correctly refused. Drive through the UI, or from an instrumented test running in the app's own process.

Locating a control by its label:

tapText() {
  adb shell uiautomator dump /sdcard/u.xml >/dev/null 2>&1
  adb shell cat /sdcard/u.xml | python3 -c "
import sys,re
d=sys.stdin.read()
m=re.search(r'text=\"$1\"[^>]*bounds=\"\[(\d+),(\d+)\]\[(\d+),(\d+)\]\"', d)
print(f'{(int(m.group(1))+int(m.group(3)))//2} {(int(m.group(2))+int(m.group(4)))//2}' if m else 'NONE')
"
}
read x y <<< "$(tapText 'START RECORDING')"; adb shell input tap $x $y

Inspecting the database

sqlite3 is not present on the emulator image. Pull the files instead — and take the -wal too, or recent writes are missing:

for f in rippr_db rippr_db-wal rippr_db-shm; do
  adb exec-out run-as com.rippr cat databases/$f > /tmp/db/$f
done
python3 -c "
import sqlite3; c=sqlite3.connect('/tmp/db/rippr_db')
print(c.execute('SELECT id,state,ROUND(distanceM),pointCount FROM trips').fetchall())
"

Hard-won harness lessons

These cost real time. All were false alarms that looked like app bugs.

Never sleep and assume a tap landed. A cold start took 8.7 s once; a 4-second sleep produced a silently-missed tap and a false "the service didn't start" conclusion. Poll uiautomator dump for the expected text, then tap.

Even a confirmed-present control can swallow a tap right after a fresh install. uiautomator reported the button present, the tap returned success, and nothing happened — no databases/, no service. Repeating it moments later worked.

Verify database state between UI steps. Every emulator false alarm in this project came from trusting a tap instead of checking what actually happened. Checking for the absence of the data directory is what finally made one of them obvious.

Suspiciously identical results mean a broken harness. A constant-sweep loop returned four results identical to six decimal places. The cause was a shell quoting bug that corrupted the source file while the compile error hid behind /dev/null. Never redirect a build to /dev/null inside a measurement loop.

The notification shade stays open between runs and will cover the app, making uiautomator report quick-settings tiles instead of your UI. adb shell cmd statusbar collapse first.


Design assets

Logos and icons are generated, not hand-drawn:

python3 design/gen_logos.py          # SVG concepts + PNG previews (needs rsvg-convert)
python3 design/to_vector_drawable.py # → app/src/main/res/drawable/
python3 design/build_preview.py      # review page

design/material/ vendors Google Material Symbols (Apache-2.0) as reference geometry.

The launcher icon lives on Android's 108-unit adaptive canvas. Content must stay inside the centre 66-unit safe circle — the first version drew its ring at r=43 and the launcher mask cropped it clean off, taking the accent segment with it.