You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a transit driver,
I want to select my vehicle and start my shift so the app tracks and reports my location in real time,
so that my agency's GTFS-RT feed reflects my vehicle's live position for riders and dispatchers.
The vehicle-positions project provides a Go backend that ingests GPS location reports and generates GTFS-RT Vehicle Positions feeds for transit agencies in developing countries that lack dedicated AVL systems. An Android companion app is the primary means for drivers to report their positions. This story covers the core post-login experience: the driver has already authenticated and now needs to configure their shift (select a vehicle) and begin live tracking. The three UI states — empty map, vehicle setup, and active tracking — are defined by existing mockups following Material 3 guidelines.
[Assumption] Authentication (login, JWT token storage, token refresh) is handled by a prior story and is complete before this story begins. The app already holds a valid JWT when this flow starts.
The vehicle list (favorites + recents) is stored locally on-device only (no server sync in v1). The app uses Google Maps SDK for the map.
Acceptance Criteria
Screen 1: Empty Map (Post-Login Landing)
Given a driver has just logged in, when the app loads the home screen, then a full-screen grayscale map is displayed centered on the device's current location (or the agency's default region if location permission is not yet granted).
Given the home screen is displayed, when no shift is active, then a transparent top app bar shows "Transit Driver" and a large FAB (+) is visible in the bottom-right corner.
Given the home screen is displayed, when the driver has not yet granted location permission, then the app first requests ACCESS_FINE_LOCATION with a rationale dialog.
Given the driver is on Android 11+, whenACCESS_FINE_LOCATION has been granted but ACCESS_BACKGROUND_LOCATION has not, then the app shows an explanatory screen directing the driver to grant "Allow all the time" in system Settings before enabling "Start Shift."
Screen 2: Vehicle Setup (Bottom Sheet)
Given the driver is on the empty map screen, when they tap the FAB (+), then an M3 bottom sheet slides up displaying the "Vehicle Setup" form.
Given the vehicle setup sheet is displayed, when the driver views it, then they see a search/text field for Vehicle ID, a "Favorites" section with starred vehicles, and a "Recent Vehicles" section showing previously-used vehicle IDs.
Given the vehicle setup sheet is displayed, when the driver types in the Vehicle ID field, then the list filters to matching vehicle IDs in real time.
Given the driver has selected or entered a Vehicle ID, when they tap "Start Shift", then the app transitions to the active tracking screen and begins reporting location to the server.
Given the driver has not entered a Vehicle ID, when they tap "Start Shift", then the button remains disabled or an inline validation error is shown.
Given the vehicle setup sheet is open, when the driver taps outside the sheet or swipes it down, then the sheet dismisses and the driver returns to the empty map.
Screen 3: Active Tracking
Given a shift has been started, when the active tracking screen loads, then the map style changes from grayscale to full color, the top app bar shows "Active Vehicle: {vehicle_id}", and a "LIVE" indicator is visible.
Given active tracking is running, when the device obtains a GPS fix, then a pulsing vehicle icon is rendered at the driver's current position on the map.
Given active tracking is running, when 10 seconds have elapsed since the last report, then the app sends a POST /api/v1/locations request with vehicle_id, latitude, longitude, bearing, speed, accuracy, and timestamp.
Given active tracking is running, when the driver taps the "Stop Shift" extended FAB, then location reporting stops, the foreground service is torn down, and the app returns to the empty map (grayscale) state.
Given active tracking is running, when the app is backgrounded or the screen is locked, then a foreground service with a persistent notification continues reporting location at the 10-second interval.
Given active tracking is running, when no GPS fix has been received for 30+ seconds, then the "LIVE" indicator changes to a visually distinct "GPS SEARCHING" state (e.g., yellow or pulsing differently) so the driver knows their position is not being reported.
Given active tracking is running, when a location report receives an HTTP error (4xx/5xx), then the error is logged locally and the next report is attempted on schedule (no retry backoff in v1).
Given active tracking is running, when the server returns HTTP 401 (token expired), then the app attempts a token refresh; if refresh fails, tracking stops and the driver is returned to the login screen with a message.
Given a location report fails with HTTP 401 and the token refresh also fails, when the app transitions to the login screen, then the stored JWT is deleted from the device.
Favorites & Recents
Given the driver completes a shift with a vehicle, when the shift ends, then that vehicle ID is added to the "Recent Vehicles" list (most recent first, max 10 entries, persisted locally).
Given the vehicle setup sheet is displayed, when the driver long-presses a vehicle in the recent list, then they can toggle it as a "Favorite" (starred), which pins it to the Favorites section.
App Crash / Resume
Given the driver force-kills and re-opens the app, when a shift was previously active (incomplete shift detected), then the app displays a prompt asking "Resume shift with Vehicle {id}?" or "End previous shift?" rather than silently starting fresh.
Given the foreground service is running, when the persistent notification is displayed, then the notification shows the active vehicle ID, elapsed shift time, and a "Stop Shift" action button.
Given the foreground service notification is displayed, when the driver taps the "Stop Shift" notification action, then location reporting stops, the foreground service is torn down, and the app returns to the empty map state (same behavior as the in-app FAB).
Security
Given a POST /api/v1/locations request with no Authorization header, when the server processes it, then the server returns HTTP 401 and does not persist or broadcast the location.
Given a POST /api/v1/locations request with an expired JWT, when the server processes it, then the server returns HTTP 401 with a body indicating token expiration, enabling the app's refresh flow.
Given a valid JWT belonging to Driver A, when Driver A submits a location report, then the driver_id from the JWT claims is stored alongside the location point in the database.
Given a vehicle_id longer than 50 characters or containing characters outside [a-zA-Z0-9_-], when submitted to POST /api/v1/locations, then the server returns HTTP 400 with a descriptive validation error.
Given a driver submits location reports more frequently than once per 5 seconds for the same vehicle_id, when the excess requests arrive, then the server returns HTTP 429 and does not persist the duplicate reports.
Given the Android app is configured for production, when it makes any network request, then it uses HTTPS exclusively (android:usesCleartextTraffic="false" in the manifest, certificate validation enabled).
Given the Android app stores a JWT, when the token is persisted locally, then it is stored using EncryptedSharedPreferences backed by the Android Keystore.
User Flow
flowchart TD
A[Driver logs in successfully] --> B[Screen 1: Empty Map]
B --> C{Tap FAB +}
C --> D[Screen 2: Vehicle Setup Bottom Sheet]
D --> E{Select or enter Vehicle ID}
E -->|Vehicle selected| F[Tap 'Start Shift']
E -->|Dismiss sheet| B
F --> G[Start Foreground Service]
G --> H[Screen 3: Active Tracking]
H --> I[Send location every 10s via POST /api/v1/locations]
I --> J{HTTP Response}
J -->|201 Created| I
J -->|401 Unauthorized| K[Attempt token refresh]
K -->|Refresh OK| I
K -->|Refresh failed| L[Return to Login]
J -->|Other error| M[Log error, continue]
M --> I
H --> N{Tap 'Stop Shift'}
N --> O[Stop foreground service]
O --> B
Loading
Sequence Diagram: Location Reporting
sequenceDiagram
actor Driver
participant App as Android App
participant FLS as FusedLocationProvider
participant API as Go Server
Driver->>App: Tap "Start Shift"
App->>App: Start Foreground Service
App->>FLS: Request location updates (10s interval)
loop Every 10 seconds
FLS-->>App: Location update
App->>API: POST /api/v1/locations {vehicle_id, lat, lng, ...}
alt Success
API-->>App: 201 Created
else Token expired
API-->>App: 401 Unauthorized
App->>API: Refresh token
alt Refresh OK
API-->>App: New JWT
App->>API: Retry POST /api/v1/locations
else Refresh failed
App->>Driver: Session expired, return to login
end
else Server error
API-->>App: 5xx
App->>App: Log error, skip this cycle
end
end
Driver->>App: Tap "Stop Shift"
App->>FLS: Remove location updates
App->>App: Stop Foreground Service
Loading
Out of Scope
Login / authentication flow — covered by a separate story; this story assumes a valid JWT is already available.
Route or trip selection — the driver selects only a Vehicle ID; route/trip assignment is deferred.
Offline queuing — if the device has no connectivity, location reports are dropped in v1. Offline buffering is a separate future story.
Admin vehicle management API — the server endpoint for listing/managing vehicles is a separate backend story.
Schedule adherence / "On Time" display — visible in Concept 3 mockup but requires GTFS static schedule integration, which is a separate effort.
Map route polyline — the active tracking mockup shows a route line, but without trip/route selection this is deferred.
Battery optimization beyond foreground service — Doze exemption and adaptive reporting intervals are deferred.
Technical Notes
Affected components:
New Android module: post-login navigation, Vehicle Setup screen, Active Tracking screen
Existing server endpoint: POST /api/v1/locations (handlers.go:handlePostLocation)
In-memory tracker: tracker.go (receives updates from location reports)
MVVM + Repository pattern with Hilt DI, per project README
Foreground service is mandatory for continuous background location on Android 10+
Location payload must match the existing LocationReport struct: vehicle_id (required), trip_id (optional, omitted in v1), latitude, longitude, bearing, speed, accuracy, timestamp
Vehicle ID favorites/recents stored in Room database or DataStore locally
Large touch targets (48dp+) for driver safety — all interactive elements must meet this minimum
Map SDK: Google Maps SDK
Data changes: Add a driver_id TEXT NOT NULL DEFAULT '' column to location_points to create an audit trail. Update InsertLocationPoint in db/query.sql to accept and store the driver identity from JWT claims. No other schema changes required.
Hardening (from security review):
Implement JWT auth middleware and apply to POST /api/v1/locations in main.go before this story ships
Add vehicle_id format validation (alphanumeric + hyphens/underscores, max 50 chars) in handlers.go:validate()
This story introduces a continuous data ingestion path from mobile clients to the server that directly feeds a public GTFS-RT transit feed. The POST /api/v1/locations endpoint currently has no authentication middleware — JWT auth middleware (Milestone 2) must be implemented and applied before this story ships.
Threats & Mitigations
#
Threat
STRIDE
Severity
Mitigation
1
Unauthenticated location injection.POST /api/v1/locations has no auth middleware.
Spoofing
Critical
Blocker. JWT verification middleware must be applied. Server rejects missing/expired tokens with 401.
2
Vehicle ID spoofing across drivers. Any authenticated driver can submit locations for any vehicle_id.
Spoofing
Medium
Log driver_id alongside each location report. Defer assignment enforcement.
3
Location data in transit over plain HTTP. JWTs and GPS data can be intercepted.
Tampering
High
Require TLS in production. Android: usesCleartextTraffic="false". DB: sslmode=require.
4
No audit trail for submissions.location_points lacks driver identity.
Repudiation
Medium
Add driver_id column populated from JWT claims.
5
GPS in GTFS-RT feed reveals driver location.
Info Disclosure
Low
Inherent to GTFS-RT. Ensure feed does not leak driver_id or device IDs.
User Story
Epic/Initiative: Android Driver Companion App (Milestone 3)
Priority: Critical
Estimated Complexity: Large
Background & Context
The vehicle-positions project provides a Go backend that ingests GPS location reports and generates GTFS-RT Vehicle Positions feeds for transit agencies in developing countries that lack dedicated AVL systems. An Android companion app is the primary means for drivers to report their positions. This story covers the core post-login experience: the driver has already authenticated and now needs to configure their shift (select a vehicle) and begin live tracking. The three UI states — empty map, vehicle setup, and active tracking — are defined by existing mockups following Material 3 guidelines.
[Assumption] Authentication (login, JWT token storage, token refresh) is handled by a prior story and is complete before this story begins. The app already holds a valid JWT when this flow starts.
The vehicle list (favorites + recents) is stored locally on-device only (no server sync in v1). The app uses Google Maps SDK for the map.
Acceptance Criteria
Screen 1: Empty Map (Post-Login Landing)
ACCESS_FINE_LOCATIONwith a rationale dialog.ACCESS_FINE_LOCATIONhas been granted butACCESS_BACKGROUND_LOCATIONhas not, then the app shows an explanatory screen directing the driver to grant "Allow all the time" in system Settings before enabling "Start Shift."Screen 2: Vehicle Setup (Bottom Sheet)
Screen 3: Active Tracking
POST /api/v1/locationsrequest withvehicle_id,latitude,longitude,bearing,speed,accuracy, andtimestamp.Favorites & Recents
App Crash / Resume
Security
POST /api/v1/locationsrequest with noAuthorizationheader, when the server processes it, then the server returns HTTP 401 and does not persist or broadcast the location.POST /api/v1/locationsrequest with an expired JWT, when the server processes it, then the server returns HTTP 401 with a body indicating token expiration, enabling the app's refresh flow.driver_idfrom the JWT claims is stored alongside the location point in the database.vehicle_idlonger than 50 characters or containing characters outside[a-zA-Z0-9_-], when submitted toPOST /api/v1/locations, then the server returns HTTP 400 with a descriptive validation error.vehicle_id, when the excess requests arrive, then the server returns HTTP 429 and does not persist the duplicate reports.android:usesCleartextTraffic="false"in the manifest, certificate validation enabled).EncryptedSharedPreferencesbacked by the Android Keystore.User Flow
flowchart TD A[Driver logs in successfully] --> B[Screen 1: Empty Map] B --> C{Tap FAB +} C --> D[Screen 2: Vehicle Setup Bottom Sheet] D --> E{Select or enter Vehicle ID} E -->|Vehicle selected| F[Tap 'Start Shift'] E -->|Dismiss sheet| B F --> G[Start Foreground Service] G --> H[Screen 3: Active Tracking] H --> I[Send location every 10s via POST /api/v1/locations] I --> J{HTTP Response} J -->|201 Created| I J -->|401 Unauthorized| K[Attempt token refresh] K -->|Refresh OK| I K -->|Refresh failed| L[Return to Login] J -->|Other error| M[Log error, continue] M --> I H --> N{Tap 'Stop Shift'} N --> O[Stop foreground service] O --> BSequence Diagram: Location Reporting
sequenceDiagram actor Driver participant App as Android App participant FLS as FusedLocationProvider participant API as Go Server Driver->>App: Tap "Start Shift" App->>App: Start Foreground Service App->>FLS: Request location updates (10s interval) loop Every 10 seconds FLS-->>App: Location update App->>API: POST /api/v1/locations {vehicle_id, lat, lng, ...} alt Success API-->>App: 201 Created else Token expired API-->>App: 401 Unauthorized App->>API: Refresh token alt Refresh OK API-->>App: New JWT App->>API: Retry POST /api/v1/locations else Refresh failed App->>Driver: Session expired, return to login end else Server error API-->>App: 5xx App->>App: Log error, skip this cycle end end Driver->>App: Tap "Stop Shift" App->>FLS: Remove location updates App->>App: Stop Foreground ServiceOut of Scope
Technical Notes
Affected components:
POST /api/v1/locations(handlers.go:handlePostLocation)tracker.go(receives updates from location reports)store.go:SaveLocation(persists location points)Dependencies:
FusedLocationProviderClientFOREGROUND_SERVICE_LOCATIONpermission (Android 14+)POST_NOTIFICATIONSruntime permission (Android 13+)Architecture considerations:
LocationReportstruct:vehicle_id(required),trip_id(optional, omitted in v1),latitude,longitude,bearing,speed,accuracy,timestampData changes: Add a
driver_id TEXT NOT NULL DEFAULT ''column tolocation_pointsto create an audit trail. UpdateInsertLocationPointindb/query.sqlto accept and store the driver identity from JWT claims. No other schema changes required.Hardening (from security review):
POST /api/v1/locationsinmain.gobefore this story shipsvehicle_idformat validation (alphanumeric + hyphens/underscores, max 50 chars) inhandlers.go:validate()golang.org/x/time/rateDATABASE_URLtosslmode=requireandroid:usesCleartextTraffic="false"in manifesthandlers.go:validate()to reject timestamps more than +/- 5 minutes from server time (prevents replay attacks)Suggested implementation sequence:
Location report payload example:
{ "vehicle_id": "2045", "latitude": -1.2921, "longitude": 36.8219, "bearing": 180.0, "speed": 8.5, "accuracy": 12.0, "timestamp": 1752566400 }Security Assessment
Threat Level: High
This story introduces a continuous data ingestion path from mobile clients to the server that directly feeds a public GTFS-RT transit feed. The
POST /api/v1/locationsendpoint currently has no authentication middleware — JWT auth middleware (Milestone 2) must be implemented and applied before this story ships.Threats & Mitigations
POST /api/v1/locationshas no auth middleware.vehicle_id.driver_idalongside each location report. Defer assignment enforcement.usesCleartextTraffic="false". DB:sslmode=require.location_pointslacks driver identity.driver_idcolumn populated from JWT claims.driver_idor device IDs.vehicle_idlength.vehicle_idmap growth.EncryptedSharedPreferences+ Android Keystore. Never log tokens.Data Classification
driver_id(to be added)vehicle_idSMART Assessment
Overall Readiness: Ready
Edge Cases
Resolved Questions
Mockups