# Making Puntito
(NB: This post was written hand-in-hand with AI, but I manually checked and tweaked it thoroughly.)
## Where was I, exactly?
I've wanted to automatically track my location for a while now, as a way to supplement my GPS-stamped photos, paper notes, and various logs. I tried a bunch of apps, settled on Google Maps timeline for a bit (it just works) until I saw the density of the information they collect in the logs (shocking!), then tried [Fog of World](https://play.google.com/store/apps/details?id=com.ollix.fogofworld) at my son's suggestion (very cool, but not great at exporting), and settled on [Geo Tracker](https://play.google.com/store/apps/details?id=com.ilyabogdanovich.geotracker) for the longest time.
But I yearned for something of my own. A quiet little app that records where my phone has been, locally, in files I can actually inspect. Not a full map app, not a fitness tracker, not a social check-in thing, and definitely not another place where my data disappears into somebody else's product. Avoid cloud accounts, dashboards, ads, subscriptions, and general app nonsense.
The requirements were simple, but pretty specific:
- Wake up at regular intervals and check my current location
- Log my location each time it gets it
- When I stop for a while at a location, capture that in a separate file
- Save location data where I can easily find it, in a .csv file
There are plenty of location history apps, but most of them are either too big, too opaque, too tied to a service, or too focused on maps and routes. I mostly wanted evidence of "I was here for a while" in a format that would still make sense ten years from now.
So Puntito became the answer: a tiny app that keeps a raw location log and a separate stop log.
## From tiny dot to useful trail
The first version was simple: wake up every 15 minutes, get a location, and write it to a file. Android quickly made that less simple. Modern Android does not let background apps run forever, write wherever they want, or grab location without explicit permission.
So, Puntito now runs as a foreground location service. Android shows a persistent notification while tracking is active; that's the price of reliable background location. The app uses Google’s fused location provider, letting Android combine GPS, Wi-Fi, cell, and cached signals instead of relying on raw GPS alone. And the stop logic is deliberately conservative:
- Poll every 5 minutes
- Confirm a stop after at least 10 minutes
- Require at least 2 good fixes
- Treat fixes within 200 meters as the same stop
- Ignore fixes worse than 100 meters accuracy for stop detection
- Still write poor-accuracy fixes to the raw log
This keeps the stop file from filling up with every minor wobble from location drift. It also means a stop row is written only after Puntito sees that I have left the stop, not while I am still there.
The screen itself is intentionally minimal: black background, Puntito icon, app name, short status, and one Start/Stop button. A gear button lets me choose the log folder when tracking isn't running.
![[puntito-make.jpg|250]]
↑ That's Puntito while it's running.
The status line pulls form a list of silly little phrases, like "the road has opinions", "small roads, big dumb sky", and "dragged around by curiosity", just for a little color.
## Make yer own!
If you want to make your own location stop logger, paste this into Codex or Claude Code from an empty working directory:
```text
Create a complete native Android app named Location Log.
Goal:
Build a minimal Android 10+ location logging app. The app should run a foreground service in the background, poll location every 5 minutes, write every successful fix to a raw CSV file, and write confirmed stops to a separate stops CSV file.
Project requirements:
- Create a standard Gradle Android application project.
- Use Java or Kotlin; prefer Java if no project convention exists.
- Use a single Activity plus a foreground Service.
- Minimum SDK: 29.
- Target/compile a current Android SDK available in the environment.
- App label: Location Log.
- Package name can be com.example.locationlog unless there is an existing convention.
- Use a programmatic native Android UI. Do not use Compose.
- Use AndroidX only where useful; avoid unnecessary dependencies.
Location provider:
- Use Google Play Services fused location provider.
- Add dependency com.google.android.gms:play-services-location.
- Prefer FusedLocationProviderClient over raw LocationManager GPS-only polling.
- For the first fix, request high accuracy.
- After that, allow balanced power accuracy when appropriate.
- Use cached fused location only if it is fresh enough for the polling interval.
- Use a timeout so a location attempt cannot hang forever.
- If a fused update fails or times out, attempt getCurrentLocation as a fallback.
Permissions:
- Request location permission at runtime.
- Request notification permission on Android 13+.
- Declare foreground service location permissions required by modern Android.
- If location permission is denied, show a clear message and do not start tracking.
- If notification permission is denied on Android 13+, explain that foreground tracking needs the persistent notification.
Background behavior:
- Use a foreground service while tracking is active.
- Show a persistent notification while the service is running.
- The notification title should be "Location Log".
- The notification text should reflect tracking state, such as "Checking for stops every 5 minutes".
- The app must not require the Activity to stay open.
- Store tracking state and stop-detection state in SharedPreferences so the service can recover across app reopenings.
- When the user presses Stop, stop the foreground service and clear the tracking flag.
Logging:
- Let the user choose a log folder using Android's Storage Access Framework.
- Persist URI permission for the chosen folder.
- If no folder has been chosen, fall back to Android's Documents folder through MediaStore scoped storage.
- Do not write directly to arbitrary /sdcard paths.
- Write CSV files, not Markdown or JSON.
- File names:
- location-raw.csv
- location-stops.csv
- Use MIME type text/csv.
- Create files with a header row if they do not already exist.
- Append rows without rewriting the whole file.
Raw CSV:
- Append every successful non-null location fix to location-raw.csv.
- Header:
timestamp_utc,latitude,longitude,accuracy_m
- timestamp_utc should be ISO-8601 UTC, for example 2026-04-25T18:30:00Z.
- latitude and longitude should use decimal degrees.
- accuracy_m should be blank if Android does not provide accuracy.
Stop detection:
- Poll interval: 5 minutes.
- Minimum stop duration: 10 minutes.
- Stop radius: 200 meters.
- Minimum good fixes to confirm a stop: 2.
- Maximum accuracy for stop decisions: 100 meters.
- Write all successful fixes to location-raw.csv, even if accuracy is poor.
- Ignore poor-accuracy fixes for stop detection.
- Use candidate stop state before confirming a stop.
- A candidate stop becomes an active stop only after the minimum duration and minimum fix count are met.
- When a good fix appears outside the active stop radius, close the active stop and append it to location-stops.csv.
- Start a new candidate stop from the new fix after closing the previous stop.
- Do not write a stop row while the user is still at that stop; write it after detecting departure.
Stops CSV:
- Header:
start_local,end_local,utc_offset,duration_minutes,latitude,longitude,accuracy_m,fix_count
- start_local and end_local should use local device time, formatted yyyy-MM-dd HH:mm:ss.
- utc_offset should be the UTC offset at the stop start, for example -07:00.
- If the user changes time zones during a single stop, ignore that complication and keep the start offset.
- duration_minutes should be an integer.
- latitude and longitude should represent the stop center.
- accuracy_m should represent the best accuracy used for the stop, if available.
- fix_count should record how many good fixes contributed to the stop.
UI:
- Main screen background: black.
- Keep the Android status bar/header visible.
- Use black status/navigation bars with light icons.
- Show a vertically centered cluster containing:
- the Location Log icon with rounded corners,
- title text "Location Log",
- status text.
- When not tracking, status text should be "Ready to track".
- When tracking, status text should be:
Location Log is running
"random message"
- Pick the random message once each time the Activity launches.
- Use one Start/Stop button near the lower part of the screen.
- When not tracking, the button says "Start" and is yellow.
- When tracking, the button says "Stop".
- Show a gear icon above the Start button only when not tracking.
- The gear opens the folder picker.
- The icon, title, status, and button y-positions should not jump when tracking state changes.
Random running messages:
- Maps, miles, minor mistakes
- The road has opinions
- A pocketful of coordinates
- Small roads, big dumb sky
- Dragged around by curiosity
- Not lost, just poorly labeled
- One more weird little place
- Your dot is showing
- Elsewhere, with snacks
- The map knows what you did
- A trail of questionable decisions
- From here to what now
- Off to bother another town
- The long way, obviously
- Road trip, no adult supervision
- Pinned, plotted, probably wrong
- The coordinates of bad ideas
- Everywhere is suspiciously far
- Just enough map to be dangerous
- Went outside; became data
- Go forth and overshoot
- Big map energy
- Places I have inconvenienced
- The map is judging us
- Wander first, explain later
- Record of a minor expedition
- Useful notes from nowhere special
- Latitude, longitude, questionable judgment
- A pocket guide to being elsewhere
- Mostly accurate since departure
- Coordinates observed, snacks consumed
- Evidence of having gone
- For persons currently in motion
- The official log of getting there
- A small report from away
- Routes, ruts, and roadside facts
- Notes from the passenger seat
- Mileage may vary, obviously
- A practical guide to wandering off
- Stamped, folded, slightly wrong
- The collected errors of movement
- Field notes from a moving dot
- Sensible records of bad directions
- Whereabouts, more or less
- An index of detours
- Cartographic mischief, neatly kept
- For use between places
- Fine observations from dumb roads
- The road was right there
Icons/assets:
- If the working directory contains an app icon image, use it as the launcher icon source.
- Generate launcher PNGs:
- mipmap-mdpi/ic_launcher.png: 48x48
- mipmap-hdpi/ic_launcher.png: 72x72
- mipmap-xhdpi/ic_launcher.png: 96x96
- mipmap-xxhdpi/ic_launcher.png: 144x144
- mipmap-xxxhdpi/ic_launcher.png: 192x192
- Also use the icon in-app near the top of the main cluster.
- If no icon is present, create a simple placeholder asset so the app still builds.
Implementation notes:
- Prefer programmatic views: FrameLayout root, vertical LinearLayout cluster, ImageView icon, TextView title, TextView status, ImageButton gear, Button Start/Stop.
- Use Material/Android standard button shapes or drawable shapes with rounded corners.
- Keep layout dimensions stable so text and controls do not shift between tracking and idle states.
- Use SharedPreferences for:
- tracking flag,
- selected log folder URI,
- candidate stop state,
- active stop state,
- last diagnostics/error state if shown during development.
- During development, make diagnostic errors visible on screen so permission, provider, and write failures are debuggable.
- It is acceptable to remove diagnostics from the final minimal UI after the app is working.
Build verification:
- Build the debug APK.
- If Gradle is available, run ./gradlew assembleDebug.
- On macOS with Android Studio installed, use:
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" ./gradlew assembleDebug
- Report the APK path, typically app/build/outputs/apk/debug/app-debug.apk.
Common pitfalls to avoid:
- Do not assume GPS-only location will get a fix indoors; use fused location.
- Do not expect a background location app to run reliably without a foreground service notification.
- Do not require the app screen to stay open.
- Do not write directly to random external storage paths on Android 10+.
- Do not use DCIM as a generic file destination through MediaStore.Files; Android may reject it for non-media files.
- Do not forget to persist Storage Access Framework URI permissions.
- Do not treat one noisy fix as proof that the user left a stop.
- Do not let poor-accuracy fixes create false stops.
- Do not write the active stop row until departure is detected.
- If the installed launcher icon/name looks stale on a phone, instruct the user to uninstall and reinstall because launchers cache app metadata.
```