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), backup/, export/
  • src/gms/java/com/colota/location/ - Google Play Services location provider
  • src/foss/java/com/colota/location/ - Native Android location provider

Bridge Modules

Three React Native bridge modules are registered by LocationServicePackage: LocationServiceModule (the primary tracking-side bridge), BackupServiceModule (the encrypted-backup bridge) and ImportServiceModule (the external-file-import bridge). The two file-touching modules (Backup and Import) share SafPickerCoordinator (util/SafPickerCoordinator.kt) for the SAF ACTION_CREATE_DOCUMENT / ACTION_OPEN_DOCUMENT plumbing - each module holds its own instance with its own request codes so multiple coordinators coexist without interfering.

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}

BackupServiceModule

Second React Native bridge module (exposed as "BackupServiceModule"). Owns the encrypted backup pipeline end-to-end so the JS layer never touches credentials, the SQLite file, or the Argon2 key directly. JS-callable methods:

  • pickBackupDestination / pickBackupSource - launches the Storage Access Framework picker (ACTION_CREATE_DOCUMENT / ACTION_OPEN_DOCUMENT); a single in-flight picker is enforced via a 5-minute timeout.
  • createBackup(uri, password) - validates password strength, claims an operation mutex, runs BackupBuilder against a cacheDir-staged file, then atomically copies into the SAF destination. Stops promotion to a foreground service when finished.
  • restoreBackup(uri, password) - cancels the location service and any auto-export work, polls until both stop, runs BackupRestorer, then forces tracking_enabled=false so the destination device doesn't auto-resume.
  • applyRestore - JS calls this after the success dialog is dismissed; it triggers reactHost.reload() so all modules re-read state from the restored DB.

Both createBackup and restoreBackup await BackupOrphanCleanup.awaitComplete() before claiming the operation mutex, ensuring the launch-time orphan sweeper can't race active operations.

Errors are surfaced to JS as E_BACKUP_<ERROR_NAME> codes that map onto the BackupError enum (WRONG_PASSWORD, BAD_MAGIC, UNSUPPORTED_VERSION, UNSUPPORTED_KDF, UNSUPPORTED_SCHEMA, MISSING_ENTRY, INTEGRITY_FAIL, TRUNCATED, TAMPERED, SECRETS_PARTIAL).

Backup Pipeline

Native-only modules in backup/ package. The on-disk format is documented in BackupFormat.kt.

ModulePurpose
BackupCryptoChunked AES-256-GCM encrypt/decrypt keyed by Argon2id. Each chunk binds the file header into its GCM AAD so any header tamper invalidates the first tag. Argon2 is deferred until the first ciphertext chunk arrives so wrong-password rejection costs no key derivation.
BackupFormatOn-disk layout constants, BackupHeader data class, BackupError enum, and BackupException. 76-byte header (magic, format version, KDF id, KDF params, 32-byte salt, 8-byte nonce prefix, chunk size, reserved) followed by length-prefixed ciphertext chunks and an end-marker + chunk-count footer.
BackupBuilderSnapshots the SQLite database via DatabaseHelper.snapshotTo (uses VACUUM INTO on API 30+, file-copy with WAL checkpoint as fallback), runs quick_check, extracts secrets via SecureStorageHelper.exportPlaintextForBackup(), deflates everything into a zip, then streams it through BackupCrypto.encrypt. Picks Argon2 memory by ActivityManager.isLowRamDevice (32 MiB / 64 MiB).
BackupRestorerDecrypts the file via a PipedInputStream into a ZipInputStream, extracts entries to a temp dir with bounded reads (zip-bomb defense), validates the manifest schema version, runs PRAGMA integrity_check on the candidate DB, calls DatabaseHelper.migrateCandidate to run any required migrations on the candidate, then atomically swaps the live DB via DatabaseHelper.replaceLiveDatabase, then re-imports secrets. A failed secrets commit is surfaced as SECRETS_PARTIAL so the UI can prompt the user to re-enter credentials.
BackupForegroundServiceNotification-only foreground service shown during long backups/restores. Uses FOREGROUND_SERVICE_TYPE_DATA_SYNC. Holds no work itself - the actual encryption stays in BackupServiceModule's coroutine so the password CharArray lives only on the heap, not in service state.
BackupOrphanCleanupSingleton kicked off from MainApplication.onCreate on a daemon thread. Sweeps cacheDir/backup_temp, cacheDir/restore_temp, cacheDir/pending_backup.colota, and <dbDir>/Colota.db.incoming left behind by a process death mid-operation. Exposes a CompletableDeferred so BackupServiceModule can await completion before claiming the operation mutex.
PasswordStrengthMirror of the JS-side passwordStrength.ts. Enforces the 12-character floor and ~50-bit entropy floor; sequential runs and <4 distinct chars cap the score.

ImportServiceModule

Third React Native bridge module (exposed as "ImportServiceModule"). Owns the location-import pipeline end-to-end: file picker, streaming parse, dedup against the live DB, and the two-stage commit. JS-callable methods:

  • pickImportSource - launches the SAF ACTION_OPEN_DOCUMENT picker via the shared SafPickerCoordinator. MIME hints cover JSON, XML, CSV and octet-stream; the bare type = "*/*" keeps .geojson and .gpx from being filtered out by strict providers.
  • importLocationsFromFile(uri) - acquires the operation mutex, opens the file once, sniffs 4 KB for format detection, then prepends those bytes back via SequenceInputStream so the format-specific parser sees the full document without a second SAF open. Stashes the surviving rows on the module instance for the commit step.
  • commitImport(asQueued) - applies the staged rows in a single transaction. When asQueued=true it also reads the live ServiceConfig, rebuilds payloads via PayloadBuilder (the same one the live tracking path uses), and inserts paired queue rows. Rejects with E_IMPORT_SYNC_UNAVAILABLE if no endpoint or offline mode is on, so a stale UI state can't bypass the gate.
  • cancelImport - sets the cancellation flag and clears the stash.

The staged rows are guarded by a 15-minute TTL job - a preview-then-walk-away doesn't pin memory indefinitely. Cancellation flows through an AtomicBoolean that the parsers and dedup walker check at array boundaries.

Errors surface to JS as E_IMPORT_* codes: E_IMPORT_UNSUPPORTED, E_IMPORT_CANCELLED, E_IMPORT_NO_PENDING, E_IMPORT_SYNC_UNAVAILABLE, E_IMPORT_FAILED, plus the shared E_BUSY when another operation holds the mutex.

Import Pipeline

Native-only modules in the importer/ package. Format-specific parsers all return the same ParseResult(rows, invalid); the dispatch logic in LocationImporter picks one based on a 4 KB content sniff.

ModulePurpose
LocationImporterOrchestrator. Detects format from the sniff (XML root for GPX/KML, JSON top-level keys for GeoJSON / Google Timeline, CSV header for CSV), dispatches to the per-format parser, runs streaming merge-walk dedup against an ASC-ordered DB cursor (no existing-key HashSet is materialised - memory is O(1) on top of the parsed-rows list, so the dedup scales to multi-million-row user histories without OOM). On commit, the recovery path writes sent = 1 and skips the queue; the migration path (asQueued=true) builds payloads via the live PayloadBuilder against a synthetic android.location.Location so the import path can't drift from the live tracking path.
GeoJsonParserStreaming android.util.JsonReader-based parser for FeatureCollection of Point features. Tolerates foreign shapes - any feature without a recognised time / non-Point geometry is counted as invalid and dropped, but parsing continues.
GoogleTimelineParserHandles both Google Timeline schemas in a single pass: legacy Takeout (locations[].latitudeE7/longitudeE7/timestampMs) and the new on-device export (semanticSegments[].timelinePath[] + rawSignals[].position with degree-suffix coord strings). Visit/activity inferences are intentionally skipped.
GpxParserXmlPullParser-based. Collects <wpt> + <rtept> + <trkpt> uniformly into the flat locations table. Recognises Garmin's nested TrackPointExtension wrapper so sport-watch metadata (speed, course) lands on the row.
KmlParserXmlPullParser-based. Reads Placemark/Point/coordinates (KML's lon,lat[,alt] order, flipped back internally) with TimeStamp/when. LineString-only Placemarks are dropped + counted as invalid since the KML schema doesn't carry per-vertex timestamps.
CsvParserHeader-driven; maps columns by name with aliases (lat/latitude, lon/lng/longitude, time/timestamp/iso_time, etc.) so foreign column orders work without renaming. Rejects headers missing the required lat+lon+time columns with UnsupportedFormatException.
JsonReadHelpersShared JsonReader extensions (readNullableInt, readNullableDouble) plus parseIso8601Seconds backed by pre-built immutable DateTimeFormatter instances (thread-safe and ~10× faster than SimpleDateFormat, which matters for Timeline files with 100k+ timestamps).
ImportFormat / ImportRow / UnsupportedFormatExceptionShared data types. ImportRow is the normalised location shape produced by every parser before dedup; format-specific quirks (E7 coords, ISO timestamps, degree-suffix strings) are resolved before construction so the orchestrator never sees them.

The on-disk format is never authoritative for sync decisions: imported rows go in with sent = 1 by default (no re-upload), and only the explicit "Import + Queue for Sync" button flips that to sent = 0 + queue rows. See Data Import for the user-facing semantics.

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

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 unplugged), detected via a battery-broadcast receiver so it fires even while GPS is paused in a zone
  • 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:

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.

Three additional methods support the backup pipeline:

  • snapshotTo(destFile) - produces a transactional copy of the live DB. Uses VACUUM INTO on API 30+; falls back to a file copy with a wal_checkpoint(FULL) on API 26-29.
  • migrateCandidate(file) - runs schema migrations on a candidate file in DELETE journal mode before it is swapped in, so a migration failure leaves the live DB untouched.
  • replaceLiveDatabase(context, newDb) - drains the live WAL via wal_checkpoint(TRUNCATE), deletes the live -wal/-shm/-journal sidecars, then atomically moves the candidate into place. Handles AtomicMoveNotSupportedException by staging through an <dbName>.incoming file. Synchronizes with getInstance() so concurrent callers either see the old singleton or the new file.

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. Injects auth headers, caches connectivity checks, and detects unmetered connections, specific SSIDs and VPN status for sync condition filtering. Endpoint policy (HTTPS-for-public, private host detection) is delegated to UrlSafety.

For mTLS-protected endpoints, builds the HttpsURLConnection with a custom SSLSocketFactory supplied by ClientCertSslContextProvider (per-instance, never setDefaultSSLSocketFactory() - the override is scoped to outbound location sync, not the whole process).

UrlSafety

HTTP endpoint policy: HTTPS is required for public hosts, HTTP is only allowed when the host resolves to a private/local address (loopback, RFC 1918 site-local, link-local, or CGNAT 100.64.0.0/10). Resolves hostnames via InetAddress so server.local-style mDNS names work, with a per-hostname cache. Pure validation with no transport state, also exposed to the JS bridge for pre-save endpoint checks.

ClientCertSslContextProvider

Builds the cached SSLSocketFactory used for mTLS. The key managers are supplied by DynamicKeyManager so the cert source can be swapped without rebuilding the SSL context; trust managers come from a CompositeX509TrustManager that layers an optional user-imported CA on top of Android's system roots. Includes a lazy migration: legacy PKCS12 + password material in EncryptedSharedPreferences (if any) is unwrapped into Android Keystore the first time the provider builds a factory or the Settings UI calls runMigrationIfNeeded. Legacy storage is wiped only on permanent failures (bad blob, wrong password, PKCS12-shape errors); transient failures (AndroidKeyStore unavailable, OOM) leave legacy data so the next request can retry.

DynamicKeyManager

X509KeyManager that resolves the active client cert at TLS handshake time from one of two sources: an alias stored in Android's system KeyChain (private key stays in the OS / hardware, app never sees the bytes) or a .p12-imported entry in Android Keystore (key sealed under the app's UID, hardware-backed where available). The resolved alias is cached and invalidated alongside the SSLSocketFactory cache, so cert swaps take effect on the next request without an app restart.

CompositeX509TrustManager

X509TrustManager that accepts a server chain if any delegate accepts it. Used to layer a user-imported private CA on top of the system trust store without losing system CA validation - additive trust, not pinning.

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, custom headers, and the user-imported server CA (public cert, no key material). Client certificate private keys live in Android Keystore instead, not here - see ClientCertSslContextProvider.

For backups, two internal methods support the export/import flow without exposing plaintext secrets to other modules:

  • exportPlaintextForBackup() - returns the BACKED_UP_KEYS as a Map<String, String> for inclusion in the encrypted backup container.
  • importPlaintextFromBackup(secrets) - clears all BACKED_UP_KEYS then writes the new map in a single sync commit(). Throws on commit failure so the restore path can report SECRETS_PARTIAL.

Other Modules

ModulePurpose
LocationBootReceiverAuto-restarts tracking after device reboot
MotionStateDetector / RawSensorMotionDetectorSingle 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.
DeviceInfoHelperDevice metadata and battery status with caching
FileOperationsFile I/O, sharing via FileProvider, and clipboard access
PayloadBuilderBuilds outgoing JSON payloads (field-mapped, Overland batch envelope, Traccar JSON) and extracts envelope custom fields
ServiceConfigCentralized configuration data class
TimedCacheGeneric TTL cache used for queue count, device info, 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 enqueued by AutoExportAlarmReceiver - performs the export (chunked writes, foreground service, retries, retention cleanup) and re-arms the next alarm in finally
AutoExportAlarmReceiverBroadcast receiver fired by AlarmManager at the configured time - hands off to AutoExportWorker because the receiver's 10s budget can't run an export
AutoExportSchedulerArms AlarmManager.setAndAllowWhileIdle for the next configured wall-clock time. Called on enable, after each worker run, after schedule edits and on boot
AutoExportConfigTyped 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()
ExportConvertersNative CSV/GeoJSON/GPX/KML serialization for both "Export all" (flat, streamed via exportToFile) and per-trip / multi-select export (trip-segmented via convertTrips, reached through the exportTripsToFile bridge). In-memory, streaming, and file-based interfaces
ShortcutHandlerActivityTransparent activity handling app shortcut intents (start/stop tracking) without UI. Delegates to the shared TrackingControl helper
TrackingControlReceiverExported broadcast receiver for automation apps - com.Colota.action.START_TRACKING / STOP_TRACKING start or stop tracking from saved settings. Delegates to TrackingControl
TrackingControlShared start/stop tracking actions used by both triggers above: reads config from DB via ServiceConfig.fromDatabase(), starts the foreground service and fires the started event; stop is routed through the service so stopForegroundServiceWithReason runs

React Native Layer

Screens

ScreenPurpose
DashboardScreenLive map with tracking controls, coordinates, database stats, geofence and profile status
SettingsScreenHub with stats card and navigation to Connection, Tracking & Sync, API Field Mapping, Tracking Profiles, Appearance and data/about screens
ConnectionScreenServer endpoint URL, offline mode toggle and connection test
TrackingSyncScreenGPS interval, distance filter, accuracy threshold and sync strategy preset
AppearanceScreenLight/dark theme, unit system, time format and custom map tile URLs (light and dark)
ApiSettingsScreenEndpoint URL, HTTP method, field mapping with backend templates
AuthSettingsScreenAuthentication method (None, Basic Auth, Bearer Token) and custom HTTP headers, with a link row to mTLS Settings
MtlsSettingsScreenClient certificate (PKCS12 import + Android Keystore storage) and Trusted Server CA management
GeofenceScreenCreate, edit, and delete pause zones on an interactive map
GeofenceEditorScreenConfigure a zone: name, radius, record pause, WiFi pause, motionless pause and timeout, stationary heartbeat
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, per-trip and multi-select export and multi-select delete
TripDetailScreenFull trip view with dedicated map, stats grid, speed and elevation profile charts, per-trip export, and per-trip delete
LocationSummaryScreenAggregated stats for selectable periods (week/month/30 days) with daily breakdown and tap-to-inspect navigation
ExportLocationsScreenExport all tracked locations via native streaming converters as CSV, GeoJSON, GPX, or KML
ImportLocationsScreenImport external location files (GeoJSON, Google Timeline legacy + new, GPX, KML, CSV) with auto format detection, dedup preview, and recovery vs migration (queue-for-sync) commit choice
AutoExportScreenConfigure scheduled auto-export: directory, format, frequency, time of day, weekday or day-of-month, 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
BackupRestoreScreenCreate or restore a password-encrypted .colota archive of all data, with strength meter and no-recovery confirmation
SetupImportScreenConfirmation screen for colota://setup deep link imports
ShareSetupScreenBundles selected config categories into a colota://setup link to share; credentials opt-in
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)
BackupServiceThin TypeScript wrapper over the native BackupServiceModule exposing pickBackupDestination, pickBackupSource, createBackup, restoreBackup, applyRestore. Surfaces typed BackupErrorCode strings for screen-side messaging
ImportServiceThin TypeScript wrapper over the native ImportServiceModule exposing pickImportSource, importLocationsFromFile, commitImport(asQueued), cancelImport. Returns a typed ImportPreview with format, counts, date range, and canQueueForSync so the UI can gate the queue-for-sync button
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, today's track overlay with toggle button, 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. Per-trip share icon plus a long-press contextual action bar for multi-select export and delete
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 Logging 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
exportConvertersExport-format metadata (labels, icons, extensions, MIME types) for the export UI. Serialization itself is native - see ExportConverters.kt
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.