Skip to content

feat(profile): profile location — coarse capture, manual entry, and an honest non-clear (#37) - #130

Merged
Adron merged 2 commits into
parity/queuefrom
issue/37-settings-location
Sep 16, 2026
Merged

Adron merged 2 commits into
parity/queuefrom
issue/37-settings-location

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #37. Part of epic #31 — the last of its six sub-issues.

The headline: you cannot clear a location, and the UI says so

The first implementation shipped a Clear button on the assumption that explicit nulls would unset the
coordinates. I probed the live API and they do not. Against the test account (values restored
afterwards and verified):

sent result
{"latitude":null,"longitude":null} 400 latitude must be a number between -90 and 90
{"latitude":"","longitude":""} 400 (same)
{"latitude":"null","longitude":"null"} 400 (same)
{"latitude":0,"longitude":0} 200, stored as 0 / 0

PATCH /api/user/update validates the key as a required number whenever present, and omitting it
means "leave unchanged". 0,0 is accepted but that is Null Island — a real coordinate, not an
absence. So there is no way to unset a profile location through the API.

The fix makes the broken path unrepresentable rather than merely avoided: clearLocation() is
gone and the private patch(update: LocationUpdate) became save(coordinates: Coordinates), which
builds LocationUpdate.Set itself — the ViewModel literally cannot express a clear, so no user action
and no future careless edit can issue the request.

"Clear location" stays visible but disabled, with the reason immediately beneath it. Visible-but-
disabled was chosen over removing the control (because "how do I remove this?" is asked at the
button
, and a missing control leaves that unanswered) and over enabled-explains-on-tap (which makes
someone press a control to learn it does nothing).

The LocationUpdate.Clear / JsonElement? plumbing is kept, with the probe recorded verbatim in
its KDoc — re-enabling is a one-liner the day the API accepts a null.

The rest

  • Coarse only. DeviceLocationSource is the single component that can read a position; it uses
    the framework LocationManager (no Play Services) and reads only fused/network/passive —
    GPS is excluded because it needs ACCESS_FINE_LOCATION, which the app never requests. One-shot,
    10 s budget, last-known fallback: no subscription, no background reader, nothing on load.
  • The reading is rounded to 2 dp (~1.1 km) before saving, since a coarse fix carries no more than
    that; a typed coordinate is saved exactly as typed.
  • Rationale before the system dialog, always — what is read, that it is read once and never in the
    background, that it is rounded, that it is saved to the account, and that saying no is fine. It now
    also states that a saved location can be replaced but not removed, which belongs in the consent
    moment.
  • Denial is inert: a note in the ordinary colour (not error) pointing at manual entry; a permanent
    denial adds where the decision can be changed. Manual entry and everything else keep working.
  • No optimistic update — unlike the sibling rows. A row claiming the account stores a position it
    does not is a privacy claim the app cannot back up, so the displayed value is always server truth.

Placement

/help/settings has two relevant sections: Profile location ("Latitude / Longitude: Optional
location for your profile…"), listed after Message settings and before Permissions; and
Permissions. The coordinates went into their own Profile location group in exactly the web's
position, and the permission request stays inside that group, attached to the button that needs
it — on Android the system dialog must follow the action that triggers it. #34's Permissions group is
untouched.

Privacy copy

The help centre does not say who can see a stored location, so nothing was guessed. The section
states: it is saved on the account (not just the device), the public profile does not include it, the
help centre does not say who else can, and it can be replaced but not removed. The public-profile
claim is live-verified — unauthenticated GET /api/users/messenger returns no coordinates.

Worth flagging: /help/settings says of the web's geolocation grant that InterlinedList "does not
store your raw coordinates", which sits badly beside a permanently stored 47.6062 / -122.3321 on the
user record. The app's copy describes what actually happens.

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, 1411 tests, 0 failures.
Includes the API refuses a clear and the cached location survives it (replaying the exact 400 body),
no user action can send a clear (exercises save, device capture, denial and dismiss, asserting no
LocationUpdate.Clear ever reaches the repository), permission-denied paths sending and reading
nothing, and out-of-range entries refused with no request. Compose tests compile, not executed.

API gap to raise separately

PATCH /api/user/update has no way to unset latitude/longitude, so "never set" and "set once,
now regretted" are not both reachable. The help centre calls it "Optional location", which is only
half true — optional to set, impossible to unset. Worth checking whether the web can clear it (a
different endpoint, or a direct DB write); if not, every web user is in the same position.

Adds the web's "Profile location" section to Android Settings: the optional
latitude/longitude pair on the account, which the help centre documents as
"Optional location for your profile, used by the Weather widget and similar
location-aware features". It sits between Message settings and Permissions,
the order /help/settings itself uses.

Coordinates can be captured from the device or typed in:

- Capture asks for ACCESS_COARSE_LOCATION only — the stored pair drives
  city-level surfaces, not navigation — and always shows the app's own
  rationale *before* the system dialog, since afterwards the decision has
  already been made. The reading is one-shot, taken only from a press of
  "Use my location", and rounded to about a kilometre before it is saved.
- Refusing costs nothing. Any denial, including a permanent one, leaves
  manual entry and the rest of Settings working; the section says so, in
  plain text and not in the error colour.
- Manual entry is validated against -90..90 and -180..180 and refused before
  any request is made.

A saved location cannot be removed, and the UI says so instead of offering a
button that would fail. PATCH /api/user/update validates `latitude` as a
required number whenever the key is present, so a JSON null, an empty string
and the string "null" are all answered with 400 "latitude must be a number
between -90 and 90"; omitting the key leaves the value untouched, and 0,0 is
a real position in the Gulf of Guinea rather than an absence. So "Clear
location" is rendered disabled with the reason beside it, the view model can
only build LocationUpdate.Set, and the rationale dialog says up front that a
location can later be replaced but not removed. The three-state
LocationUpdate / JsonElement plumbing stays, with the probe results recorded
on LocationUpdate.Clear and its request shape pinned by tests, so re-enabling
it when the API allows a null is a one-liner. This is an API gap, not an app
limitation.

Nothing is applied optimistically here: unlike a page size, a row claiming
the account stores a position it does not would be a privacy claim the app
cannot back up, so the displayed value is only ever what the server confirmed.

Closes #37
@Adron
Adron merged commit 56a23e4 into parity/queue Sep 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant