Skip to content

Commit 4f2affd

Browse files
sprobst76claude
andcommitted
feat(notifications): show immediate notification on geofence events
- New BackgroundNotificationService for notifications from background isolate - Shows "Arbeitszeit automatisch gestartet" on zone enter - Shows "Arbeitszeit automatisch gestoppt" on zone exit - Notifications appear even when app is closed - No notification for ignored events (bounce protection) - GeofenceEventQueue.enqueue now returns result (added/duplicate/bounce) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cd201b4 commit 4f2affd

File tree

5 files changed

+157
-7
lines changed

5 files changed

+157
-7
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
1414

1515
---
1616

17+
## [0.1.0-beta.32] - 2026-01-19
18+
19+
### Hinzugefügt
20+
- **Sofortige Notification bei Geofence-Events**: Push-Benachrichtigung auch bei geschlossener App
21+
- "▶️ Arbeitszeit automatisch gestartet" beim Betreten der Zone
22+
- "⏹️ Arbeitszeit automatisch gestoppt" beim Verlassen der Zone
23+
- Erscheint sofort in der Statusleiste, auch wenn App nicht geöffnet ist
24+
- Keine Notification bei ignorierten Events (Bounce-Protection)
25+
26+
---
27+
1728
## [0.1.0-beta.31] - 2026-01-19
1829

1930
### Behoben
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'package:flutter/material.dart' show Color;
2+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
3+
4+
/// Service für Notifications aus dem Background-Isolate
5+
/// Wird vom Geofence-Callback verwendet
6+
class BackgroundNotificationService {
7+
static final FlutterLocalNotificationsPlugin _notifications =
8+
FlutterLocalNotificationsPlugin();
9+
static bool _initialized = false;
10+
11+
// Notification IDs
12+
static const int geofenceEventNotificationId = 300;
13+
14+
// Channel ID
15+
static const String channelId = 'geofence_event_channel';
16+
17+
/// Initialisiert den Service (kann mehrfach aufgerufen werden)
18+
static Future<void> init() async {
19+
if (_initialized) return;
20+
21+
const androidSettings =
22+
AndroidInitializationSettings('@mipmap/ic_launcher');
23+
const iosSettings = DarwinInitializationSettings();
24+
25+
const initSettings = InitializationSettings(
26+
android: androidSettings,
27+
iOS: iosSettings,
28+
);
29+
30+
await _notifications.initialize(initSettings);
31+
_initialized = true;
32+
}
33+
34+
/// Zeigt eine Notification für Geofence ENTER (Arbeitszeit gestartet)
35+
static Future<void> showWorkStartedNotification(DateTime timestamp) async {
36+
await init();
37+
38+
final timeStr = '${timestamp.hour.toString().padLeft(2, '0')}:'
39+
'${timestamp.minute.toString().padLeft(2, '0')}';
40+
41+
await _notifications.show(
42+
geofenceEventNotificationId,
43+
'▶️ Arbeitszeit automatisch gestartet',
44+
'Gestartet um $timeStr - Tippe zum Öffnen',
45+
const NotificationDetails(
46+
android: AndroidNotificationDetails(
47+
channelId,
48+
'Geofence Events',
49+
channelDescription: 'Benachrichtigungen bei automatischem Start/Stop',
50+
importance: Importance.high,
51+
priority: Priority.high,
52+
icon: '@mipmap/ic_launcher',
53+
color: Color(0xFF4CAF50), // Grün
54+
autoCancel: true,
55+
),
56+
iOS: DarwinNotificationDetails(),
57+
),
58+
);
59+
}
60+
61+
/// Zeigt eine Notification für Geofence EXIT (Arbeitszeit gestoppt)
62+
static Future<void> showWorkStoppedNotification(DateTime timestamp) async {
63+
await init();
64+
65+
final timeStr = '${timestamp.hour.toString().padLeft(2, '0')}:'
66+
'${timestamp.minute.toString().padLeft(2, '0')}';
67+
68+
await _notifications.show(
69+
geofenceEventNotificationId,
70+
'⏹️ Arbeitszeit automatisch gestoppt',
71+
'Gestoppt um $timeStr - Tippe zum Öffnen',
72+
const NotificationDetails(
73+
android: AndroidNotificationDetails(
74+
channelId,
75+
'Geofence Events',
76+
channelDescription: 'Benachrichtigungen bei automatischem Start/Stop',
77+
importance: Importance.high,
78+
priority: Priority.high,
79+
icon: '@mipmap/ic_launcher',
80+
color: Color(0xFFF44336), // Rot
81+
autoCancel: true,
82+
),
83+
iOS: DarwinNotificationDetails(),
84+
),
85+
);
86+
}
87+
88+
/// Zeigt eine Notification dass Event ignoriert wurde (Bounce-Protection)
89+
static Future<void> showEventIgnoredNotification(String reason) async {
90+
await init();
91+
92+
await _notifications.show(
93+
geofenceEventNotificationId + 1,
94+
'Geofence Event ignoriert',
95+
reason,
96+
const NotificationDetails(
97+
android: AndroidNotificationDetails(
98+
channelId,
99+
'Geofence Events',
100+
channelDescription: 'Benachrichtigungen bei automatischem Start/Stop',
101+
importance: Importance.low,
102+
priority: Priority.low,
103+
icon: '@mipmap/ic_launcher',
104+
autoCancel: true,
105+
),
106+
iOS: DarwinNotificationDetails(),
107+
),
108+
);
109+
}
110+
}

lib/services/geofence_callback.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:developer';
33
import 'package:geofence_foreground_service/geofence_foreground_service.dart';
44
import 'package:geofence_foreground_service/constants/geofence_event_type.dart';
55
import 'geofence_event_queue.dart';
6+
import 'background_notification_service.dart';
67

78
/// Callback-Dispatcher für Geofence-Events
89
/// Wird im Background-Isolate ausgeführt
@@ -17,22 +18,42 @@ void callbackDispatcher() async {
1718
if (triggerType == GeofenceEventType.enter) {
1819
// Benutzer betritt die Zone -> Arbeitszeit starten
1920
log('ENTER Zone: $zoneID - Queuing work start event');
20-
await GeofenceEventQueue.enqueue(GeofenceEventData(
21+
final result = await GeofenceEventQueue.enqueue(GeofenceEventData(
2122
zoneId: zoneID,
2223
event: GeofenceEvent.enter,
2324
timestamp: timestamp,
2425
));
26+
27+
// Notification basierend auf Ergebnis
28+
if (result == GeofenceEventQueue.resultAdded) {
29+
await BackgroundNotificationService.showWorkStartedNotification(timestamp);
30+
log('ENTER: Event queued and notification shown');
31+
} else if (result == GeofenceEventQueue.resultBounce) {
32+
log('ENTER: Event ignored (bounce protection)');
33+
} else {
34+
log('ENTER: Event ignored (duplicate)');
35+
}
2536
} else if (triggerType == GeofenceEventType.exit) {
2637
// Benutzer verlässt die Zone -> Arbeitszeit stoppen
2738
log('EXIT Zone: $zoneID - Queuing work stop event');
28-
await GeofenceEventQueue.enqueue(GeofenceEventData(
39+
final result = await GeofenceEventQueue.enqueue(GeofenceEventData(
2940
zoneId: zoneID,
3041
event: GeofenceEvent.exit,
3142
timestamp: timestamp,
3243
));
44+
45+
// Notification basierend auf Ergebnis
46+
if (result == GeofenceEventQueue.resultAdded) {
47+
await BackgroundNotificationService.showWorkStoppedNotification(timestamp);
48+
log('EXIT: Event queued and notification shown');
49+
} else if (result == GeofenceEventQueue.resultBounce) {
50+
log('EXIT: Event ignored (bounce protection)');
51+
} else {
52+
log('EXIT: Event ignored (duplicate)');
53+
}
3354
}
3455
} catch (e) {
35-
log('Error queuing geofence event: $e', name: 'GeofenceCallback');
56+
log('Error in geofence callback: $e', name: 'GeofenceCallback');
3657
}
3758

3859
return true;

lib/services/geofence_event_queue.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ class GeofenceEventQueue {
5050
static const _queueKey = 'geofence_event_queue';
5151
static const _lastEventKey = 'geofence_last_event';
5252

53+
/// Ergebnis des Enqueue-Vorgangs
54+
static const String resultAdded = 'added';
55+
static const String resultDuplicate = 'duplicate';
56+
static const String resultBounce = 'bounce';
57+
5358
/// Fügt ein Event zur Queue hinzu (aus Background Isolate aufrufbar)
54-
static Future<void> enqueue(GeofenceEventData event) async {
59+
/// Gibt zurück ob Event hinzugefügt wurde oder warum nicht
60+
static Future<String> enqueue(GeofenceEventData event) async {
5561
final prefs = await SharedPreferences.getInstance();
5662

5763
// Aktuelle Queue laden
@@ -65,15 +71,15 @@ class GeofenceEventQueue {
6571
if (lastEvent.event == event.event &&
6672
lastEvent.zoneId == event.zoneId &&
6773
timeDiff < 30) {
68-
return; // Duplikat ignorieren
74+
return resultDuplicate; // Duplikat ignorieren
6975
}
7076

7177
// Bounce-Protection: EXIT→ENTER oder ENTER→EXIT innerhalb von 5 Minuten ignorieren
7278
// Verhindert zerstückelte Einträge bei GPS-Fluktuation an der Zonengrenze
7379
if (lastEvent.zoneId == event.zoneId &&
7480
lastEvent.event != event.event &&
7581
timeDiff < 300) { // 5 Minuten
76-
return; // Bounce ignorieren
82+
return resultBounce; // Bounce ignorieren
7783
}
7884
}
7985

@@ -86,6 +92,8 @@ class GeofenceEventQueue {
8692

8793
// Letztes Event speichern für schnellen Zugriff
8894
await prefs.setString(_lastEventKey, jsonEncode(event.toJson()));
95+
96+
return resultAdded;
8997
}
9098

9199
/// Holt alle Events aus der Queue

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: time_tracker
22
description: Zeiterfassung mit Geofence, Urlaub, Feiertagen & ICS-Sync
3-
version: 0.1.0-beta.31+31
3+
version: 0.1.0-beta.32+32
44
publish_to: 'none'
55

66
environment:

0 commit comments

Comments
 (0)