Skip to content

feat: context profiles (auto-switch locator settings by Wi-Fi, Bluetooth, charging, region) - #2340

Open
Rasbandit wants to merge 15 commits into
owntracks:masterfrom
Rasbandit:feat/context-profiles
Open

Rasbandit wants to merge 15 commits into
owntracks:masterfrom
Rasbandit:feat/context-profiles

Conversation

@Rasbandit

@Rasbandit Rasbandit commented Oct 8, 2026 •

Copy link
Copy Markdown

Summary

Adds context profiles: the locator settings change automatically depending on what the device is connected to or doing. The main use is saving battery while still reporting accurately. Today people do this with Tasker sending CHANGE_MONITORING intents; this makes it native.

Examples it handles:

  • At home (home Wi-Fi): stop location updates entirely and report a static location every 30 minutes.
  • Driving (car Bluetooth): Move mode, high accuracy, short interval.
  • On any other Wi-Fi: Significant mode, balanced priority.
  • Out and about (none of the above): Significant mode with a smaller displacement.

It's off by default. While it's off, nothing about the device is watched and behaviour is unchanged.

What's in it

  • Conditions: a Wi-Fi network (SSID), any Wi-Fi, a Bluetooth device, charging (or charging from cable, wireless or dock), or being inside a waypoint's region. A profile applies when any, all or none of its conditions match.
  • Profiles are kept in priority order, and the first that applies wins.
  • Overrides: monitoring mode, locatorInterval, moveModeLocatorInterval, locatorDisplacement, locatorPriority, ping, and an optional static location that's reported instead of tracking.
  • Manual changes win. Picking a monitoring mode (map, notification, CHANGE_MONITORING intent), or changing a preference the applied profile overrides, suspends that profile until a different one applies. This survives restarts.
  • The ongoing notification shows the applied profile, e.g. "Move monitoring mode · Driving".
  • Settings UI:
    • A Context profiles screen showing the active profile live, with an ordered list and up/down arrows.
    • A profile editor that only shows the overrides relevant to the chosen mode.
    • Nearby Wi-Fi and paired Bluetooth pickers.
    • A map location picker for the static location, using the same kind of map the user has chosen for the main map (Google Maps or OpenStreetMap).

Design

  • The core is ContextProfileManager. It matches profiles against a DeviceContext (Wi-Fi, Bluetooth, charging, entered regions) and publishes the resulting LocatorSettings. The location request, ping schedule, publish suppression, significant-motion rate limit and displayed monitoring mode read those settings instead of the raw preferences.
  • Device context sources are cold flows. Their listeners are only registered while profiles are enabled.
  • Nothing is matched, requested or pinged until every source has reported once (with a 3s fallback), so the empty context at startup can't briefly apply a profile.
  • While a static location is in use, real locations are only published when the user explicitly asks (USER). Other on-demand requests (e.g. remote reportLocation) get the static location, and it's not used for region transition detection.
  • Region conditions reference waypoints by tst, like exported waypoints do, so they survive waypoint re-imports.

Open questions for maintainers

  1. Storage format. Profiles are stored as a JSON string in the exportable contextProfiles preference, because PreferencesStore only handles primitives. That means escaped JSON in .otrc exports. Would you prefer a typed preference with a serializer, or something else? I'm happy to rework it.
  2. Permissions.
    • BLUETOOTH (maxSdk 30) and BLUETOOTH_CONNECT are added for Bluetooth conditions. The runtime permission is only requested when a Bluetooth condition is added.
    • CHANGE_WIFI_STATE is only for requesting a fresh scan in the Wi-Fi picker. Without it the picker falls back to cached scan results, which are often empty. It can be dropped if you'd rather.
  3. oss limitation. Without OS geofencing, region transitions come from location fixes, so a profile that combines a region condition with a static location can't detect leaving the region. This is documented rather than solved.
  4. Splitting. The commits are laid out to be reviewable one at a time: prep refactors, then the engine, then the UI, the picker following the user's map choice, then fixes from a further review and tests. I can split this into separate PRs if that's easier.

Testing

  • 120 new JVM unit tests, covering:
    • matching, settings resolution, suspension and settling, readiness, ping scheduling
    • static-location publishing and gating, entered regions, Wi-Fi tracking, decoding
    • the new preferences, added to PreferencesGettersAndSetters, PreferenceTest, ImportExportTest and LoadViewModelTest as with other preferences
  • 12 new instrumented tests:
    • ContextProfilesPreferencesTests: add, configure, delete and reorder profiles, and show the active one
    • ContextProfileTests: static location published instead of real fixes, the profile's mode on the map and in the notification, CHANGE_MONITORING suspending a profile
    • LocationPickerActivityTests: the picker's results
    • They use a new FakeDeviceContextProvider swapped in through @TestInstallIn, the same way as TestGeofencingCapabilityModule, because an emulator's Wi-Fi, Bluetooth and charging state can't be set.
  • The same checks as CI, run locally:
    • ktfmtCheck
    • assembleDebug assembleAndroidTest assembleRelease
    • createGmsDebugUnitTestCoverageReport, all passing
    • lint: nothing new from this branch apart from the untranslated new strings
  • Instrumented tests run locally: only ContextProfilesPreferencesTests (passing on an API 36 emulator). ContextProfileTests and LocationPickerActivityTests haven't been run locally (the only emulator available was busy with another project), so CI will be their first run.
  • Manually tested on an emulator (both flavours) and on a Pixel 8 Pro running Android 17, reporting over HTTP to a self-hosted Dawarich:
    • Wi-Fi, Bluetooth and charging switching
    • the static location being reported once on arrival and then every ping, with no real fixes leaking
    • manual override
    • startup with no profile flicker
    • rotation in the location picker

AI assistance

This was written with Claude Code, with me directing the design, reviewing it and testing on my own device. The commits carry a Co-Authored-By trailer.

🤖 Generated with Claude Code

Rasbandit and others added 15 commits October 8, 2026 01:22
Pull the osmdroid set-up out of OSMMapFragment, so that other screens
showing an osmdroid map can share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose the waypoints whose regions were last entered, keyed by tst as
exported waypoints are, so that other features can react to being in a
region.

It's updated atomically from each changed waypoint, including the
unnotified updates that transitions make. The in-memory test repo now
implements update and delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a flow of the connected Wi-Fi network's SSID. It follows Wi-Fi
itself rather than the default network, so it's still known behind a
VPN, treats a redacted SSID as unknown, and only watches Wi-Fi while
it's collected.

Also list the SSIDs from the latest Wi-Fi scan, strongest first.
Requesting a fresh scan needs CHANGE_WIFI_STATE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Context profiles change the locator settings automatically depending on
the device's context, replacing the automation apps (e.g. Tasker) that
people use to switch monitoring modes. They're off by default, and
nothing about the device is watched until they're turned on.

A profile has conditions (a Wi-Fi network, any Wi-Fi, a Bluetooth
device, charging, charging from a cable, wireless or dock, being inside
a waypoint's region) and applies when any, all or none of them match.
Profiles are kept in priority order and the first that applies wins. Its
overrides (monitoring mode, intervals, displacement, priority, ping)
replace the matching preferences while it applies, and a static location
can be reported instead of tracking at all.

ContextProfileManager matches the profiles against the DeviceContext and
publishes the resulting LocatorSettings, which the location request,
ping schedule, publish suppression, significant motion rate limit and
displayed monitoring mode now use instead of the raw preferences.
Nothing is matched, requested or pinged until every part of the device
context has reported once, so that a profile isn't applied for an empty
context at startup.

Choosing a monitoring mode by hand, or changing a preference the applied
profile overrides, suspends that profile until a different one applies,
and that's remembered across restarts. While a static location is
reported, real locations are only sent when the user explicitly asks for
one, and it isn't used to detect region transitions.

Profiles are stored as JSON in the exportable contextProfiles
preference, documented in PREFERENCES.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a Context profiles screen to turn profiles on, see which profile
applies right now (or that one is paused by a manual change), and list,
add and reorder the profiles.

Each profile is edited on its own screen: its conditions and how they
match, and its overrides, showing only the ones that make a difference
in the chosen monitoring mode. Wi-Fi networks can be picked from the
nearby ones, several at once, and Bluetooth devices from the paired
ones, asking for BLUETOOTH_CONNECT only then.

A static location is picked on a map, with the coordinates shown and
editable underneath and a button to go to the current location. The
picker uses OpenStreetMap so that it works the same in both flavours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The location picker always showed OpenStreetMap, so it could look
different from the main map. It now shows the same kind of map the user
has chosen for the main map: Google Maps, in the chosen map type and
with the same night style, or OpenStreetMap with the chosen tiles,
inverted at night as on the main map.

The picker talks to a small PickerMap interface, with an OpenStreetMap
implementation shared by both flavours and a Google Maps one in gms,
picked per flavour like the main map fragment. The dark mode tile
handling is shared with the main OpenStreetMap map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ContextProfileManager saved the suspended profile while holding its own
lock. Saving a preference notifies listeners while holding the
preferences' listener lock, and the manager's onPreferenceChanged is
called with that held and then takes the manager's lock, so a device
context change and a preference change at the same time could deadlock.
Save it after releasing the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Region transitions were published with the real location that triggered
them, even while a static location was being reported. They're now
reported at the static location.

Real locations requested before it's known which profile applies (e.g.
by significant motion or a remote reportLocation at startup) could be
published, though that profile might report a static location. They're
now only published once it's known, unless the user asked for their
location.

A static location also went through the inaccurate location filter, so
with that set to its accuracy or lower, nothing was published at all.
Its accuracy is configured rather than measured, so it no longer does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Bluetooth connection broadcasts are sent by the Bluetooth stack
rather than the system, and before T a receiver registered as not
exported requires a permission of ours, which it doesn't have, so they
were never received there. Register the receiver as exported; they're
protected broadcasts, so other apps can't send them.

Also fixes lint warnings: helpers used from nested classes are internal
rather than private to avoid synthetic accessors, and the Bluetooth
permission request drops an SDK check that's always true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One profile that couldn't be decoded, e.g. one using a condition from a
newer version, made decoding fail for them all, and saving the list
afterwards then lost every profile. Decode each profile on its own,
leaving out any that can't be decoded, and keep those when saving the
others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Losing any Wi-Fi network reported that there was no Wi-Fi, even if
another was still connected, as can briefly happen while switching
networks. Keep track of each connected Wi-Fi network's SSID instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
orFallbackAfter checked whether anything had been emitted separately
from sending, so the fallback could win the check but be sent after a
real value that arrived at the same moment, leaving that part of the
device context stale. Check and send together under a mutex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A momentary change in the device context, e.g. Wi-Fi or a charger
dropping out for a second, switched profiles straight away, and dropped
the suspension of a profile the user had overridden by hand, so their
choice was silently undone when it came back. Once the context is known,
a change now has to last ten seconds before profiles are matched again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the contextProfiles and contextProfilesEnabled preferences to the
preference getter, setter and export tests, as with the other
preferences, and check that the suspended profile isn't exported. Also
test that profiles survive importing and exporting a configuration,
including region conditions keeping their waypoint, and that malformed
profiles import as none.

Also adds labels for the new preferences to the configuration import
screen, rather than it showing their raw keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the DeviceContextProvider binding into its own DeviceContextModule,
like GeofencingCapabilityModule, so that instrumented tests can replace
it with a FakeDeviceContextProvider whose Wi-Fi, Bluetooth and charging
they set, as an emulator's can't be.

Adds end to end tests that a profile's static location is published
instead of real locations, that an applied profile's monitoring mode
shows on the map and in the notification, and that a change monitoring
intent suspends it; tests of adding, configuring, deleting and
reordering profiles and showing the one that applies; and tests of the
location picker's results.

Also documents that context changes have to settle before profiles
switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Rasbandit
Rasbandit marked this pull request as ready for review October 8, 2026 08:22
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