32 KiB
Canteen Asset Tracker — Android App Implementation Plan
For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.
Goal: Native Android app that uses device GPS to detect when a technician is near a machine, enabling background proximity alerts, automatic check-in prompts, and offline-capable asset lookup — all backed by the existing FastAPI backend.
Architecture: Kotlin + Jetpack Compose native Android app communicating with the existing FastAPI backend via REST. FusedLocationProviderClient for efficient background GPS. GeofencingClient for proximity zones defined by asset/location coordinates. Firebase Cloud Messaging (FCM) for push notifications. Room database for offline cache.
Tech Stack:
- Language: Kotlin 2.0+
- UI: Jetpack Compose (Material 3)
- Navigation: Compose Navigation
- Networking: Retrofit 2 + OkHttp + kotlinx.serialization
- Location: Google Play Services (FusedLocationProvider + GeofencingClient)
- Local DB: Room (offline asset cache, pending check-ins)
- Camera: CameraX (barcode scanning + OCR photo upload)
- Maps: Google Maps Compose / OSMDroid (open-source fallback)
- Push: Firebase Cloud Messaging
- DI: Hilt
- Target: Android API 26+ (covers 96%+ devices)
Phase 0: Backend Prerequisites
The existing backend at ~/projects/canteen-asset-tracker/server.py needs GPS-coordinate storage and a proximity-check API before the Android app can function. These are gating changes.
Task 0.1: Add latitude/longitude columns to assets and locations tables
Objective: Store GPS coordinates for each machine and location so the Android app can check proximity.
Files:
- Modify:
server.py:_create_v2_tables(assets + locations table DDL) - Modify:
server.py:migration block (add ALTER TABLE if missing) - Modify:
server.py:AssetCreate/AssetUpdate Pydantic models - Modify:
server.py:LocationCreate/LocationUpdate Pydantic models - Modify:
tests/test_server.py
Step 1: Add columns to table creation
In _create_v2_tables() (around line 77), add to locations:
latitude REAL DEFAULT NULL,
longitude REAL DEFAULT NULL,
In _create_v2_tables() (around line 136), add to assets:
latitude REAL DEFAULT NULL,
longitude REAL DEFAULT NULL,
geofence_radius_meters INTEGER DEFAULT 50,
Step 2: Add migration logic
After the existing migration block (around line 340), add:
# Add lat/lng to assets if not present (v3 migration)
cursor = conn.execute("PRAGMA table_info(assets)")
asset_cols = {row[1] for row in cursor.fetchall()}
if "latitude" not in asset_cols:
conn.execute("ALTER TABLE assets ADD COLUMN latitude REAL DEFAULT NULL")
if "longitude" not in asset_cols:
conn.execute("ALTER TABLE assets ADD COLUMN longitude REAL DEFAULT NULL")
if "geofence_radius_meters" not in asset_cols:
conn.execute("ALTER TABLE assets ADD COLUMN geofence_radius_meters INTEGER DEFAULT 50")
cursor = conn.execute("PRAGMA table_info(locations)")
loc_cols = {row[1] for row in cursor.fetchall()}
if "latitude" not in loc_cols:
conn.execute("ALTER TABLE locations ADD COLUMN latitude REAL DEFAULT NULL")
if "longitude" not in loc_cols:
conn.execute("ALTER TABLE locations ADD COLUMN longitude REAL DEFAULT NULL")
Step 3: Update Pydantic models
Add optional fields to AssetCreate and AssetUpdate:
latitude: Optional[float] = None
longitude: Optional[float] = None
geofence_radius_meters: Optional[int] = 50
Add optional fields to LocationCreate and LocationUpdate:
latitude: Optional[float] = None
longitude: Optional[float] = None
Step 4: Update create/update handlers
Modify POST /api/assets, PUT /api/assets/{id}, POST /api/locations, PUT /api/locations/{id} to include the new optional fields in INSERT/UPDATE SQL.
Step 5: Run tests
Run: cd ~/projects/canteen-asset-tracker && python -m pytest tests/test_server.py -v -x
Expected: All 319 existing tests pass (new columns have defaults, existing INSERTs unaffected)
Step 6: Add tests for new fields
Add ~6 new tests verifying:
- Create asset with lat/lng
- Update asset lat/lng
- Create location with lat/lng
- Update location lat/lng
- GET returns lat/lng fields
- geofence_radius_meters defaults to 50
Task 0.2: Add proximity-check API endpoint
Objective: Backend endpoint that takes a GPS point and returns nearby assets sorted by distance.
Files:
- Modify:
server.py(newGET /api/proximityendpoint) - Modify:
tests/test_server.py(proximity tests)
Step 1: Write failing test
def test_proximity_returns_nearby_assets(client, seed_db):
# Seed assets with coordinates
resp = client.post("/api/assets", json={
"machine_id": "PROX-000001",
"name": "Nearby Fridge",
"category": "Appliances",
"latitude": 40.7128, "longitude": -74.0060, # NYC
"geofence_radius_meters": 100,
})
assert resp.status_code == 201
resp = client.post("/api/assets", json={
"machine_id": "PROX-000002",
"name": "Far Freezer",
"category": "Appliances",
"latitude": 40.8000, "longitude": -74.1000, # ~10km away
"geofence_radius_meters": 100,
})
assert resp.status_code == 201
# Query from near the first asset
resp = client.get("/api/proximity?lat=40.7129&lng=-74.0061&radius_meters=200")
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
assert data[0]["machine_id"] == "PROX-000001" # Closest first
Step 2: Run test to verify failure
Run: pytest tests/test_server.py::test_proximity_returns_nearby_assets -v
Expected: FAIL — 404 Not Found
Step 3: Implement the endpoint
@app.get("/api/proximity")
def proximity_check(
lat: float = Query(...),
lng: float = Query(...),
radius_meters: int = Query(200, ge=1, le=50000),
):
"""
Return assets within radius_meters of (lat, lng), sorted by distance.
Uses Haversine formula for accurate spherical distance.
"""
conn = get_db()
rows = conn.execute("""
SELECT *, (
6371000 * acos(
cos(radians(?)) * cos(radians(latitude)) *
cos(radians(longitude) - radians(?)) +
sin(radians(?)) * sin(radians(latitude))
)
) AS distance_meters
FROM assets
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
HAVING distance_meters <= ?
ORDER BY distance_meters
LIMIT 50
""", (lat, lng, lat, radius_meters)).fetchall()
conn.close()
results = [row_to_dict(r) for r in rows]
return results
Step 4: Run test to verify pass
Run: pytest tests/test_server.py::test_proximity_returns_nearby_assets -v
Expected: PASS
Step 5: Add edge case tests
- No nearby assets returns
[] - Missing lat/lng params returns 422
- radius_meters below 1 or above 50000 returns 422
- Assets with NULL lat/lng are excluded
Step 6: Commit
git add server.py tests/test_server.py
git commit -m "feat: add lat/lng to assets/locations + proximity API"
Task 0.3: Add geofence point-check API endpoint
Objective: Endpoint that checks whether a given GPS point falls inside any geofence polygon.
Files:
- Modify:
server.py(newPOST /api/geofences/checkendpoint) - Modify:
tests/test_server.py(geofence check tests)
Step 1: Implement the endpoint
@app.post("/api/geofences/check")
def check_geofence_point(body: GeofencePointCheck):
"""
Check if a GPS point falls inside any geofence polygon.
Returns list of matching geofences.
"""
conn = get_db()
rows = conn.execute("SELECT * FROM geofences ORDER BY name").fetchall()
conn.close()
matches = []
for row in rows:
points = _json.loads(row["points"])
if _point_in_polygon(body.lat, body.lng, points):
matches.append(row_to_dict(row))
return matches
def _point_in_polygon(lat: float, lng: float, polygon: list) -> bool:
"""Ray-casting algorithm for point-in-polygon test."""
inside = False
n = len(polygon)
j = n - 1
for i in range(n):
yi = polygon[i]["lat"] if isinstance(polygon[i], dict) else polygon[i][0]
xi = polygon[i]["lng"] if isinstance(polygon[i], dict) else polygon[i][1]
yj = polygon[j]["lat"] if isinstance(polygon[j], dict) else polygon[j][0]
xj = polygon[j]["lng"] if isinstance(polygon[j], dict) else polygon[j][1]
if ((yi > lng) != (yj > lng)) and (lat < (xj - xi) * (lng - yi) / (yj - yi) + xi):
inside = not inside
j = i
return inside
Step 2: Add Pydantic model
class GeofencePointCheck(BaseModel):
lat: float
lng: float
Step 3: Write tests (~5 tests: inside, outside, empty geofences, multiple matches)
Step 4: Commit
Phase 1: Android Project Scaffold
Task 1.1: Create Android project
Objective: Boot a bare Kotlin + Compose project with Gradle Kotlin DSL.
Files:
- Create:
android/directory in project root - Create:
android/build.gradle.kts(project-level) - Create:
android/app/build.gradle.kts(module-level) - Create:
android/gradle.properties - Create:
android/settings.gradle.kts - Create:
android/app/src/main/AndroidManifest.xml
Dependencies in app/build.gradle.kts:
dependencies {
// Compose BOM
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.activity:activity-compose:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.0")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.7")
// Networking
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
// Location
implementation("com.google.android.gms:play-services-location:21.3.0")
// CameraX
implementation("androidx.camera:camera-camera2:1.3.4")
implementation("androidx.camera:camera-lifecycle:1.3.4")
implementation("androidx.camera:camera-view:1.3.4")
// Local DB
implementation("androidx.room:room-runtime:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
// DI
implementation("com.google.dagger:hilt-android:2.51.1")
ksp("com.google.dagger:hilt-compiler:2.51.1")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Maps (OSMDroid — open-source, no API key)
implementation("org.osmdroid:osmdroid-android:6.1.18")
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
Step 1: mkdir -p ~/projects/canteen-asset-tracker/android
Step 2: Write all scaffold files
Step 3: Verify: cd android && ./gradlew assembleDebug succeeds
Step 4: Commit
git add android/
git commit -m "feat: Android project scaffold (Kotlin + Compose)"
Task 1.2: Networking layer — Retrofit API client
Objective: Define typed API client for all backend endpoints the Android app needs.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/network/CanteenApi.kt - Create:
android/app/src/main/java/com/canteen/assettracker/network/dto/*.kt - Create:
android/app/src/main/java/com/canteen/assettracker/network/RetrofitModule.kt(Hilt DI)
API endpoints to model:
POST /api/auth/login→AuthTokenGET /api/auth/me→UserGET /api/assets→List<Asset>GET /api/assets/{id}→AssetDetailGET /api/assets/search?machine_id=→List<Asset>POST /api/checkins→CheckinGET /api/checkins?asset_id=&limit=→List<Checkin>GET /api/proximity?lat=&lng=&radius_meters=→List<Asset>POST /api/geofences/check→List<Geofence>GET /api/geofences→List<Geofence>POST /api/ocr(multipart) →OcrResult
DTOs: LoginRequest, AuthResponse, User, Asset, AssetDetail, CheckinCreate, Checkin, GeofencePointCheck, Geofence, OcrResult, ProximityResponse
Step 1: Write all DTOs as @Serializable data classes
Step 2: Write CanteenApi interface with Retrofit annotations
Step 3: Write Hilt @Module for providing Retrofit instance with auth token interceptor
Step 4: Write unit test mocking OkHttp (verify auth header injection)
Step 5: Commit
Task 1.3: Local database — Room cache
Objective: Offline cache of assets, pending check-ins, and auth token.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/data/AppDatabase.kt - Create:
android/app/src/main/java/com/canteen/assettracker/data/entity/*.kt - Create:
android/app/src/main/java/com/canteen/assettracker/data/dao/*.kt - Create:
android/app/src/main/java/com/canteen/assettracker/data/DatabaseModule.kt(Hilt DI)
Entities:
CachedAsset— mirrors API asset with lat/lng, last_synced timestampPendingCheckin— check-ins created offline, synced when onlineAuthSession— token, user_id, role, expiry
DAOs:
AssetDao— insertAll, getAll, searchByName, getById, clearAllPendingCheckinDao— insert, getAllPending, markSynced, deleteAuthSessionDao— insert, getActive, invalidate
Step 1: Write entities and DAOs Step 2: Write AppDatabase (Room 2.6.1, schema export on) Step 3: Write Hilt module Step 4: Write instrumentation test: insert → query → verify
Step 5: Commit
Phase 2: Core Android Features
Task 2.1: Login screen + auth flow
Objective: Login screen that authenticates against the backend, stores token securely, and gates the rest of the app.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/login/LoginScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/login/LoginViewModel.kt - Modify:
android/app/src/main/java/com/canteen/assettracker/MainActivity.kt - Modify:
android/app/src/main/java/com/canteen/assettracker/navigation/NavGraph.kt
UI: Username + password fields, Sign In button, error banner, "Connecting..." loading state. Dark theme matching webapp. Server URL configurable via settings (default: https://canteen.ourpad.casa:8901).
Auth flow:
- POST /api/auth/login → get bearer token
- Store token in Room + OkHttp interceptor
- GET /api/auth/me → show user badge
- Navigate to main screen
Step 1: Write LoginViewModel (StateFlow: isLoading, error, token)
Step 2: Write LoginScreen composable
Step 3: Wire into NavGraph (login → authenticated graph)
Step 4: Write RetrofitModule auth interceptor (reads token from Room)
Step 5: Compose preview + manual test on device/emulator
Step 6: Commit
Task 2.2: Home dashboard screen
Objective: Dashboard with stats (total assets, by category, recent check-ins). Data loaded from backend, cached in Room.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/dashboard/DashboardScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/dashboard/DashboardViewModel.kt
UI: Stats cards (total, active, maintenance), category breakdown bar, recent check-ins list, pull-to-refresh.
Step 1: Write DashboardViewModel (loads assets + checkins on init) Step 2: Write DashboardScreen (LazyColumn with stat cards) Step 3: Error state — show retry button when offline Step 4: Commit
Task 2.3: Asset list + search + detail
Objective: Browse all assets, search by name/machine_id, view full asset detail with map pin.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/assets/AssetListScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/assets/AssetDetailScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/assets/AssetViewModel.kt
UI:
- List: search bar + LazyColumn cards (name, machine_id, status chip, category icon)
- Detail: full fields, map showing pin if lat/lng set, "Check In" FAB button
Step 1: AssetViewModel (load, search local + remote, cache) Step 2: AssetListScreen Step 3: AssetDetailScreen with map Step 4: Commit
Task 2.4: Map screen with asset pins
Objective: Full map showing all asset markers, geofence overlays, and current GPS position.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/map/MapScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/map/MapViewModel.kt
UI: OSMDroid map view, asset markers (tappable → asset detail), geofence polygon overlays, current-location blue dot, "center on me" button.
Step 1: MapViewModel (load geofences + assets with lat/lng)
Step 2: MapScreen composable with MapView AndroidView wrapper
Step 3: Marker click → navigate to AssetDetail
Step 4: Current location dot
Step 5: Commit
Phase 3: GPS Proximity Detection (Core Feature)
Task 3.1: Background location service
Objective: Foreground service that continuously tracks device location using FusedLocationProviderClient. Runs even when app is in background.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/location/LocationService.kt - Create:
android/app/src/main/java/com/canteen/assettracker/location/LocationRepository.kt - Modify:
android/app/src/main/AndroidManifest.xml(add FOREGROUND_SERVICE + location permissions)
Permissions added to manifest:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
LocationService behavior:
- Request location updates every 10 seconds (configurable: 5-60s)
- Priority: PRIORITY_HIGH_ACCURACY (GPS)
- Emit location to a shared Kotlin
SharedFlow - Show persistent notification: "Canteen Tracker — monitoring location"
- Auto-restart on device boot (BootReceiver)
Step 1: Write LocationRepository (wraps FusedLocationProviderClient) Step 2: Write LocationService (ForegroundService) Step 3: Register in AndroidManifest Step 4: Add runtime permission request flow in MainActivity Step 5: Test: start service, background app, verify location updates in logcat
Step 6: Commit
Task 3.2: Proximity engine — detect when near a machine
Objective: Core logic that continuously checks device location against asset/location coordinates and triggers notifications when entering proximity zones.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/location/ProximityEngine.kt - Create:
android/app/src/main/java/com/canteen/assettracker/location/ProximityRepository.kt
ProximityEngine algorithm:
Every location update (debounced to 5 seconds):
1. If user is moving (speed > 1 m/s), skip (save battery)
2. Query local Room cache for assets within 500m (pre-filter using simple lat/lng box)
3. Compute Haversine distance for each candidate
4. If distance < asset.geofence_radius_meters AND asset not in activeProximities:
→ Emit "entered proximity" event
→ Show notification: "Near MachineName (50m) — tap to check in"
→ Add to activeProximities set
5. If distance > asset.geofence_radius_meters * 1.5 AND asset in activeProximities:
→ Emit "exited proximity" event
→ Remove from activeProximities
6. Also check geofences: call POST /api/geofences/check every 60 seconds
ProximityRepository:
- Cache assets with lat/lng locally
- Periodically sync assets from server (every 5 min)
- Expose
observeProximityEvents(): Flow<ProximityEvent> - Expose
getActiveProximities(): StateFlow<List<Asset>>
Step 1: Implement Haversine distance function (in km, returns meters)
fun haversineDistance(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
val R = 6371000.0 // Earth radius in meters
val dLat = Math.toRadians(lat2 - lat1)
val dLng = Math.toRadians(lng2 - lng1)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
sin(dLng / 2) * sin(dLng / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return R * c
}
Step 2: Write ProximityEngine with the full check loop Step 3: Write ProximityRepository (local cache + periodic sync) Step 4: Unit test: mock locations, verify proximity events fire correctly Step 5: Commit
Task 3.3: Push notifications for proximity alerts
Objective: When device enters a machine's proximity zone, show a local notification with quick-action buttons (Check In, Dismiss).
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/notification/NotificationHelper.kt - Create:
android/app/src/main/java/com/canteen/assettracker/notification/ProximityNotificationReceiver.kt
Notification channels:
proximity_alerts— high importance, heads-up, sound + vibrationcheckin_reminders— default importance
Proximity notification content:
- Title: "Near MachineName"
- Body: "~50m away at 123 Main St — Room 3A"
- Actions: "Check In" (opens check-in screen or auto-checks-in), "Dismiss" (snoozes for 5 min)
- Tap: opens AssetDetailScreen
Step 1: Write NotificationHelper (channel creation, notification builder) Step 2: Wire ProximityEngine events → NotificationHelper Step 3: Implement BroadcastReceiver for notification actions Step 4: Commit
Task 3.4: Check-in flow from proximity alert
Objective: When user taps "Check In" on a proximity notification, auto-create a check-in with current GPS coordinates and minimal interaction.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/checkin/QuickCheckinViewModel.kt - Modify: ProximityNotificationReceiver
Flow:
- User taps "Check In" on notification or FAB on asset detail
- QuickCheckinScreen shows asset name, current GPS coords, accuracy
- Optional: photo capture (CameraX), notes field
- "Submit" → POST /api/checkins
- On success: toast + dismiss notification + remove from activeProximities
- On failure (offline): save to PendingCheckin table, retry on connectivity
Step 1: QuickCheckinViewModel Step 2: QuickCheckinScreen (sheet/bottom sheet) Step 3: Pending check-in retry logic (WorkManager periodic task) Step 4: Commit
Phase 4: Camera & OCR
Task 4.1: Barcode scanner (CameraX + ML Kit)
Objective: Scan machine_id barcodes using device camera. Same as webapp scan tab but native Android performance.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/scan/ScanScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/scan/ScanViewModel.kt
Stack: CameraX + Google ML Kit Barcode Scanning (on-device, no network needed).
Alternative: ZXing (same as webapp) via com.journeyapps:zxing-android-embedded.
Flow:
- Open ScanScreen → CameraX preview with barcode overlay
- Detect barcode → extract machine_id
- Call
GET /api/assets/search?machine_id=... - If found → navigate to AssetDetail
- If not found → prompt "Add new asset?"
Step 1: ScanViewModel with ML Kit integration Step 2: ScanScreen with CameraX preview + barcode bounding box Step 3: Manual entry fallback (type machine_id) Step 4: Commit
Task 4.2: OCR photo upload for sticker IDs
Objective: Take photo of sticker, upload to backend OCR endpoint, display extracted machine_id.
Files:
- Modify:
ScanScreen.kt(add "Photo OCR" tab/mode) - Create:
android/app/src/main/java/com/canteen/assettracker/ui/scan/OcrViewModel.kt
Flow:
- Switch to OCR mode in ScanScreen
- Tap capture button → CameraX takePicture()
- Show preview, confirm or retake
- Upload as multipart to
POST /api/ocr - Show extracted machine_id + confidence
- Offer "Look up asset" button
Step 1: OcrViewModel (multipart upload via Retrofit) Step 2: Photo capture + preview UI Step 3: Commit
Phase 5: Polish & Release
Task 5.1: Offline mode handling
Objective: Gracefully handle no-network states. Show cached data, queue mutations.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/util/ConnectivityObserver.kt - Modify: All ViewModels (add offline-aware data sources)
Approach:
ConnectivityObservermonitorsConnectivityManager→ emitsStateFlow<Boolean>- When offline: load from Room, show "Offline" banner
- Pending check-ins saved to Room, synced via WorkManager when connectivity returns
- Asset list shows last-synced timestamp
Step 1: ConnectivityObserver Step 2: Offline data-source layer in repositories Step 3: WorkManager sync worker Step 4: Commit
Task 5.2: Settings screen
Objective: Server URL config, GPS update interval, geofence radius defaults, dark mode toggle, logout.
Files:
- Create:
android/app/src/main/java/com/canteen/assettracker/ui/settings/SettingsScreen.kt - Create:
android/app/src/main/java/com/canteen/assettracker/ui/settings/SettingsViewModel.kt
Settings stored via DataStore (Preferences):
server_url— default:https://canteen.ourpad.casa:8901gps_interval_seconds— default: 10, range: 5-60proximity_scan_radius_meters— default: 500dark_mode— boolean (app is dark by default)offline_mode— boolean (skip network calls, use Room only)
Step 1: DataStore preferences Step 2: SettingsScreen composable Step 3: Wire settings into LocationService and ProximityEngine Step 4: Commit
Task 5.3: Self-signed cert trust
Objective: The backend uses a self-signed TLS cert. OkHttp must trust it.
Files:
- Modify:
RetrofitModule.kt
Approach: Bundle cert.pem as raw resource. Create custom TrustManager that trusts the bundled cert. Alternatively: add network_security_config.xml for dev builds.
Step 1: Copy ~/projects/canteen-asset-tracker/cert.pem → android/app/src/main/res/raw/cert.pem
Step 2: Update OkHttpClient builder to trust the custom cert
Step 3: Commit
Task 5.4: Signed APK build + distribution
Objective: Produce a signed release APK for distribution.
Files:
- Create:
android/keystore.properties(gitignored) - Modify:
android/app/build.gradle.kts(signing config)
Step 1: Generate release keystore: keytool -genkey -v -keystore canteen-release.jks ...
Step 2: Signing config in build.gradle.kts
Step 3: ./gradlew assembleRelease → app/build/outputs/apk/release/app-release.apk
Step 4: Document installation: sideload APK on Android device
Step 5: Commit
Phase 6: Testing
Task 6.1: Unit tests (ViewModels, repositories, proximity engine)
Objective: Test core logic without device dependencies.
Files:
- Create:
android/app/src/test/java/com/canteen/assettracker/location/ProximityEngineTest.kt - Create:
android/app/src/test/java/com/canteen/assettracker/ui/login/LoginViewModelTest.kt - Create:
android/app/src/test/java/com/canteen/assettracker/ui/dashboard/DashboardViewModelTest.kt
Test targets:
- LoginViewModel: successful login, auth failure, network error
- ProximityEngine: inside geofence triggers event, outside doesn't, exit event, debounce
- DashboardViewModel: loads stats, handles empty, refresh
- Haversine distance: known coordinates (NYC → LA ~3944 km)
Step 1: Write ProximityEngineTest
Step 2: Write LoginViewModelTest
Step 3: Write DashboardViewModelTest
Step 4: Verify: ./gradlew test — all pass
Step 5: Commit
Task 6.2: Instrumentation tests (Room, Compose UI)
Objective: Test database operations and Compose UI on device/emulator.
Files:
- Create:
android/app/src/androidTest/java/com/canteen/assettracker/data/AssetDaoTest.kt - Create:
android/app/src/androidTest/java/com/canteen/assettracker/ui/login/LoginScreenTest.kt
Step 1: AssetDaoTest (insert, query, search, clear)
Step 2: LoginScreenTest (fill fields, tap sign in, verify navigation)
Step 3: Verify: ./gradlew connectedAndroidTest
Step 4: Commit
Technical Decisions & Tradeoffs
| Decision | Rationale | Alternative considered |
|---|---|---|
| Kotlin + Compose | Native performance, best GPS/BG service support | Flutter (BG services trickier), React Native (same) |
| OSMDroid (not Google Maps) | No API key required, works offline | Google Maps Compose (richer, needs billing) |
| Room (not SQLDelight) | Mature, Hilt integration, Compose-friendly | SQLDelight (KMP-friendly, overkill here) |
| FusedLocationProvider | Battery-efficient, Google Play Services | Raw GPS (LocationManager) — worse battery |
| Haversine client-side | No server round-trip per location update, instant | Server-side proximity (more accurate, network-dependent) |
| Local notifications (not FCM) | No Firebase dependency for basic alerts, works offline | FCM push (needed for server-triggered alerts in future) |
| Self-signed cert bundling | Backend uses self-signed TLS, must trust it for Retrofit | Let's Encrypt cert on backend (preferred long-term) |
Risks
- Background process killed by Android: Android 14+ aggressively kills background services. Mitigation: Foreground service with persistent notification. Test on Android 14/15.
- Battery drain: GPS every 10 seconds can drain battery. Mitigation: debounce when stationary, increase interval when battery low, use geofencing API for wide zones.
- Self-signed cert: Android 14+ blocks user-added CAs by default for apps targeting SDK 34+. Mitigation: network_security_config.xml for debug, or switch backend to Let's Encrypt.
- Location permissions: Users may deny background location — app becomes foreground-only. Mitigation: degrade gracefully, explain why background access is needed.
Open Questions
- Should proximity also check locations (buildings) in addition to individual assets?
- Should the app support multiple backend server profiles (e.g., different customer sites)?
- Do we need Firebase Cloud Messaging for server-pushed alerts, or are local notifications sufficient for v1?
- Should the app upload check-in photos immediately or queue them for WiFi-only upload?
Implementation Order (Recommended)
Phase 0 (Backend): Tasks 0.1 → 0.2 → 0.3 [prerequisite for all Android work]
Phase 1 (Scaffold): Tasks 1.1 → 1.2 → 1.3 [sequential, no parallelism]
Phase 2 (Core UI): Tasks 2.1 → (2.2, 2.3, 2.4) [2.2+2.3+2.4 can run in parallel after 2.1]
Phase 3 (GPS Engine): Tasks 3.1 → 3.2 → 3.3 → 3.4 [sequential dependencies]
Phase 4 (Camera): Tasks 4.1, 4.2 [can run in parallel]
Phase 5 (Polish): Tasks 5.1 → 5.2 → 5.3 → 5.4 [sequential]
Phase 6 (Testing): Tasks 6.1, 6.2 [can run in parallel]
Estimated total effort: ~25-35 hours of focused development.
Verification Checklist
- Backend: proximity API returns correct nearest assets
- Backend: geofence point-check works (inside/outside polygon)
- Android: app installs on API 26+ device
- Android: login flow completes against real backend
- Android: GPS location updates appear in logcat (foreground + background)
- Android: proximity notification fires when walking within 50m of a machine with coordinates
- Android: "Check In" from notification creates check-in on backend
- Android: asset list loads and displays from backend
- Android: barcode scan finds asset by machine_id
- Android: offline mode shows cached assets, queues check-ins
- Android: queued check-ins sync when connectivity returns
- Android: release APK installs and runs on a real device