Frank

2026-10-15

privacy · android

I don't want a GPS diary

I wanted to know if I showed up. That is a smaller question than a phone usually answers.

A phone will happily keep the line. Office to the shop, shop to the car, car to the door, and then a year of that line in a retention policy I did not read because a maps SDK shipped with "helpful" turned on. I do not want the line. I want a ledger. Presence Ledger is that ledger. Places I define. Arrived and Left. Sparse samples underneath, so I can ask why it said what it said. A route is a different product, and I am not building it.

A place is a circle I drew

A place has a name, a center, and a radius. The editor opens on 200 meters. I can drag it. I can type a Wi-Fi name if that network is one I am willing to treat as evidence. The name is one I typed. The app does not walk the block collecting everyone else's names and file the serial numbers off. A scan of the neighborhood is a diary. A name I entered is a label on a place I already decided to care about.

The circle is not a guess about my life. It is the area I said counts. Home can be tight. A campus can be loose. If I draw it badly, the day will be wrong, and that is my drawing, not the phone inventing a workplace out of a cell tower.

Places have revisions, and a revision carries the UTC time it became true. I can add a place later and replay an old day against it. The old files stay. I do not rewrite Tuesday because Wednesday I finally named the building.

Inside, outside, and the samples that decide neither

Android hands the app a reading. The shared coordinator turns that into a PositionObservation or a WifiObservation, appends it, and passes the same reading to the core. The core is the part I trust. It does not know about activities, services, or map tiles. It knows a place, a previous state, and one observation.

A position is classified against the circle with the accuracy attached. Definitely inside counts. Probably inside counts. Definitely outside counts as absence. Anything less decisive does nothing. A fat accuracy circle that covers both the room and the street is not allowed to flip the day. I would rather have a hole than a confident lie. I will believe the lie next week, when I no longer remember the street.

The standard policy wants three minutes. Three minutes of corroboration before Arrived. Three minutes of absence before Left. The location-only policy is stricter on the way in: five minutes to confirm, three to leave, and a ten-minute cap on how long evidence is allowed to go stale. There used to be a two-minute gap. Android does not give you a polite stream of fixes when the app is bouncing between foreground and background. The phone is walking, the process is frozen, the next fix shows up several minutes later, and a two-minute rule never confirms a visit you actually made. I widened the gap because the old one was a policy that could not succeed on the device I was holding.

Wi-Fi is the same shape, not a second product. If I named the network, a connected SSID can support the place. If I did not name it, it is noise. The app reads the connected network when Android allows that. It does not inventory nearby networks. On current Android the SSID is redacted unless location permission is also granted, which is annoying and also the point. Identity of a network is location-shaped, and the platform treats it that way.

While the state is "present" and the next fix is still inside, the clock just moves. A candidate absence starts when the fixes are definitely outside, and it only becomes Left if that absence holds for the departure duration. A later inside fix cancels the candidate. The event, when it finally exists, is a confirmed transition. It is not a breadcrumb.

The file is the explanation

Samples land in one append-only NDJSON file per UTC day:

<app-private-root>/observations/YYYY-MM-DD.ndjson

The name is the UTC date, always. The Today view takes the day I am looking at in the device timezone, finds the samples that belong to that local day, and replays them in order. Position first, then connected Wi-Fi, against the place definitions that were in force. What I see is segments, plus the evidence if I ask for it. The raw file is debug data. I can open it. I can delete it. The app does not upload it.

Restart-safe inference state sits beside that, because a process death in the middle of a three-minute confirmation should not forget that the confirmation had already started. Confirmed events are their own records. The samples explain the events. The events are what I meant to keep.

NDJSON is the store I have. The contracts leave room for SQLite later without making the engine care. I am not in a hurry. A file I can read with a text editor has saved me more often than a database I have to trust.

The phone is a bad employee

The Android head is a foreground service with a notification, because that is the deal the platform offers for location in the background. The service owns no inference. It starts the coordinator, the coordinator writes the sample and hands it to the core through a channel, and stopping the service cancels the subscriptions. Nothing in that path is a server.

Permissions are the short list the job actually needs. Internet, for map tiles and address search while I am setting a place up. Coarse and fine location, for the sparse fixes. Background location, as its own request after the foreground one, because Android wants that staged and I am not going to bury it. Nearby Wi-Fi devices, so a modern phone will tell me the connected network. Foreground-service permissions, so the low-frequency observer is allowed to exist. Notifications, so the service can show the notification the platform requires.

If location is off, or the permission is missing, or Android has decided background work is a prize, the app waits. The diagnostics page says what is missing. A hole in the day I can live with. A guessed arrival is a lie I will believe next week.

Backup of the app is disabled. Cleartext is disabled. Map traffic is HTTPS and only to the Kartverket and Geonorge hosts declared for the app. That is the whole network story.

Kartverket sees the map, not the day

There is no account. There is no cloud copy, no analytics, no advertising id, no telemetry. The ledger stays on the device.

The map is the exception, and it is a narrow one. Tiles and address search go to Kartverket and Geonorge while I am picking a place or looking at Today. Those requests carry a viewport or a search. They do not carry the history, the places, or the NDJSON. The map widget says © Kartverket while map content is on screen, because that data is theirs. If the network fails, the local day is still there. Setup is allowed to be offline-ugly. The ledger is not allowed to depend on the tile server to remember that I was at the office.

Novolis.Avalonia.Map is provider-neutral. This app is the one that plugs Kartverket in. Novolis.Avalonia.Mobile owns the reading contracts. The Android package owns the Android adapters. Inference, retention, and the day projection stay in the core, where the tests can run them without a phone. That split is the same rule as the rest of the platform. A library does not grow a window, and a domain does not grow a location API from Google because the sample was annoying.

Taking the ledger with me

Export is a ZIP. Places, revisions, confirmed history, inference state, the daily observations, the cumulative observations, diagnostics, a manifest, and SHA-256 checksums. Import checks every entry before it replaces the local ledger, then rebuilds the derived history. Android uses the system document picker. Windows uses a normal file dialog. I am not syncing this through someone else's account to get a copy onto the other machine. A file I copied is a file I can see.

The older shared-folder action is still there for legacy Android files. It is a recovery hatch, not the design.

What I keep refusing

I will not store the route. I will not keep every GPS fix as a trail I can scrub. Samples exist so a displayed day can be replayed and argued with. They are not a trip.

I will not scan Wi-Fi. I will not invent an arrival to fill a gap. I will not put an account in front of a question that is "was I there."

The Windows build and the Android APK ship from the Presence Ledger release in novolis-apps. The privacy notes live next to the source, in PRIVACY.md and ANDROID-PERMISSIONS.md, because a blog post is not the policy. The policy is what the app refuses to do when the convenient API is one call away.

All posts