Design Maps & Live Location Tracking
A deep-dive Android system design breakdown of real-time location tracking β covering FusedLocationProviderClient, WebSocket streaming, smooth marker animation, geofencing, battery strategy, and every edge case interviewers probe at senior and staff level.
Design the Android client for maps and live location tracking β a driver's position updating in real-time on a passenger's screen in Ola/Uber, or a delivery agent tracked in Swiggy. The core challenges: getting accurate location efficiently without destroying battery, streaming location over a persistent connection, animating a map marker smoothly between GPS fixes, drawing and trimming a route polyline, and navigating Android's complex permission landscape.
Three technologies work in concert. FusedLocationProviderClient fuses GPS, Wi-Fi, and cell towers, selecting the best source for the requested accuracy/battery trade-off. WebSocket provides a bi-directional persistent connection to stream location at low latency, with exponential-backoff reconnection. ValueAnimator linearly interpolates a map marker between GPS fixes at 60 fps so it glides rather than teleports, even with updates every 2β5 seconds.
β Functional Requirements
- Show the user's own location on the map in real time
- Track a driver/agent's position received from the server
- Draw a route polyline, origin β destination
- Animate the driver marker smoothly between position updates
- Show ETA and distance, updated as driver moves
- Geofence: alert when driver enters the pickup radius
- Driver app uploads location continuously in background
β Non-Functional Requirements
- Location must not drain battery β choose right accuracy tier
- Driver app: Foreground Service required for background location
- Android 10+ requires explicit ACCESS_BACKGROUND_LOCATION
- Marker animation must feel smooth (60 fps interpolation)
- Graceful degradation when GPS signal is weak
- Map tiles load offline (cached tiles)
- Camera auto-follows driver unless user manually pans
Three core models drive the system. DriverLocation is a single GPS fix uploaded by the driver. TripState is the driver-side state machine that determines accuracy tier and upload interval. LocationUpdateMessage is the WebSocket payload the server fans out to passengers.
| Fragment / UI | LocationViewModel | WebSocketManager | Marker / Map | FusedLocation |
|---|---|---|---|---|
| 1.onViewCreated() calls viewModel.startTracking(tripId) | Opens WS to wss://api/trip/{tripId}/track | β | β | β |
| β | β | 2.WS connects. Server sends initial snapshot {lat, lng, bearing, eta} | β | β |
| β | 3.Emits DriverState.Located(pos, bearing) via StateFlow | β | β | β |
| 4.Fragment collects StateFlow. Calls markerAnimator.animateTo() + cameraTracker.onDriverMoved() | β | β | Marker appears. Camera animates with tilt + bearing. | Passenger blue dot via map.isMyLocationEnabled = true |
| β | β | β | 5.PolylineDrawer fetches route from Directions API and draws polyline origin β destination | β |
| Driver / Server | WS (Passenger) | LocationViewModel | MarkerAnimator | GoogleMap |
|---|---|---|---|---|
| 1.Driver FusedLocation fires. App sends {lat, lng, bearing} every 2 s | β | β | β | β |
| Server fans out to all passengers on this tripId | 2.WS receives onMessage(json). Parses LocationUpdateMessage. | β | β | β |
| β | β | 3.Emits DriverState.Located(newLatLng, newBearing, etaMinutes) | β | β |
| β | β | β | 4.ValueAnimator lerps marker oldβnew over 1 000 ms. Bearing rotates smoothly. 60 fps. | Marker glides. ETA label updates. |
| β | β | β | 5.polylineDrawer.trimUpTo(driverPos) β removes travelled portion | Polyline shrinks from origin end. |
| OS / FusedLocation | GeofencingClient | BroadcastReceiver | NotificationManager | UI / Fragment |
|---|---|---|---|---|
| 1.OS monitors driver against geofences at low power (no GPS chip) | β | β | β | β |
| β | 2.Driver crosses 200 m pickup radius. OS fires PendingIntent. | β | β | β |
| β | β | 3.onReceive() confirms ENTER for "PICKUP_ZONE". | β | β |
| β | β | 4.Removes geofence. Posts "Driver is 200 m away" notification. | Fires with sound + vibration on CH_DRIVER_NEARBY | β |
| β | β | β | β | 5.If foreground: StateFlow emits DriverState.Nearby β "Driver arriving!" banner |
| Use Case | Required Permission | When Prompted | Notes |
|---|---|---|---|
| Passenger blue dot | ACCESS_FINE_LOCATION | Runtime β app open | Needed for HIGH_ACCURACY. |
| Driver background upload | ACCESS_BACKGROUND_LOCATION | Separate dialog Android 10+, Settings on 11+ | Request FINE first, then BACKGROUND separately. Most critical. |
| Geofencing | FINE + BACKGROUND_LOCATION | Same as above | Geofences fire when app is killed β needs background permission. |
| Foreground Service (Android 14+) | FOREGROUND_SERVICE_LOCATION | Manifest only β no dialog | Add android:foregroundServiceType="location" in manifest. |
These are the questions that separate senior from staff. Each one represents a real production failure mode in a live-tracking app.
The problem: Driver enters a tunnel or underground parking. GPS satellite signal drops. FusedLocation falls back to Wi-Fi/cell positioning β accuracy degrades to 50β200 m. Updates may stop entirely if the driver is deep underground.
The fix: FusedLocation handles the hardware fallback automatically, but you need to surface accuracy to the UI. Check location.accuracy on each callback β if it exceeds 100 m, show an accuracy ring around the marker (a semi-transparent circle proportional to accuracy radius). On the passenger side: if no WS message arrives for >10 s, grey out the marker and show "Weak signal β tracking paused". Resume automatically when updates return. Never stop the Foreground Service β it will resume once the driver exits the tunnel.
Dead reckoning (staff-level): Extrapolate position between fixes using last known speed and bearing: newLat β lastLat + (speed * cos(bearing) * dt / R). Continue animating the marker on the estimated trajectory rather than freezing it.
The problem: The passenger's WebSocket drops mid-trip β network switch, server restart, or brief connectivity loss. The marker freezes. The passenger sees stale data with no indication anything is wrong.
The fix: In onFailure(), immediately emit DriverState.Disconnected(attempt = n). UI shows a "Reconnecting..." banner and greys the marker. Reconnect with exponential backoff: delay = min(base * 2^attempt, 30_000) + jitter. Also listen to ConnectivityManager.NetworkCallback.onAvailable() to trigger immediate reconnect when network returns β don't wait for the backoff timer. On reconnect, request a fresh server snapshot (last position + ETA) before resuming streaming. Cap at 10 retries then show a manual "Retry" button.
The problem: Android's Low Memory Killer or Doze mode kills the driver's Foreground Service during a long shift. Location uploads stop. The passenger's tracker goes stale.
The fix: Return START_STICKY from onStartCommand() β the OS restarts the service with a null Intent when resources free up. The service rebuilds its LocationRequest and resumes uploading. For Doze: Foreground Services are exempt as long as the notification is visible β this is why the persistent notification is mandatory, not optional. Additionally, guide the driver to whitelist the app from battery optimisation via Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS), explaining the reason clearly. Don't use PendingIntent for the driver upload path β that's for low-frequency geofencing, not 2 s uploads.
The problem: WebSocket messages arrive faster than the 1 s animation duration β highway driving, server sends updates every 500 ms. Animations queue up, causing the marker to lag further and further behind the driver's actual position, eventually showing a position that's 5β10 seconds stale.
The fix: In MarkerAnimator.animateTo(), always call activeAnimator?.cancel() before starting a new animation. Cancelling mid-animation leaves the marker at its current interpolated position and immediately starts the new one from there β no queue, no lag. The marker catches up instantly. Also make animation duration adaptive: if updates arrive every 500 ms, use 500 ms duration rather than 1 000 ms β match the animation to the update frequency.
The problem: Android 12 introduced one-time location grants. The user granted "Only this time" β when they background the app and return, location access has expired. Or the user goes to Settings and revokes the permission during an active trip.
The fix: FusedLocation stops calling your LocationCallback silently when permission is revoked β no exception, just silence. Detect this: if no update arrives within 2Γ the expected interval, check ContextCompat.checkSelfPermission(). If revoked, emit a PermissionRevoked state and show a non-dismissable dialog explaining the impact, with a direct link to Settings. On the passenger side, if the server stops receiving driver updates it can push a separate "tracking unavailable" WS message.
The problem: The driver stops at a red light. GPS chipsets oscillate Β±5β15 m β generating constant callbacks even though the car hasn't moved. Without filtering this generates hundreds of unnecessary server uploads per minute and causes the marker to jitter nervously on the passenger's screen.
The fix: Use setMinUpdateDistanceMeters(5f) in LocationRequest.Builder β FusedLocation suppresses callbacks if the device has moved less than 5 m since the last update. Additionally check location.speed β if speed is 0 m/s, skip the WebSocket upload even if a callback fires. On the animation side: if the new position is within 3 m of the current marker position, skip the ValueAnimator entirely. Combined effect: zero uploads and zero animation churn during red lights.
The problem: The driver enters an area with no connectivity. The map SDK can't fetch tiles β large grey squares appear. The route polyline still displays (it's vector data in memory), but the blank background map makes navigation confusing.
The fix: (1) Use the Maps SDK's built-in offline area support β let the user pre-download a region before the trip. (2) Implement a custom TileProvider: override getTile(x, y, zoom), check a local LRU disk cache, return cached bytes if found, otherwise fetch and cache. Pre-download tiles for the route corridor at trip start: compute the route polyline's bounding box, determine tile coordinates at zoom 14β16, and fetch them via WorkManager before the driver departs.
The problem: The passenger rotates their phone or the process is killed under low RAM. The tracking Fragment is recreated. Naively this re-fetches everything, flashes a spinner, and resets the camera.
The fix (config change): The LocationViewModel survives rotation. The WebSocket lives inside the ViewModel and stays connected. The Fragment re-collects the existing StateFlow β the last emitted DriverState is replayed immediately (StateFlow always emits current value to new collectors). The marker and camera restore from the last state in under 100 ms.
The fix (process death): The Foreground Service is independent of the Fragment β it keeps running. When the passenger app restarts, the ViewModel reconnects to the WebSocket and receives the server's fresh snapshot within one round-trip. Use SavedStateHandle to persist the tripId so the ViewModel reconnects to the correct trip.
- Knows FusedLocationProviderClient vs raw LocationManager
- Can set up LocationRequest with interval and priority
- Understands foreground/background permission difference
- Can add a marker and draw a polyline with the Maps SDK
- Knows a Foreground Service is required for background GPS
- Uses animateCamera() to move the map view
- Implements ValueAnimator LatLng lerp for smooth marker movement
- Handles bearing wrap-around (359Β° β 1Β°) in animation
- Cancels active animator to prevent queue backlog on rapid updates
- CameraTracker: auto-follow disabled on user pan gesture
- Polyline trimming β removes travelled portion as driver moves
- GeofencingClient for arrival alerts without polling
- Adapts priority/interval by TripState to save battery
- Exponential backoff WebSocket reconnection
- Dead reckoning: extrapolate marker between fixes (Kalman filter)
- Adaptive animation duration matching update frequency
- Custom TileProvider + MBTiles for offline map tiles
- Location spoof detection: isMock, velocity sanity, sensor fusion
- ClusterManager for many driver markers at city scale
- Accuracy ring overlay β surfaces GPS uncertainty visually
- Speed-adaptive GPS interval (faster updates on highways)
- Battery impact metering across driver shift with Battery Historian
FusedLocationProviderClient combines GPS, Wi-Fi positioning, Bluetooth beacons, and cell towers, choosing the best source for the requested accuracy/battery trade-off. Raw LocationManager with GPS_PROVIDER only uses satellites β cold start takes 30β60 s, fails indoors, and drains battery constantly. FLPC gets a fix in ~1β2 s by using the last known Wi-Fi position immediately while GPS warms up. For consumer apps, always use FLPC over raw LocationManager.
HIGH_ACCURACY activates the GPS hardware β 3β5 m accuracy but significant battery cost. Use it during active trips. BALANCED_POWER uses Wi-Fi and cell towers only β no GPS chip activated, 50β100 m accuracy, minimal battery. Use it for the passenger's own blue dot, or the driver app during IDLE/SEARCHING state. Only activate HIGH_ACCURACY when the driver is actively on a trip.
Since Android 8, location updates in a background service are throttled to once per hour. A Foreground Service with a visible notification has much higher OS priority and receives continuous updates. Declare android:foregroundServiceType="location" in the manifest (required since Android 10), and call startForeground(id, notification) within 5 seconds of service start. Return START_STICKY so the OS restarts the service if killed under memory pressure.
Use ValueAnimator.ofFloat(0f, 1f) with duration matching the update interval (1 000 ms). In addUpdateListener, linearly interpolate lat/lng: lat = startLat + (endLat - startLat) * t. Also interpolate bearing β handle the 359Β° β 1Β° wrap: compute delta, if >180 subtract 360, if <-180 add 360. Crucially, cancel any active animation before starting a new one (activeAnimator?.cancel()) to prevent backlog when updates arrive faster than the animation completes.
Introduced in Android 10, it's a separate runtime permission from ACCESS_FINE_LOCATION, granting location access when the app is not visible. You must request FINE first; only after that can you request BACKGROUND. On Android 11+, you can't request both in the same dialog β the system forces the user to Settings. The Play Store also requires justification. For driver apps, explain the benefit clearly before requesting.
Register GoogleMap.setOnCameraMoveStartedListener. The callback receives a reason: REASON_GESTURE means the user is panning β set isFollowing = false. REASON_API_ANIMATION means your own code called animateCamera() β don't change the flag. When isFollowing is false, skip animateCamera() but still animate the marker. Show a "Re-center" FAB that sets isFollowing = true and snaps back to the driver.
You register a Geofence with a circular region and transition types. Play Services monitors the device position at the OS level using low-power cell/Wi-Fi β no GPS chip activated. When a transition is detected, it fires a PendingIntent to your BroadcastReceiver. Your app code doesn't run during monitoring β the OS handles everything and only wakes your app on boundary crossing. Far more battery-efficient than polling your own location every few seconds.
Fetch the route from the Directions API as a list of LatLng waypoints. Draw with map.addPolyline(PolylineOptions().addAll(points)). Store the Polyline reference. As the driver moves, find the closest waypoint to the driver's current position, then set polyline.points = originalPoints.drop(closestIndex). This gives the "road disappearing behind" effect without a full redraw. Don't remove and re-add the polyline on every update β updating points in-place avoids a redraw flash.
setMinUpdateDistanceMeters(5f) suppresses callbacks if the device hasn't moved more than 5 m since the last update, even if the time interval has elapsed. This filters GPS jitter when a driver is stationary β the chipset oscillates slightly, generating false position updates. Without this filter you'd upload hundreds of tiny "moves" per minute. Combine with the time interval for both time-based and distance-based filtering.
On onFailure(), emit DriverState.Disconnected(attempt = n). UI shows "Reconnecting..." and greys the marker. Reconnect with exponential backoff: min(base * 2^attempt, 30_000) + jitter. Listen to ConnectivityManager.NetworkCallback.onAvailable() to trigger immediate reconnect when network returns β don't wait for the timer. On reconnect, request a fresh snapshot from the server before resuming streaming. Cap retries at 10, then show a "Retry" button.
Use ClusterManager from Maps Android Utils. It groups nearby markers into a cluster showing the count, splitting on zoom-in. For updates: maintain a Map<driverId, Marker>. On each position update, set marker.position = newLatLng directly β O(1), preserves cluster grouping. For high volumes (100+ drivers): filter to only markers within the visible map bounds via map.projection.visibleRegion.latLngBounds.contains(pos) before adding to ClusterManager.
Dead reckoning extrapolates position between GPS fixes using last known speed and bearing: newLat β lastLat + (speed * cos(bearing) * dt / R). If the driver is doing 14 m/s at 90Β° and the GPS fix is 1 s late, you estimate the position and keep the marker moving continuously. A Kalman filter combines these estimates with actual measurements, weighted by uncertainty, to produce a smooth filtered track β used in Google Maps navigation. Trade-off: errors compound if speed/bearing change during the gap.
Generate a JSON style file at mapstyle.withgoogle.com and save it in res/raw/map_style.json. Apply with: map.setMapStyle(MapStyleOptions.loadRawResourceStyle(context, R.raw.map_style)) inside onMapReady(). For dark mode: detect resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES and load a dark JSON. The style is client-side β no server changes. Common use: hide POI labels to reduce visual noise during navigation.
Three cases after RequestPermission(): (1) Granted β proceed. (2) Denied but not permanently β show rationale dialog, re-request. (3) Permanently denied (shouldShowRequestPermissionRationale() returns false) β show "Open Settings" dialog launching Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS). Never loop permission requests. Show the feature as disabled with a CTA. For background location on Android 11+, the user is always directed to Settings β prepare UI for this flow.
Two approaches. (1) Maps SDK offline areas β built-in support, user downloads a rectangular region. No custom code needed. (2) Custom TileProvider: implement getTile(x, y, zoom) β check a local disk cache (LRU), return cached bytes if found, else fetch and cache. Use MBTiles for structured offline storage. Register with map.addTileOverlay(TileOverlayOptions().tileProvider(yourProvider)). For trip-start pre-caching: compute route bounding box, determine tile coordinates at zoom 14β16, fetch via WorkManager before departure.
fusedClient.lastLocation resolves instantly from cache β no GPS activation β but can be null after a fresh reboot. Use it for: showing an initial map position before live updates start, pre-populating a pickup address field, or a coarse check whether the user is in a service area. Don't use it as the main tracking mechanism β it's stale by design. Start requestLocationUpdates() for live data and use lastLocation as a quick first position while the live stream warms up.
Multiple strategies together: (1) Trip-state-adaptive priority β LOW_POWER when IDLE, HIGH_ACCURACY only during EN_ROUTE. (2) setMinUpdateDistanceMeters(5f) β suppresses callbacks when stationary. (3) Client-side filtering before WS send β skip upload if speed == 0 or delta < 3 m. (4) Batch uploads β buffer 3β5 fixes to reduce radio activations. (5) Guide driver to whitelist the app from battery optimisation via ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS β only during active trips. (6) Verify with Battery Historian before shipping.
Multiple signals: (1) location.isMock (API 31+) β true if a mock location app is active. (2) Cross-check GPS vs network location β consistent disagreement >1 km is suspicious. (3) Velocity sanity β position jumps 10 km in 1 second is physically impossible. (4) Sensor fusion β compare GPS bearing with accelerometer/gyroscope; spoofed GPS that "teleports" won't match inertial data. (5) Check if developer options mock locations is enabled. Flag signals server-side in an anti-fraud system β don't block locally.
map.isMyLocationEnabled = true shows the built-in blue dot for the user's position, plus a "My Location" button. It requires ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION at runtime β enabling it without the permission throws SecurityException. Always check ContextCompat.checkSelfPermission() first. The SDK handles all FusedLocation calls internally. Use it for the passenger's position; use your own marker + FusedLocation for the driver's position that needs server upload and custom animation.
Two approaches. (1) Server-side (standard): server receives each driver position, queries a routing engine from driver's current position to destination, pushes updated ETA back via WebSocket to the passenger. (2) Client-side: detect deviation by checking if the driver is >N metres off the drawn polyline. If deviation exceeds threshold consistently for 10+ seconds, request a new route from the Directions API, redraw the polyline, and recompute ETA from the new route's duration field. Debounce rerouting β don't trigger on every minor deviation. Use server-side in production β routing is expensive and best centralised.