Skip to content

Android: Driver post-login experience — vehicle selection & active tracking #36

@aaronbrethorst

Description

@aaronbrethorst

User Story

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.

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)

  • 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+, when ACCESS_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)
    • Database: store.go:SaveLocation (persists location points)
  • Dependencies:

    • Android login/auth story must be complete (JWT available)
    • Google Play Services for FusedLocationProviderClient
    • Android FOREGROUND_SERVICE_LOCATION permission (Android 14+)
    • POST_NOTIFICATIONS runtime permission (Android 13+)
  • Architecture considerations:

    • 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()
    • Add per-vehicle rate limiting (1 req / 5s / vehicle_id) using golang.org/x/time/rate
    • Enforce TLS in production via reverse proxy; set DATABASE_URL to sslmode=require
    • Android: set android:usesCleartextTraffic="false" in manifest
    • Tighten timestamp validation in handlers.go:validate() to reject timestamps more than +/- 5 minutes from server time (prevents replay attacks)
  • Suggested implementation sequence:

    1. Screen 1 + location permissions
    2. Foreground service + location reporting loop (Screen 3 core — can demo with hardcoded vehicle ID)
    3. Vehicle Setup bottom sheet (Screen 2) + favorites/recents
  • 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/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.
6 DoS via location flooding. No rate limiting. DoS High Per-vehicle rate limiting (1 req / 5s). Validate vehicle_id length.
7 Tracker memory exhaustion. Unbounded vehicle_id map growth. DoS Medium Cap tracked vehicles (e.g., 10,000). Validate format server-side.
8 JWT stored insecurely on device. Info Disclosure Medium EncryptedSharedPreferences + Android Keystore. Never log tokens.

Data Classification

Data Element Classification Handling Requirements
GPS coordinates (lat/lng/bearing/speed) PII / Location Data Encrypt in transit (TLS). Do not log at INFO level.
driver_id (to be added) PII Encrypt in transit. Do not expose in GTFS-RT feed.
JWT tokens Credential Never log. Encrypted storage. TLS only. ~1 hour expiry with refresh rotation.
vehicle_id Operational Combined with GPS can identify individuals. Same protections as GPS data.

SMART Assessment

Overall Readiness: Ready

Dimension Rating Key Observation
Specific Strong Three discrete screens with acceptance criteria tied to mockups; clear persona; explicit scope boundaries.
Measurable Strong Every criterion follows Given/When/Then; 10s interval, max 10 recents, HTTP status codes provide concrete thresholds.
Achievable Adequate Large but realistic within Milestone 3's 80-hour budget; foreground service lifecycle is main complexity risk.
Relevant Strong Directly implements Milestone 3; without this the Android app cannot fulfill its core purpose.
Time-bound Adequate Estimated as "Large"; implementation sequence guidance added to Technical Notes.

Edge Cases

Scenario Expected Behavior
GPS unavailable (tunnel/garage) App continues; skips reports; resumes on GPS return. "LIVE" → "GPS SEARCHING" after 30s.
Driver kills app during shift Foreground service destroyed. On relaunch, prompt to resume or end previous shift.
Device reboots during shift Shift state lost. Driver starts new shift on next login. No boot receiver in v1.
Vehicle ID in use by another driver Server accepts (no exclusivity check in v1). Vehicle exclusivity deferred.
GPS jitter (rapid updates) App enforces 10s minimum interval; intermediate updates discarded.
Location permission revoked while tracking Foreground service detects loss, stops tracking, notifies driver, returns to empty map.
Very long shift (8+ hours) Must handle without memory leaks or excessive battery drain. Verify with profiling.
Vehicle ID with special chars / long string Client enforces alphanumeric + hyphens/underscores, max 50 chars. Server validates independently (400).
Replay attack with old timestamps Server rejects timestamps > +/- 5 minutes from server time.
Attacker floods endpoint Per-vehicle rate limiting (1 req/5s) returns 429. Tracker caps at 10,000 vehicles.
Stolen JWT from another device Short expiry (~1 hour) limits window. Refresh rotation invalidates old tokens.

Resolved Questions

  • Favorites/recents sync: Local-only storage for v1.
  • Map provider: Google Maps SDK.
  • Notification "Stop Shift" button: Yes — included.
  • Maximum favorites: No limit.
  • JWT auth middleware dependency: On track (Milestone 2).

Mockups

Image

Image Image

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions