Skip to main content

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 provider
  • src/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 received
  • onTrackingStopped - service stopped (user action or OOM kill)
  • onSyncError - 3+ consecutive sync failures
  • onSyncProgress - batch sync progress updates with {sent, failed, total}
  • onPauseZoneChange - entered or exited a geofence pause zone
  • onProfileSwitch - a tracking profile was activated or deactivated
  • onAutoExportComplete - 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/) - GmsLocationProvider wraps Google Play Services FusedLocationProviderClient
  • FOSS (src/foss/) - NativeLocationProvider wraps Android's native LocationManager with GPS_PROVIDER and NETWORK_PROVIDER fallback

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 LocationProvider abstraction
  • 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 MotionDetector to 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:

TablePurpose
locationsAll recorded GPS locations
queueLocations pending upload
settingsApp configuration key-value pairs
geofencesPause zone definitions
tracking_profilesCondition-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

ModulePurpose
LocationBootReceiverAuto-restarts tracking after device reboot
MotionDetectorWraps 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
DeviceInfoHelperDevice metadata and battery status with caching
FileOperationsFile I/O, sharing via FileProvider, and clipboard access
PayloadBuilderBuilds JSON payloads with dynamic field mapping
ServiceConfigCentralized configuration data class
TimedCacheGeneric TTL cache used for queue count, device info, geofences, profiles, and network state
BuildConfigModuleExposes build constants (SDK versions, app version) to JS
AppLoggerCentralized logger - always active, all tags prefixed with Colota. for logcat filtering
AutoExportWorkerWorkManager CoroutineWorker for scheduled exports - checks AutoExportConfig.isExportDue() on each run, streams chunked writes, verifies output, and cleans up old files beyond retention limit
AutoExportSchedulerSchedules a daily (24h) check worker via WorkManager with battery-not-low constraint - frequency logic (daily/weekly/monthly) is handled at runtime by the worker
AutoExportConfigTyped data class wrapping auto-export settings from the SQLite settings table with validation, isExportDue(), and nextExportTimestamp()
ExportConvertersNative Kotlin export converters (CSV, GeoJSON, GPX, KML) with in-memory, streaming, and file-based (exportToFile) interfaces
ShortcutHandlerActivityHandles app shortcut intents (start/stop tracking) without showing UI - reads config from DB via ServiceConfig.fromDatabase() and dispatches to LocationForegroundService

React Native Layer

Screens

ScreenPurpose
DashboardScreenLive map with tracking controls, coordinates, database stats, geofence and profile status
SettingsScreenGPS interval, distance filter, sync strategy, offline mode, accuracy threshold, unit system, time format
ApiSettingsScreenEndpoint URL, HTTP method, field mapping with backend templates
AuthSettingsScreenAuthentication method (None, Basic Auth, Bearer Token) and custom HTTP headers
GeofenceScreenCreate, edit, and delete pause zones on an interactive map
TrackingProfilesScreenList and manage condition-based tracking profiles
ProfileEditorScreenCreate/edit a profile's name, condition, GPS settings, priority, and deactivation delay
LocationInspectorScreenCalendar day picker with activity dots, map tab with trip-colored tracks, trips tab with trip cards and export
TripDetailScreenFull trip view with dedicated map, stats grid, speed and elevation profile charts, and per-trip export
LocationSummaryScreenAggregated stats for selectable periods (week/month/30 days) with daily breakdown and tap-to-inspect navigation
ExportDataScreenExport all tracked locations via native streaming converters as CSV, GeoJSON, GPX, or KML
AutoExportScreenConfigure scheduled auto-export: directory, format, frequency, export range, and file retention
OfflineMapsScreenDownload and manage offline map areas - interactive bounding box picker, size estimate, progress tracking, and area deletion
DataManagementScreenClear sent history, delete old data, vacuum database, sync controls
SetupImportScreenConfirmation screen for colota://setup deep link imports
ActivityLogScreenIn-app log viewer with level filtering, search, and export
AboutScreenApp version, device info, links to repository and privacy policy

Services

ServicePurpose
NativeLocationServiceTypeScript bridge to the native LocationServiceModule with typed methods for all native operations
LocationServicePermissionSequential Android permission requests (fine location → background location → notifications → battery exemption)
ProfileServiceThin wrapper over NativeLocationService for tracking profile CRUD and trip event queries
SettingsServiceBridges UI state to native SQLite with type conversion (seconds↔ms, objects↔JSON)
modalServiceCentralized 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.

ComponentPurpose
ColotaMapViewShared base map component wrapping MapLibre's MapView with OpenFreeMap vector tiles, dark mode style transformation, custom compass, and attribution
DashboardMapLive tracking map with user marker, accuracy circle, geofence polygons with labels, auto-center, and center button
TrackMapLocation history map with trip-colored track segments, tappable point markers with detail popups, fit-to-track bounds, and trip legend
CalendarPickerDay picker with month navigation, dot indicators for days with data, and daily distance/count display
TripListSegmented trip cards with distance, duration, avg speed, elevation gain/loss, and per-trip or bulk export
GeofenceLayersShared geofence rendering (fill polygons, stroke outlines, labels) used by DashboardMap and GeofenceScreen
UserLocationOverlayUser position dot with accuracy circle, used by DashboardMap and GeofenceScreen
MapCenterButtonReusable button overlay to re-center the map

OfflinePackManager.ts handles the offline maps feature:

ExportPurpose
createOfflinePackCreates a MapLibre offline pack for a bounding box at z8-14
loadOfflineAreasFetches all stored packs from MapLibre's OfflineManager and returns status info (size, complete, active)
deleteOfflineAreaUnsubscribes, pauses, and deletes a pack; resets the tile database when the last pack is removed to reclaim OS storage
willExceedTileLimitEstimates whether an area would hit the 100k-tile cap before downloading
estimateSizeLabel / estimateSizeBytesPre-download size estimates using per-zoom tile counting and per-tile byte averages
loadOfflineAreaBounds / saveOfflineAreaBounds / removeOfflineAreaBoundsPersist area metadata (center, radius) to the native SQLite settings table

Supporting utilities in mapUtils.ts:

UtilityPurpose
lerpColorLinearly interpolates between two hex colors by factor t - used by getSpeedColor
getSpeedColorReturns a theme-aware color for a given speed (m/s) using green→yellow→red interpolation
createCirclePolygonGenerates a 64-point GeoJSON Polygon approximating a circle on Earth's surface (for meter-based geofence radius)
buildTrackSegmentsGeoJSONCreates per-segment LineString features with pre-computed speed colors for data-driven styling
buildTrackPointsGeoJSONCreates Point features with speed, timestamp, accuracy, and altitude properties
buildGeofencesGeoJSONCreates fill polygons and label points for geofence visualization
computeTrackBoundsComputes the bounding box for a set of track locations
darkifyStyleTransforms OpenFreeMap vector style JSON into a dark theme variant by overriding paint properties

Utils

UtilityPurpose
loggerEnvironment-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
geoHaversine distance, speed/distance/duration/time formatting with configurable unit system (metric/imperial) and time format (12h/24h), auto-detected from locale on first use
exportConvertersConverts location data to CSV, GeoJSON, GPX, and KML export formats (flat and trip-aware variants)
tripsTrip segmentation via time-gap detection (15-min threshold) with distance computation, trip stats (avg speed, elevation gain/loss), and trip color assignment
queueStatusMaps sync queue size to color indicators for the dashboard
settingsValidationURL validation and security checks for endpoint configuration

Hooks

HookPurpose
useLocationTrackingManages the foreground service lifecycle, native event subscriptions, and location state
useThemeProvides theme colors, mode, and toggle from ThemeProvider context
useAutoSaveDebounced auto-save pattern for settings screens
useTimeoutManaged 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 - lightColors and darkColors objects with all theme colors
  • Typography - fontFamily ("Inter") and fontSizes scale
  • Types - ThemeColors interface and ThemeMode type

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.