Architecture
Colota is a monorepo with three workspace packages:
colota/
├── apps/
│ ├── mobile/ # React Native + Kotlin Android app
│ └── docs/ # Docusaurus documentation site
└── packages/
└── shared/ # Shared colors, typography, types
Mobile App Stack
The mobile app has a React Native UI layer and native Kotlin modules for background GPS tracking.
┌─────────────────────────────────────────┐
│ React Native UI │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Screens │ │ Hooks │ │ Context │ │
│ └────┬─────┘ └────┬─────┘ └────┬────┘ │
│ └─────────────┼────────────┘ │
│ NativeLocationService │
│ (TypeScript bridge) │
├─────────────────────────────────────────┤
│ React Native Bridge │
├─────────────────────────────────────────┤
│ Native Kotlin Layer │
│ ┌──────────────────────────────────┐ │
│ │ LocationServiceModule │ │
│ │ (bridge entry point) │ │
│ └──────────────┬───────────────────┘ │
│ ┌─────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ ForegroundService DatabaseHelper ... │
│ SyncManager GeofenceHelper │
│ NetworkManager SecureStorage │
└─────────────────────────────────────────┘
Native Kotlin Modules
All native code lives in apps/mobile/android/app/src/, organized by build flavor:
src/main/java/com/colota/- Shared code:bridge/,service/,data/,sync/,util/,location/(interface)src/gms/java/com/colota/location/- Google Play Services location providersrc/foss/java/com/colota/location/- Native Android location provider
LocationServiceModule
The primary React Native bridge module (exposed as "LocationServiceModule"). Handles all JS-to-native communication for:
- Service control (
startService,stopService) - Database queries (
getStats,getTableData,getLocationsByDateRange,getDaysWithData,getDailyStats) - Geofence CRUD operations
- Settings persistence
- Device info, file operations, authentication
Emits events back to JavaScript:
onLocationUpdate- new GPS fix receivedonTrackingStopped- service stopped (user action or OOM kill)onSyncError- 3+ consecutive sync failuresonSyncProgress- batch sync progress updates with{sent, failed, total}onPauseZoneChange- entered or exited a geofence pause zoneonProfileSwitch- a tracking profile was activated or deactivatedonAutoExportComplete- auto-export finished with{success, fileName, rowCount, error}
LocationProvider Abstraction
Location services are abstracted behind a LocationProvider interface (location/LocationProvider.kt), with flavor-specific implementations:
- GMS (
src/gms/) -GmsLocationProviderwraps Google Play ServicesFusedLocationProviderClient - FOSS (
src/foss/) -NativeLocationProviderwraps Android's nativeLocationManagerwithGPS_PROVIDERandNETWORK_PROVIDERfallback
Each flavor provides a LocationProviderFactory that instantiates and returns the correct implementation at runtime. The service and bridge code in src/main/ depends only on the LocationProvider interface, never on a concrete class.
LocationForegroundService
An Android foreground service that runs continuously for GPS tracking. Manages:
- GPS location capture via the
LocationProviderabstraction - Pause zone detection (geofencing)
- Geofence entry delay - keeps recording for 3.5× the tracking interval before pausing on zone entry, logging real arrival points for backends like GeoPulse
- Anchor points - a synthetic location saved on zone exit as a clean start point for the departing trip, timestamped 1s before the first real GPS fix
- Battery critical shutdown (below 5% while discharging)
- Location accuracy filtering
- Stationary detection - pauses GPS after 60s without movement and arms
MotionDetectorto resume on motion (suspended during entry delay and inside geofence pause zones) - Queuing data for server sync
NotificationHelper
Handles all notification logic for the tracking service:
- Channel creation and notification building
- Dynamic title: "Colota Tracking" by default, "Colota · ProfileName" when a tracking profile is active
- Status text generation (coordinates, sync status, pause zones)
- Throttled updates (10s minimum interval, 2m minimum movement)
- Deduplication to avoid unnecessary notification redraws
DatabaseHelper
SQLite database singleton with five tables:
| Table | Purpose |
|---|---|
locations | All recorded GPS locations |
queue | Locations pending upload |
settings | App configuration key-value pairs |
geofences | Pause zone definitions |
tracking_profiles | Condition-based tracking profile definitions |
Uses WAL (Write-Ahead Logging) mode and prepared statements for performance.
SyncManager
Orchestrates batch location uploads with:
- Configurable batch size (50 items per batch, 10 concurrent HTTP requests)
- Exponential backoff on failure
- Periodic sync scheduling
- Manual flush support
NetworkManager
HTTP client. Validates endpoints, enforces HTTPS for public hosts, injects auth headers, caches connectivity checks, and detects unmetered connections for Wi-Fi only sync.
GeofenceHelper
Manages pause zones using the haversine formula for distance calculations. Maintains an in-memory cache of geofences that invalidates on CRUD changes.
Each geofence supports three independent GPS pause modes, configured per zone:
- Pause tracking - Stops saving and syncing locations inside the zone. GPS continues running to detect exit.
- WiFi pause - Stops GPS entirely when connected to an unmetered network (WiFi/Ethernet). Implemented via
ConnectivityManager.NetworkCallback, which fires immediately on network availability changes. An active network counter handles devices with multiple simultaneous unmetered networks - GPS only resumes once all of them are gone, after a short debounce. - Motionless pause - Stops GPS after no device motion is detected for a configurable timeout. Uses the hardware motion sensor (via
MotionDetector) to resume GPS when movement is detected again.
When both WiFi and motionless pause are enabled, GPS only resumes when both conditions clear - WiFi disconnected and motion detected. Changes made in the editor take effect immediately even when already inside the zone, via applyZoneSettingsIfChanged on the next zone recheck.
ProfileManager
Evaluates tracking profile conditions and switches GPS settings automatically. Supports five condition types: charging, Android Auto / car mode, speed above threshold, speed below threshold, and stationary. Uses a rolling speed buffer for averaged speed readings, deactivation delays (hysteresis) to prevent rapid toggling, and priority-based resolution when multiple profiles match.
ProfileHelper
Database access layer for tracking profiles and trip events. Maintains a TimedCache of enabled profiles (30s TTL) and provides CRUD operations plus trip event logging.
ConditionMonitor
Monitors charging state via BroadcastReceiver and Android Auto connection via the CarConnection API. Forwards state changes to ProfileManager for condition evaluation.
ProfileConstants
Centralized constants for condition type strings (charging, android_auto, speed_above, speed_below, stationary), event types (activated, deactivated), cache TTL, speed buffer size, and minimum interval.
SecureStorageHelper
Wraps Android's EncryptedSharedPreferences for encrypted credential storage (AES-256-GCM for values, AES-256-SIV for keys). Stores Basic Auth passwords, Bearer tokens, and custom headers.
Other Modules
| Module | Purpose |
|---|---|
LocationBootReceiver | Auto-restarts tracking after device reboot |
MotionDetector | Wraps TYPE_SIGNIFICANT_MOTION sensor - arms a one-shot hardware trigger that fires when the device starts moving, used to resume GPS after a stationary pause |
DeviceInfoHelper | Device metadata and battery status with caching |
FileOperations | File I/O, sharing via FileProvider, and clipboard access |
PayloadBuilder | Builds JSON payloads with dynamic field mapping |
ServiceConfig | Centralized configuration data class |
TimedCache | Generic TTL cache used for queue count, device info, geofences, profiles, and network state |
BuildConfigModule | Exposes build constants (SDK versions, app version) to JS |
AppLogger | Centralized logger - always active, all tags prefixed with Colota. for logcat filtering |
AutoExportWorker | WorkManager CoroutineWorker for scheduled exports - checks AutoExportConfig.isExportDue() on each run, streams chunked writes, verifies output, and cleans up old files beyond retention limit |
AutoExportScheduler | Schedules a daily (24h) check worker via WorkManager with battery-not-low constraint - frequency logic (daily/weekly/monthly) is handled at runtime by the worker |
AutoExportConfig | Typed data class wrapping auto-export settings from the SQLite settings table with validation, isExportDue(), and nextExportTimestamp() |
ExportConverters | Native Kotlin export converters (CSV, GeoJSON, GPX, KML) with in-memory, streaming, and file-based (exportToFile) interfaces |
ShortcutHandlerActivity | Handles app shortcut intents (start/stop tracking) without showing UI - reads config from DB via ServiceConfig.fromDatabase() and dispatches to LocationForegroundService |
React Native Layer
Screens
| Screen | Purpose |
|---|---|
DashboardScreen | Live map with tracking controls, coordinates, database stats, geofence and profile status |
SettingsScreen | GPS interval, distance filter, sync strategy, offline mode, accuracy threshold, unit system, time format |
ApiSettingsScreen | Endpoint URL, HTTP method, field mapping with backend templates |
AuthSettingsScreen | Authentication method (None, Basic Auth, Bearer Token) and custom HTTP headers |
GeofenceScreen | Create, edit, and delete pause zones on an interactive map |
TrackingProfilesScreen | List and manage condition-based tracking profiles |
ProfileEditorScreen | Create/edit a profile's name, condition, GPS settings, priority, and deactivation delay |
LocationInspectorScreen | Calendar day picker with activity dots, map tab with trip-colored tracks, trips tab with trip cards and export |
TripDetailScreen | Full trip view with dedicated map, stats grid, speed and elevation profile charts, and per-trip export |
LocationSummaryScreen | Aggregated stats for selectable periods (week/month/30 days) with daily breakdown and tap-to-inspect navigation |
ExportDataScreen | Export all tracked locations via native streaming converters as CSV, GeoJSON, GPX, or KML |
AutoExportScreen | Configure scheduled auto-export: directory, format, frequency, export range, and file retention |
OfflineMapsScreen | Download and manage offline map areas - interactive bounding box picker, size estimate, progress tracking, and area deletion |
DataManagementScreen | Clear sent history, delete old data, vacuum database, sync controls |
SetupImportScreen | Confirmation screen for colota://setup deep link imports |
ActivityLogScreen | In-app log viewer with level filtering, search, and export |
AboutScreen | App version, device info, links to repository and privacy policy |
Services
| Service | Purpose |
|---|---|
NativeLocationService | TypeScript bridge to the native LocationServiceModule with typed methods for all native operations |
LocationServicePermission | Sequential Android permission requests (fine location → background location → notifications → battery exemption) |
ProfileService | Thin wrapper over NativeLocationService for tracking profile CRUD and trip event queries |
SettingsService | Bridges UI state to native SQLite with type conversion (seconds↔ms, objects↔JSON) |
modalService | Centralized alert and confirm dialogs via showAlert() and showConfirm() |
Map Components
The app uses MapLibre GL Native (@maplibre/maplibre-react-native) for GPU-accelerated map rendering. The default tile server is a self-hosted instance at maps.mxd.codes serving OpenMapTiles-compatible vector tiles. A custom tile server URL can be configured in Settings - see the tile server guide. No API tokens required. Fully FOSS-compatible.
| Component | Purpose |
|---|---|
ColotaMapView | Shared base map component wrapping MapLibre's MapView with OpenFreeMap vector tiles, dark mode style transformation, custom compass, and attribution |
DashboardMap | Live tracking map with user marker, accuracy circle, geofence polygons with labels, auto-center, and center button |
TrackMap | Location history map with trip-colored track segments, tappable point markers with detail popups, fit-to-track bounds, and trip legend |
CalendarPicker | Day picker with month navigation, dot indicators for days with data, and daily distance/count display |
TripList | Segmented trip cards with distance, duration, avg speed, elevation gain/loss, and per-trip or bulk export |
GeofenceLayers | Shared geofence rendering (fill polygons, stroke outlines, labels) used by DashboardMap and GeofenceScreen |
UserLocationOverlay | User position dot with accuracy circle, used by DashboardMap and GeofenceScreen |
MapCenterButton | Reusable button overlay to re-center the map |
OfflinePackManager.ts handles the offline maps feature:
| Export | Purpose |
|---|---|
createOfflinePack | Creates a MapLibre offline pack for a bounding box at z8-14 |
loadOfflineAreas | Fetches all stored packs from MapLibre's OfflineManager and returns status info (size, complete, active) |
deleteOfflineArea | Unsubscribes, pauses, and deletes a pack; resets the tile database when the last pack is removed to reclaim OS storage |
willExceedTileLimit | Estimates whether an area would hit the 100k-tile cap before downloading |
estimateSizeLabel / estimateSizeBytes | Pre-download size estimates using per-zoom tile counting and per-tile byte averages |
loadOfflineAreaBounds / saveOfflineAreaBounds / removeOfflineAreaBounds | Persist area metadata (center, radius) to the native SQLite settings table |
Supporting utilities in mapUtils.ts:
| Utility | Purpose |
|---|---|
lerpColor | Linearly interpolates between two hex colors by factor t - used by getSpeedColor |
getSpeedColor | Returns a theme-aware color for a given speed (m/s) using green→yellow→red interpolation |
createCirclePolygon | Generates a 64-point GeoJSON Polygon approximating a circle on Earth's surface (for meter-based geofence radius) |
buildTrackSegmentsGeoJSON | Creates per-segment LineString features with pre-computed speed colors for data-driven styling |
buildTrackPointsGeoJSON | Creates Point features with speed, timestamp, accuracy, and altitude properties |
buildGeofencesGeoJSON | Creates fill polygons and label points for geofence visualization |
computeTrackBounds | Computes the bounding box for a set of track locations |
darkifyStyle | Transforms OpenFreeMap vector style JSON into a dark theme variant by overriding paint properties |
Utils
| Utility | Purpose |
|---|---|
logger | Environment-aware logging - suppresses debug/info console output in production via __DEV__, always logs warn/error to console. All levels are always captured in a ring buffer (2000 entries) for the Activity Log screen |
geo | Haversine distance, speed/distance/duration/time formatting with configurable unit system (metric/imperial) and time format (12h/24h), auto-detected from locale on first use |
exportConverters | Converts location data to CSV, GeoJSON, GPX, and KML export formats (flat and trip-aware variants) |
trips | Trip segmentation via time-gap detection (15-min threshold) with distance computation, trip stats (avg speed, elevation gain/loss), and trip color assignment |
queueStatus | Maps sync queue size to color indicators for the dashboard |
settingsValidation | URL validation and security checks for endpoint configuration |
Hooks
| Hook | Purpose |
|---|---|
useLocationTracking | Manages the foreground service lifecycle, native event subscriptions, and location state |
useTheme | Provides theme colors, mode, and toggle from ThemeProvider context |
useAutoSave | Debounced auto-save pattern for settings screens |
useTimeout | Managed timeout with automatic cleanup on unmount |
State Management
The app uses React Context for global state:
- ThemeProvider - Light/dark theme with system preference sync
- TrackingProvider - Single source of truth for tracking state, coordinates, settings, and active profile name. Hydrates from SQLite on mount, restores the active profile from the running service on reconnect, and persists changes back through
SettingsService.
Data Flow
User taps "Start" → TrackingProvider.startTracking()
→ NativeLocationService.start(config)
→ LocationServiceModule.startService(config)
→ LocationForegroundService starts
→ GPS fix received
→ DatabaseHelper.saveLocation()
→ SyncManager.queueAndSend()
→ NetworkManager.sendToEndpoint()
→ LocationServiceModule emits "onLocationUpdate"
→ NativeEventEmitter → useLocationTracking → UI updates
Shared Package
packages/shared is the single source of truth for:
- Colors -
lightColorsanddarkColorsobjects with all theme colors - Typography -
fontFamily("Inter") andfontSizesscale - Types -
ThemeColorsinterface andThemeModetype
Both the mobile app and docs site import from @colota/shared. The package compiles TypeScript to dist/ via tsc so Docusaurus can consume it without a custom webpack loader.