Back to Blog

Building PathTrace: A native battery-friendly location tracker

Emmanouil Athanasopoulos4 min read878 words
React NativeExpoKotlinNative ModulesLocation

The Motivation Behind the Project

PathTrace was born out of a desire for a privacy-respecting alternative to Google Maps Timeline. I wanted an app that could track my daily movements and plot them beautifully on a map without sending my location data to a remote server. The goal was simple: build a 100% local timeline app that runs efficiently in the background. However, continuous background location tracking on Android is notoriously battery-intensive. The primary engineering challenge of PathTrace was finding a way to reliably track a user's location 24/7 without their phone battery dying before lunch.

Core Features and Design Goals

  • Zero-Battery Sleeping: The app utilizes Android's low-power `ActivityRecognition` API via a custom native module. When the user is sitting still, the GPS is completely powered down.
  • Instant Wake Foreground Service: The exact moment the user starts moving (walking, biking, driving), the native module wakes a robust Foreground Service to begin tracking GPS coordinates.
  • Direct SQLite Writes: To prevent the app from lagging over time with thousands of GPS points, the native Kotlin service bypasses the JavaScript bridge entirely and writes points directly into the `pathtrace.db` database.
  • Retroactive Visit Detection: Time gaps between GPS points are mathematically analyzed on app launch to retroactively detect 'Visits' (places stayed at for >5 mins) and generate the route segments leading up to them.
  • MapLibre GL Interactive Maps: The UI utilizes a custom MapLibre WebView bridge for 60fps rendering of thousands of route polylines and marker points.
  • Google Takeout Import: Seamlessly parse and import historical timeline data directly from Google Takeout JSON files directly into the local SQLite database.

Deep Dive: How It Works Under the Hood

The most significant optimization in PathTrace was migrating away from the standard `expo-location` module. While `expo-location` is great for simple use cases, running it 24/7 in the background is extremely demanding. It relies on continuous polling and frequent wake-locks, which triggers Android's Doze mode and ultimately gets the app killed. I decided to rewrite the entire tracking layer as a custom Android Native Module in Kotlin. Instead of constantly pinging the GPS, the module registers an `ActivityTransitionRequest` with Google Play Services. This relies on the device's low-power accelerometer and gyroscope to detect when the user's state changes from `STILL` to `WALKING` or `IN_VEHICLE`. When movement is detected, the OS triggers a BroadcastReceiver, which in turn launches a Foreground Service. This Foreground Service then activates the GPS and begins logging coordinates. As soon as the user becomes `STILL` again, the GPS shuts down. This architecture reduced background battery consumption from ~15% per day to less than 2% per day.

Technical Implementation

  • Built a custom Android Native Module in Kotlin (`pathtrace-tracker`) to hook directly into the `ActivityRecognition` API for state changes.
  • Implemented a bulletproof Foreground Service to ensure tracking continues reliably during long commutes without being killed by Android Doze.
  • Bypassed the React Native bridge for data persistence by opening the Expo SQLite database directly from Kotlin and performing direct SQLite `INSERT` queries from the background service.
  • Developed a JavaScript Visit Detection Algorithm that runs on app launch, examining the timeline of raw GPS points to identify time gaps > 5 minutes, resolving them into semantic 'Visits' and reverse-geocoding the addresses.
  • Designed a MapLibre GL WebView bridge to handle the massive rendering load of drawing daily polylines and markers smoothly without UI thread blockage.

Architecture Overview

PathTrace separates tracking from processing. The native layer is entirely responsible for collecting data: detecting movement, capturing coordinates, and writing them blindly into the `location_points` SQLite table. The React Native (JavaScript) layer acts purely as a consumer. When the user opens the app, the JS layer reads the raw points, runs the retroactive visit detection algorithm to build the semantic timeline, and orchestrates the UI.

Challenges I Faced Along the Way

A major challenge was dealing with the raw data volume. A single day of tracking can generate thousands of location points. Pushing this much data across the React Native bridge from a background JS task caused significant memory pressure and occasional crashes. The breakthrough was discovering that I could access the same SQLite file used by `expo-sqlite` directly from my Kotlin native module. By writing the GPS points directly to `pathtrace.db` at the native level, the app could run indefinitely in the background without waking the JavaScript engine at all.

The Technology Stack

Built with Expo SDK 57, React Native, and TypeScript. The native tracking engine is written in Kotlin using the Expo Modules API. Data is stored locally using `expo-sqlite`. The UI is powered by NativeWind and Expo Router, with MapLibre GL JS providing the mapping engine via `react-native-webview`.

What I Would Do Differently Next Time

Working heavily with Android's battery optimization features taught me that the OS is extremely aggressive about killing apps. You cannot just request background location and assume it will run. You have to design the app to be explicitly battery-friendly so the OS allows it to live. Hooking into `ActivityRecognition` was a game-changer.

Final Reflections

Building PathTrace proved that it is entirely possible to create powerful, continuous-tracking applications in React Native. You just have to know when to drop down into the native layer. Offloading the heavy lifting to Kotlin while keeping the beautiful UI in React Native provided the best of both worlds.