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>
6.1 KiB
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.