Skip to content

Commit 0907667

Browse files
sprobst76claude
andcommitted
feat(battery): add battery optimization check and settings
Problem: Android kills GeofenceForegroundService due to memory pressure. When restarting, the service crashes with NullPointerException because the geofence_foreground_service package doesn't handle null intents. This causes unreliable geofence tracking - work time "falls out" randomly. Solution: - Add REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission - Add BatteryOptimizationService to check and request exemption - Add battery optimization status to Geofence Debug Screen - Add button to directly open Android settings Workaround: User should disable battery optimization for VibedTracker to prevent Android from killing the geofence service. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4f2affd commit 0907667

File tree

6 files changed

+200
-7
lines changed

6 files changed

+200
-7
lines changed

CHANGELOG.md

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

1515
---
1616

17+
## [0.1.0-beta.33] - 2026-01-22
18+
19+
### Hinzugefügt
20+
- **Akkuoptimierung-Check**: Diagnose und Einstellungen im Geofence Debug Screen
21+
- Zeigt ob Akkuoptimierung deaktiviert ist (erforderlich für zuverlässiges Geofencing)
22+
- Button zum direkten Öffnen der Android-Einstellungen
23+
- Erklärt warum der Geofence-Service von Android gekillt werden kann
24+
25+
### Bekanntes Problem (extern)
26+
- **Geofence Service Crash**: Bug im `geofence_foreground_service` Package
27+
- Wenn Android den Service wegen Speicherdruck killt, crasht er beim Neustart
28+
- Workaround: Akkuoptimierung für VibedTracker deaktivieren
29+
- Issue wurde an Package-Maintainer gemeldet
30+
31+
---
32+
1733
## [0.1.0-beta.32] - 2026-01-19
1834

1935
### Hinzugefügt

android/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
<!-- Wake lock for background processing -->
2121
<uses-permission android:name="android.permission.WAKE_LOCK"/>
2222

23+
<!-- Request to ignore battery optimizations (keeps geofence service alive) -->
24+
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
25+
2326
<application
2427
android:label="VibedTracker"
2528
android:name="${applicationName}"
Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
package com.vibedtracker.app
22

3+
import android.content.Context
4+
import android.content.Intent
5+
import android.net.Uri
6+
import android.os.Build
7+
import android.os.PowerManager
8+
import android.provider.Settings
39
import io.flutter.embedding.android.FlutterFragmentActivity
10+
import io.flutter.embedding.engine.FlutterEngine
11+
import io.flutter.plugin.common.MethodChannel
412

5-
class MainActivity : FlutterFragmentActivity()
13+
class MainActivity : FlutterFragmentActivity() {
14+
private val BATTERY_CHANNEL = "com.vibedtracker.app/battery"
15+
16+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
17+
super.configureFlutterEngine(flutterEngine)
18+
19+
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL).setMethodCallHandler { call, result ->
20+
when (call.method) {
21+
"isIgnoringBatteryOptimizations" -> {
22+
result.success(isIgnoringBatteryOptimizations())
23+
}
24+
"requestIgnoreBatteryOptimizations" -> {
25+
requestIgnoreBatteryOptimizations()
26+
result.success(true)
27+
}
28+
else -> {
29+
result.notImplemented()
30+
}
31+
}
32+
}
33+
}
34+
35+
private fun isIgnoringBatteryOptimizations(): Boolean {
36+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
37+
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
38+
return powerManager.isIgnoringBatteryOptimizations(packageName)
39+
}
40+
return true // Pre-M doesn't have battery optimization
41+
}
42+
43+
private fun requestIgnoreBatteryOptimizations() {
44+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
45+
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
46+
data = Uri.parse("package:$packageName")
47+
}
48+
startActivity(intent)
49+
}
50+
}
51+
}

lib/screens/geofence_debug_screen.dart

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import '../models/geofence_zone.dart';
99
import '../providers.dart';
1010
import '../services/geofence_event_queue.dart';
1111
import '../services/geofence_sync_service.dart';
12+
import '../services/battery_optimization_service.dart';
1213
import '../models/work_entry.dart';
1314
import '../theme/theme_colors.dart';
1415
import 'package:hive/hive.dart';
@@ -26,6 +27,7 @@ class _GeofenceDebugScreenState extends ConsumerState<GeofenceDebugScreen> {
2627
PermissionStatus? _locationAlwaysPermission;
2728
PermissionStatus? _notificationPermission;
2829
bool? _locationServiceEnabled;
30+
bool? _batteryOptimizationDisabled;
2931

3032
// Current position
3133
Position? _currentPosition;
@@ -113,6 +115,7 @@ class _GeofenceDebugScreenState extends ConsumerState<GeofenceDebugScreen> {
113115
Permission.locationAlways.status,
114116
Permission.notification.status,
115117
Geolocator.isLocationServiceEnabled(),
118+
BatteryOptimizationService.isIgnoringBatteryOptimizations(),
116119
]);
117120

118121
if (mounted) {
@@ -121,6 +124,7 @@ class _GeofenceDebugScreenState extends ConsumerState<GeofenceDebugScreen> {
121124
_locationAlwaysPermission = results[1] as PermissionStatus;
122125
_notificationPermission = results[2] as PermissionStatus;
123126
_locationServiceEnabled = results[3] as bool;
127+
_batteryOptimizationDisabled = results[4] as bool;
124128
});
125129
}
126130
}
@@ -302,18 +306,44 @@ class _GeofenceDebugScreenState extends ConsumerState<GeofenceDebugScreen> {
302306
_locationServiceEnabled == true ? 'Aktiviert' : 'Deaktiviert',
303307
Icons.gps_fixed,
304308
),
309+
const Divider(),
310+
_buildStatusRow(
311+
'Akku-Optimierung',
312+
_batteryOptimizationDisabled == true,
313+
_batteryOptimizationDisabled == true ? 'Deaktiviert (gut)' : 'Aktiv (problematisch)',
314+
Icons.battery_alert,
315+
),
305316
const SizedBox(height: 12),
306317
if (_locationAlwaysPermission != PermissionStatus.granted)
318+
Padding(
319+
padding: const EdgeInsets.only(bottom: 8),
320+
child: ElevatedButton.icon(
321+
onPressed: () async {
322+
final status = await Permission.locationAlways.request();
323+
_addLog('Location Always angefordert: $status');
324+
await _loadPermissions();
325+
},
326+
icon: const Icon(Icons.settings),
327+
label: const Text('Location Always anfordern'),
328+
style: ElevatedButton.styleFrom(
329+
backgroundColor: Colors.orange,
330+
foregroundColor: Colors.white,
331+
),
332+
),
333+
),
334+
if (_batteryOptimizationDisabled != true)
307335
ElevatedButton.icon(
308336
onPressed: () async {
309-
final status = await Permission.locationAlways.request();
310-
_addLog('Location Always angefordert: $status');
337+
await BatteryOptimizationService.requestIgnoreBatteryOptimizations();
338+
_addLog('Akkuoptimierung-Einstellungen geöffnet');
339+
// Reload after a delay (user may have changed setting)
340+
await Future.delayed(const Duration(seconds: 2));
311341
await _loadPermissions();
312342
},
313-
icon: const Icon(Icons.settings),
314-
label: const Text('Location Always anfordern'),
343+
icon: const Icon(Icons.battery_saver),
344+
label: const Text('Akkuoptimierung deaktivieren'),
315345
style: ElevatedButton.styleFrom(
316-
backgroundColor: Colors.orange,
346+
backgroundColor: Colors.red,
317347
foregroundColor: Colors.white,
318348
),
319349
),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'dart:io';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter/services.dart';
4+
5+
/// Service to handle battery optimization settings for reliable background geofencing.
6+
///
7+
/// When Android's battery optimization is enabled for an app, the system may:
8+
/// - Kill background services during memory pressure
9+
/// - Delay or skip scheduled work
10+
/// - Throttle location updates
11+
///
12+
/// This service helps the user disable battery optimization for VibedTracker
13+
/// to ensure reliable geofence detection.
14+
class BatteryOptimizationService {
15+
static const platform = MethodChannel('com.vibedtracker.app/battery');
16+
17+
/// Checks if battery optimization is currently disabled for the app.
18+
/// Returns true if the app is whitelisted (optimization disabled).
19+
static Future<bool> isIgnoringBatteryOptimizations() async {
20+
if (!Platform.isAndroid) return true;
21+
22+
try {
23+
final result = await platform.invokeMethod<bool>('isIgnoringBatteryOptimizations');
24+
return result ?? false;
25+
} on PlatformException catch (e) {
26+
debugPrint('Error checking battery optimization: ${e.message}');
27+
return false;
28+
} on MissingPluginException {
29+
debugPrint('Battery optimization plugin not available');
30+
return true; // Assume OK if plugin not available
31+
}
32+
}
33+
34+
/// Opens the system settings to disable battery optimization for this app.
35+
static Future<bool> requestIgnoreBatteryOptimizations() async {
36+
if (!Platform.isAndroid) return true;
37+
38+
try {
39+
final result = await platform.invokeMethod<bool>('requestIgnoreBatteryOptimizations');
40+
return result ?? false;
41+
} on PlatformException catch (e) {
42+
debugPrint('Error requesting battery optimization exemption: ${e.message}');
43+
return false;
44+
} on MissingPluginException {
45+
debugPrint('Battery optimization plugin not available');
46+
return false;
47+
}
48+
}
49+
50+
/// Shows a dialog explaining why battery optimization should be disabled
51+
/// and offers to open the settings.
52+
static Future<void> showOptimizationDialog(BuildContext context) async {
53+
final isIgnoring = await isIgnoringBatteryOptimizations();
54+
55+
if (isIgnoring) {
56+
debugPrint('Battery optimization already disabled');
57+
return;
58+
}
59+
60+
if (!context.mounted) return;
61+
62+
await showDialog<void>(
63+
context: context,
64+
builder: (context) => AlertDialog(
65+
title: const Text('Akkuoptimierung deaktivieren'),
66+
content: const Column(
67+
mainAxisSize: MainAxisSize.min,
68+
crossAxisAlignment: CrossAxisAlignment.start,
69+
children: [
70+
Text(
71+
'Für zuverlässige automatische Zeiterfassung muss die '
72+
'Akkuoptimierung für VibedTracker deaktiviert werden.',
73+
),
74+
SizedBox(height: 12),
75+
Text(
76+
'Ohne diese Einstellung kann Android den Geofence-Service '
77+
'beenden und die Arbeitszeit wird nicht korrekt erfasst.',
78+
style: TextStyle(fontSize: 13, color: Colors.grey),
79+
),
80+
],
81+
),
82+
actions: [
83+
TextButton(
84+
onPressed: () => Navigator.pop(context),
85+
child: const Text('Später'),
86+
),
87+
FilledButton(
88+
onPressed: () async {
89+
Navigator.pop(context);
90+
await requestIgnoreBatteryOptimizations();
91+
},
92+
child: const Text('Einstellungen öffnen'),
93+
),
94+
],
95+
),
96+
);
97+
}
98+
}

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.32+32
3+
version: 0.1.0-beta.33+33
44
publish_to: 'none'
55

66
environment:

0 commit comments

Comments
 (0)