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>
This commit is contained in:
188
rippr-src/docs/DEVELOPMENT.md
Normal file
188
rippr-src/docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user