testsAndMisc/focus_owner/lib/home_location.dart
Krzysztof kuhy Rudnicki d0fcda3a47
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Provision home coordinates out of git; layer the workout signal; drop the test hook
The geofence needs HOME_LAT/HOME_LON, but those say where I live, so the
committed policy asset is generated with --redact-home and the real values are
pushed to app-private storage instead. push_home_location.sh greps
config_secrets.sh rather than sourcing it -- that file holds other secrets and
this script has no business evaluating them -- and refuses the shipped
REDACTED_* placeholder with a message naming the file to fill in, rather than
writing whatever the parse happened to land on.

Private storage was chosen for its lifetime as much as its permissions: it does
not survive an uninstall, so reinstalling forces re-provisioning instead of
silently inheriting a stale home. The default umask left the file 666, so the
script chmods it 600; the shell user gets Permission denied, and the
/data/local/tmp staging copy is removed after the transfer.

Every read failure returns null rather than throwing. Null reaches the
enforcement layer as location-unknown, which already fails closed into
enforcing -- so a corrupt coordinates file cannot become an unlock, and cannot
crash the service either.

Workout detection replaces a root-only SQLite read of StrongLifts' private
database. The manual source is time-boxed rather than a plain toggle, because a
workout mode left on by accident is indistinguishable from having no exception
at all. The guarded wrapper catches broadly and times out: nothing about a
workout probe failing should be able to stop enforcement running. The combined
source can name which signal opened the exception, so the UI can explain an
unlock rather than presenting it as magic. Only the two sources verified to
exist on this device will be wired -- com.kuhy.workout_app exposes nothing
readable and org.runnerup is not installed.

HideTestReceiver is deleted. It proved setApplicationHidden persistence, which
is what it was for, but it applied policy from an exported receiver and that has
no place in a build that will later hold device owner. Confirmed absent from the
rebuilt and reinstalled APK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
2026-08-09 21:42:20 +02:00

61 lines
2.3 KiB
Dart

import 'dart:convert';
import 'dart:io';
/// The home coordinates, kept out of the committed policy asset.
///
/// `assets/policy.json` ships with `latitude`/`longitude` null so that the
/// repository never records where the user lives. The real values are pushed
/// to the app's private storage at provisioning time by
/// `scripts/push_home_location.sh`, which reads them from the untracked
/// `phone_focus_mode/config_secrets.sh`.
///
/// Private app storage is not world-readable and does not survive an uninstall,
/// which is the right lifetime for this: reinstalling the app should require
/// re-provisioning rather than silently inheriting a stale home.
class HomeLocationStore {
const HomeLocationStore(this.file);
/// Filename written by the provisioning script.
static const String fileName = 'home_location.json';
final File file;
/// Reads the stored coordinates, or null when none have been provisioned.
///
/// Returns null rather than throwing on a malformed or unreadable file. The
/// caller treats an absent home the same way it treats an absent GPS fix —
/// as "cannot determine location", which fails closed into enforcing rather
/// than releasing.
Future<({double latitude, double longitude})?> read() async {
if (!file.existsSync()) return null;
try {
final decoded = jsonDecode(await file.readAsString());
if (decoded is! Map<String, Object?>) return null;
final lat = decoded['latitude'];
final lon = decoded['longitude'];
if (lat is! num || lon is! num) return null;
if (lat.abs() > 90 || lon.abs() > 180) return null;
return (latitude: lat.toDouble(), longitude: lon.toDouble());
} on FormatException {
return null;
} on FileSystemException {
return null;
}
}
/// Writes coordinates, rejecting values that cannot describe a real place.
Future<void> write(double latitude, double longitude) async {
if (latitude.abs() > 90 || longitude.abs() > 180) {
throw ArgumentError('coordinates out of range: $latitude, $longitude');
}
await file.writeAsString(
jsonEncode({'latitude': latitude, 'longitude': longitude}),
);
}
/// Removes any stored coordinates.
Future<void> clear() async {
if (file.existsSync()) await file.delete();
}
}