# 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`: ```bash 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 ```bash 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: ```bash brew cleanup -s rm -rf ~/Library/Caches/Homebrew/* ~/Library/Caches/ms-playwright pip cache purge; go clean -cache ``` --- ## Build and test ```bash ./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 ```bash 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 ```bash 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: ```bash 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 ```bash adb emu geo fix ``` **Coordinate order is longitude first.** See [TESTING.md](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: ```bash 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: ```bash 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: ```bash 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.