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>
189 lines
6.1 KiB
Markdown
189 lines
6.1 KiB
Markdown
# 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 <lon> <lat> <altitude>
|
||
```
|
||
|
||
**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.
|