feat(todo): places are watched by a quiet service, and asked to notify (ADR 0033, D32)
The first walk showed two faults. Android gives a proximity alert of an app in the background a fix every 30 minutes (17:04:38, then 17:34:43 on the Pixel), so a short stop goes unseen. And the place did remind, at 17:36:43, but nothing had asked to notify, so Android showed nothing. A foreground service of the type location now takes its own fixes while places wait, under the quietest notification there is: as long as reaching the nearest circle takes at 30 m/s, doubled while the phone lies still, between 30 s and 5 min. A place with a reminder asks to notify as any reminder does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
0a9facc3a2
commit
a7385e2169
7 changed files with 499 additions and 125 deletions
|
|
@ -292,6 +292,8 @@
|
|||
"locationRemindArrive": "Beim Ankommen",
|
||||
"locationRemindLeave": "Beim Verlassen",
|
||||
"locationRemove": "Entfernen",
|
||||
"locationWatching": "Achtet auf deine Orte",
|
||||
"locationWatchingChannel": "Überwachte Orte",
|
||||
"locationAccessTitle": "Standort immer erlauben",
|
||||
"locationAccessHint": "Damit Halkyon Todo dich beim Ankommen oder Verlassen erinnert, braucht es deinen Standort auch, wenn es geschlossen ist. Er bleibt auf diesem Gerät. Wähle als Nächstes „Immer zulassen“.",
|
||||
"locationAccessGo": "Weiter",
|
||||
|
|
|
|||
|
|
@ -988,6 +988,8 @@
|
|||
"@locationRemove": {
|
||||
"description": "A task's location's menu."
|
||||
},
|
||||
"locationWatching": "Watching your places",
|
||||
"locationWatchingChannel": "Places watched",
|
||||
"locationAccessTitle": "Allow location all the time",
|
||||
"@locationAccessTitle": {
|
||||
"description": "Before asking for the position in the background."
|
||||
|
|
|
|||
|
|
@ -1852,6 +1852,18 @@ abstract class AppLocalizations {
|
|||
/// **'Remove'**
|
||||
String get locationRemove;
|
||||
|
||||
/// No description provided for @locationWatching.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Watching your places'**
|
||||
String get locationWatching;
|
||||
|
||||
/// No description provided for @locationWatchingChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Places watched'**
|
||||
String get locationWatchingChannel;
|
||||
|
||||
/// Before asking for the position in the background.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1141,6 +1141,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get locationRemove => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get locationWatching => 'Achtet auf deine Orte';
|
||||
|
||||
@override
|
||||
String get locationWatchingChannel => 'Überwachte Orte';
|
||||
|
||||
@override
|
||||
String get locationAccessTitle => 'Standort immer erlauben';
|
||||
|
||||
|
|
|
|||
|
|
@ -1140,6 +1140,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get locationRemove => 'Remove';
|
||||
|
||||
@override
|
||||
String get locationWatching => 'Watching your places';
|
||||
|
||||
@override
|
||||
String get locationWatchingChannel => 'Places watched';
|
||||
|
||||
@override
|
||||
String get locationAccessTitle => 'Allow location all the time';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,23 @@ enum LocationAccess() {
|
|||
/// A point on the map.
|
||||
typedef LatLon = ({double lat, double lon});
|
||||
|
||||
/// Where the device watches places: the system's proximity alerts on Android
|
||||
/// alone, with no Google services, on the device, as the instance reads no
|
||||
/// task (D32).
|
||||
/// Where the device watches places: a foreground service on Android alone,
|
||||
/// with no Google services, on the device, as the instance reads no task
|
||||
/// (D32).
|
||||
abstract interface class LocationReminderGateway() {
|
||||
/// Whether this device can watch places at all.
|
||||
bool get available;
|
||||
|
||||
/// Makes [watches] the places watched, those watched before and no longer
|
||||
/// among them stopped; [channel] names the notifications' kind.
|
||||
Future<void> replace(List<LocationWatch> watches, {required String channel});
|
||||
/// among them stopped; [channel] names the notifications' kind, [watching]
|
||||
/// says what the quiet notification of watching says, [watchingChannel]
|
||||
/// names its kind.
|
||||
Future<void> replace(
|
||||
List<LocationWatch> watches, {
|
||||
required String channel,
|
||||
String watching = '',
|
||||
String watchingChannel = '',
|
||||
});
|
||||
|
||||
/// What the device allows now.
|
||||
Future<LocationAccess> access();
|
||||
|
|
@ -49,6 +56,8 @@ class const NoLocationReminders() implements LocationReminderGateway {
|
|||
Future<void> replace(
|
||||
List<LocationWatch> watches, {
|
||||
required String channel,
|
||||
String watching = '',
|
||||
String watchingChannel = '',
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
|
|
@ -81,9 +90,13 @@ class const AndroidLocationReminders() implements LocationReminderGateway {
|
|||
Future<void> replace(
|
||||
List<LocationWatch> watches, {
|
||||
required String channel,
|
||||
String watching = '',
|
||||
String watchingChannel = '',
|
||||
}) => _channel.invokeMethod<void>('replace', {
|
||||
'watches': [for (final watch in watches) watch.toJson()],
|
||||
'channel': channel,
|
||||
'watching': watching,
|
||||
'watchingChannel': watchingChannel,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -64,15 +64,16 @@ class _ReminderSchedulerState() extends State<ReminderScheduler> {
|
|||
final tasks = [
|
||||
for (final task in state.tasks) StoredTask(task.id, task.content),
|
||||
];
|
||||
_watch(tasks);
|
||||
final watching = _watch(tasks);
|
||||
final plan = planReminders(tasks, now: clock.now());
|
||||
// Asked to notify wherever the reminders came from (D31): typed, added
|
||||
// by the automatic reminder, chosen in a sheet or synced; and read
|
||||
// again as the app comes back, from the system's settings perhaps.
|
||||
// by the automatic reminder, chosen in a sheet or synced, or at a place
|
||||
// (D32); and read again as the app comes back, from the system's
|
||||
// settings perhaps.
|
||||
unawaited(
|
||||
PersonScope.of<TodoPerson>(
|
||||
context,
|
||||
).notifications.planned(any: plan.isNotEmpty),
|
||||
).notifications.planned(any: plan.isNotEmpty || watching),
|
||||
);
|
||||
final snooze = state.settings.reminders.snooze;
|
||||
if (listEquals(plan, _planned) && snooze == _snooze) {
|
||||
|
|
@ -101,8 +102,9 @@ class _ReminderSchedulerState() extends State<ReminderScheduler> {
|
|||
);
|
||||
}
|
||||
|
||||
/// Hands the places [tasks] remind at to the device, where they changed.
|
||||
void _watch(List<StoredTask> tasks) {
|
||||
/// Hands the places [tasks] remind at to the device, where they changed;
|
||||
/// whether there are any.
|
||||
bool _watch(List<StoredTask> tasks) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
final watches = [
|
||||
|
|
@ -120,9 +122,12 @@ class _ReminderSchedulerState() extends State<ReminderScheduler> {
|
|||
todo.locationReminders.replace(
|
||||
watches,
|
||||
channel: l10n.remindersChannel,
|
||||
watching: l10n.locationWatching,
|
||||
watchingChannel: l10n.locationWatchingChannel,
|
||||
),
|
||||
);
|
||||
}
|
||||
return watches.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
Loading…
Reference in a new issue