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_PROVIDER
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; resume is driven by the shared
MotionStateDetector(accelerometer variance, with SIG_MOTION as a fast-path for sharp wake events). 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, specific SSIDs and VPN status for sync condition filtering.
GeofenceHelper
Manages pause zones using the haversine formula for distance calculations. Reads geofences directly from SQLite on each lookup.
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 the device has been still for the configured per-zone dwell window (default 1 minute). Stillness is detected by
RawSensorMotionDetector(batched accelerometer variance + parallel SIG_MOTION); any motion above the variance threshold resets the timer. The same detector fires GPS resume when movement returns.
A per-zone stationary heartbeat can send periodic location updates while paused. It sends the geofence center as a synthetic anchor point - no GPS wake required - bypassing sync conditions. Configured via heartbeatEnabled and heartbeatIntervalMinutes per geofence.
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 |
MotionStateDetector / RawSensorMotionDetector | Single detector behind a MotionState { STATIONARY, MOVING } interface. Backed by 30s-batched accelerometer variance (hysteresis: > 0.30 m/s² for 3s -> MOVING; < 0.15 m/s² for the configured per-zone dwell -> STATIONARY) and parallel TYPE_SIGNIFICANT_MOTION as a fast-path for sharp events. Fans out to both motionless-pause and stationary-profile exit consumers via one callback site in LocationForegroundService.onMotionStateChange. |
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, 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 enqueued by AutoExportAlarmReceiver - performs the export (chunked writes, foreground service, retries, retention cleanup) and re-arms the next alarm in finally |
AutoExportAlarmReceiver | Broadcast receiver fired by AlarmManager at the configured time - hands off to AutoExportWorker because the receiver's 10s budget can't run an export |
AutoExportScheduler | Arms AlarmManager.setAndAllowWhileIdle for the next configured wall-clock time. Called on enable, after each worker run, after schedule edits and on boot |
AutoExportConfig | Typed data class wrapping auto-export settings (interval, time-of-day, weekday, day-of-month, enabledAt) 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 | Hub with stats card and navigation to Connection, Tracking & Sync, API Field Mapping, Tracking Profiles, Appearance and data/about screens |
ConnectionScreen | Server endpoint URL, offline mode toggle and connection test |
TrackingSyncScreen | GPS interval, distance filter, accuracy threshold and sync strategy preset |
AppearanceScreen | Light/dark theme, unit system, time format and custom map tile URLs (light and dark) |
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 |
GeofenceEditorScreen | Configure a zone: name, radius, record pause, WiFi pause, motionless pause and timeout, stationary heartbeat |
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, per-trip export, and per-trip delete |
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, time of day, weekday or day-of-month, 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, today's track overlay with toggle button, 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.