Skip to content

narumiruna/kestrel

Repository files navigation

🦅 Kestrel

Kestrel is an Android mock GPS app that lets you pin the system location to any point on the globe, simulate walking along a route, or generate random walking paths — all without root access.

Built with Kotlin, Jetpack Compose, Material 3, and MapLibre. Kestrel uses Android's official mock location APIs and is designed for development, testing, and personal location-simulation workflows.


✨ Features

Feature Description
📍 Single-point mock Tap the map or paste a lat,lng coordinate / Google Maps URL to lock the system location to that point.
🚶 Route playback Define waypoints, set speed (km/h), and let the engine walk the path automatically. Supports Once / Loop / PingPong playback modes.
🎲 Random route generation Enter a waypoint count and step distance; Kestrel generates a smooth random walk from the current map centre.
Favorites Save single points or full routes. Sort by Recent / Alphabetical / Manual order.
🔄 Startup behaviour Resume the last mock state, stay at the current location, or apply a saved favourite on every launch.

📋 Requirements

  • Android 10+ (API 29+)

  • Developer Options → Select mock location app → Kestrel

  • (Recommended) Settings → Location → Location Services → Google Location Accuracy → Off

    Disabling Google Location Accuracy prevents Google Play Services from overriding the mocked position with Wi-Fi / cell-tower fusion.


⚙️ How It Works

Kestrel uses the Android platform APIs LocationManager.addTestProvider() and setTestProviderLocation() — the official mock location mechanism. No root or system modification is required.

A foreground service (type location) keeps the mock alive while the UI is in the background. Movement is driven by a 1 Hz tick that advances MovementEngine along the route and pushes each sample through MockProviderManager.

The web console can remote-control an Android device only after the Android app is signed in and the user enables Options → Web remote control. First-version remote control uses the cloud command queue plus Android polling: Kestrel must be open in the foreground or already running its mock-location foreground service to receive commands. Commands expire after a bounded window if they are not delivered, and neither the web console, Google services, nor the backend can wake an app process that Android has killed.

⚠️ Note: Apps protected by Play Integrity or SafetyNet can still detect mock locations — this is a system-level behaviour that Kestrel does not attempt to bypass.


🗂️ Project Structure

app/src/main/java/dev/narumi/kestrel/
├── core/
│   ├── data/        # DataStore Preferences, @Serializable schema
│   ├── location/    # LatLng, Geo, MovementEngine, RouteGenerator,
│   │                #   LocationService, MockProviderManager, CoordParser
│   └── map/         # KestrelMap (MapLibre Compose wrapper), MapStyle
└── feature/
    ├── map/         # Main screen with map and bottom sheet
    ├── favorites/   # Saved points and routes list
    ├── options/     # Startup behaviour settings
    ├── routes/
    ├── tracks/
    └── settings/

backend/             # NestJS + Prisma cloud platform backend
web/                 # Next.js cloud console

🛠️ Development

Prerequisites: Android Studio, JDK (bundled with Android Studio), adb on PATH. All common tasks are driven by the justfile — install just to use them.

Task Command
Build debug APK just build
Build release APK just release
Build → install → launch just (or just br)
Run unit tests just test
Auto-format (Spotless + ktlint + Biome) just format
Verify formatting (no writes) just check
Detekt static analysis just lint
Regenerate Detekt baseline just lint-baseline
Install git hooks (prek) just hooks
Reset app data just reset
Follow logcat just log
Start full dev stack (Docker Compose) just cloud-up
Stop Docker Compose stack just cloud-down

🧪 Unit tests

Pure-Kotlin tests (no Android context needed) live in app/src/test/. Tests that require an Android context go in app/src/androidTest/.

just test

☁️ Cloud platform backend

The NestJS + Prisma backend lives in backend/.

cd backend
npm install
npm run db:up
npm run prisma:migrate:dev
npm run start:dev

🌐 Web console

The Next.js cloud console lives in web/ and proxies /api/backend/* to the NestJS API.

cd web
npm install
npm run dev

Open http://localhost:3301. Set KESTREL_API_BASE_URL if the API is not running on http://localhost:3300.

🐳 Docker Compose stack

To run PostgreSQL, the NestJS backend, and the Next.js web console together:

just cloud-up   # or: docker compose up --build
Service Address
PostgreSQL localhost:15432
Backend API http://localhost:3300
Web console http://localhost:3301

The Compose file uses development defaults and bind-mounts backend/ and web/ for live reload. If those ports are already taken, copy .env.example to .env and adjust the KESTREL_*_PORT variables. Use just cloud-down to stop the stack.


🤖 Continuous Integration

.github/workflows/ci.yml runs three lanes on every PR and push to main:

Lane What it runs Triggers on
android spotlessCheck, detekt, :app:assembleDebug, unit tests (non-blocking), uploads app-debug.apk artifact changes under app/, top-level Gradle files, detekt*, justfile, or .github/workflows/**
backend npm ci, prisma generate, lint, test, test:e2e, typecheck, build changes under backend/ or .github/workflows/**
web npm ci, lint, typecheck, build changes under web/ or .github/workflows/**

A leading changes job uses dorny/paths-filter to gate each lane, so a PR touching only one workspace skips the others. Push events to main always run all three.

🚀 Releases

  • just release runs :app:assembleRelease and produces app/build/outputs/apk/release/app-release-unsigned.apk.
  • .github/workflows/bump-version.yml takes a major / minor / patch choice, calls scripts/bump-version.py, and bumps the shared Android/backend/web version plus Android appVersionCode, then opens a PR using repository secret PAT_TOKEN so the resulting PR can trigger downstream CI workflows.
  • .github/workflows/tag-release.yml watches version-file merges to main, resolves the shared Android/backend/web version, and pushes the matching v* tag with PAT_TOKEN if it does not already exist.
  • .github/workflows/release.yml calls scripts/resolve-release-metadata.sh, then publishes a GitHub Release when a matching v* tag is pushed (or when manually dispatched against an existing tag) and uploads the unsigned release APK as a release asset.

🔐 Permissions

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/> <!-- map tile downloads -->

ACCESS_MOCK_LOCATION is declared in the manifest (required for the app to appear in the Developer Options mock-location picker) but has no runtime effect since Android 6.


📄 License

GNU Affero General Public License v3.0

About

Android mock GPS app — lock your device location to any coordinate, play back a route automatically, or generate a random walking path.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors