diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0a01c032..ba0d2876 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,7 +16,7 @@ val backgroundGeolocation = project(":flutter_background_geolocation") apply { from("${backgroundGeolocation.projectDir}/background_geolocation.gradle") } val keystoreProperties = Properties() -val keystorePropertiesFile = rootProject.file("../../environment/key.properties") +val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { keystoreProperties.load(FileInputStream(keystorePropertiesFile)) } diff --git a/lib/configuration_service.dart b/lib/configuration_service.dart index a3c34ead..e0b7ac04 100644 --- a/lib/configuration_service.dart +++ b/lib/configuration_service.dart @@ -1,6 +1,7 @@ import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; import 'preferences.dart'; +import 'schedule_service.dart'; class ConfigurationService { static Future applyUri(Uri uri) async { @@ -23,7 +24,10 @@ class ConfigurationService { await _applyBoolParameter(parameters, Preferences.buffer); await _applyBoolParameter(parameters, Preferences.wakelock); await _applyBoolParameter(parameters, Preferences.stopDetection); + await _applyScheduleParameters(parameters); await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); + await ScheduleService.sync(); + await _applyServiceParameter(parameters); } static Future _applyStringParameter( @@ -59,5 +63,65 @@ class ConfigurationService { } } } + + static Future _applyScheduleParameters( + Map parameters) async { + final scheduleEntry = parameters['schedule']?.trim(); + if (scheduleEntry != null && scheduleEntry.isNotEmpty) { + await Preferences.instance.setString(Preferences.scheduleEntry, scheduleEntry); + await Preferences.instance.setBool(Preferences.scheduleEnabled, true); + return; + } + + final start = parameters['startTime']; + final stop = parameters['stopTime']; + if (_isValidTime(start) && _isValidTime(stop)) { + final normalizedStart = _normalizeTime(start!); + final normalizedStop = _normalizeTime(stop!); + final days = parameters['days']?.trim(); + final entryDays = (days != null && days.isNotEmpty) ? days : '1-7'; + final entry = '$entryDays $normalizedStart-$normalizedStop'; + await Preferences.instance.setString(Preferences.scheduleStart, normalizedStart); + await Preferences.instance.setString(Preferences.scheduleStop, normalizedStop); + await Preferences.instance.setString(Preferences.scheduleEntry, entry); + await Preferences.instance.setBool(Preferences.scheduleEnabled, true); + } + } + + static Future _applyServiceParameter( + Map parameters) async { + final value = parameters['service']; + switch (value) { + case 'true': + await bg.BackgroundGeolocation.start(); + break; + case 'false': + await bg.BackgroundGeolocation.stop(); + break; + } + } + + static bool _isValidTime(String? value) { + if (value == null) { + return false; + } + final parts = value.split(':'); + if (parts.length != 2) { + return false; + } + final hour = int.tryParse(parts[0]); + final minute = int.tryParse(parts[1]); + if (hour == null || minute == null) { + return false; + } + return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59; + } + + static String _normalizeTime(String value) { + final parts = value.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4e01dd9b..d891fce0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -32,5 +32,9 @@ "passwordError": "Wrong password", "startAction": "Start service", "stopAction": "Stop service", - "sosAction": "Send SOS" + "sosAction": "Send SOS", + "scheduleEnabledLabel": "Enable schedule", + "scheduleEntryLabel": "Schedule entries", + "scheduleEntryHint": "Example: 1-5 08:00-17:00", + "scheduleUnsetLabel": "Not set" } diff --git a/lib/main.dart b/lib/main.dart index 80d8c505..c89d7a09 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:rate_my_app/rate_my_app.dart'; import 'package:traccar_client/geolocation_service.dart'; import 'package:traccar_client/push_service.dart'; import 'package:traccar_client/quick_actions.dart'; +import 'package:traccar_client/schedule_service.dart'; import 'l10n/app_localizations.dart'; import 'main_screen.dart'; @@ -24,6 +25,7 @@ void main() async { await Preferences.init(); await Preferences.migrate(); await GeolocationService.init(); + await ScheduleService.sync(); await PushService.init(); runApp(const MainApp()); } diff --git a/lib/preferences.dart b/lib/preferences.dart index fe206ab5..fd0da8c7 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -20,6 +20,10 @@ class Preferences { static const String buffer = 'buffer'; static const String wakelock = 'wakelock'; static const String stopDetection = 'stop_detection'; + static const String scheduleStart = 'schedule_start'; + static const String scheduleStop = 'schedule_stop'; + static const String scheduleEntry = 'schedule_entry'; + static const String scheduleEnabled = 'schedule_enabled'; static const String lastTimestamp = 'lastTimestamp'; static const String lastLatitude = 'lastLatitude'; @@ -35,6 +39,7 @@ class Preferences { allowList: { id, url, accuracy, distance, interval, angle, heartbeat, fastestInterval, buffer, wakelock, stopDetection, + scheduleStart, scheduleStop, scheduleEntry, scheduleEnabled, lastTimestamp, lastLatitude, lastLongitude, lastHeading, 'device_id_preference', 'server_url_preference', 'accuracy_preference', 'frequency_preference', 'distance_preference', 'buffer_preference', @@ -70,6 +75,24 @@ class Preferences { await instance.setBool(buffer, instance.getBool(buffer) ?? true); await instance.setBool(stopDetection, instance.getBool(stopDetection) ?? true); await instance.setInt(fastestInterval, instance.getInt(fastestInterval) ?? 30); + await instance.setBool(scheduleEnabled, instance.getBool(scheduleEnabled) ?? false); + await _ensureScheduleEntry(); + } + + static Future _ensureScheduleEntry() async { + final hasSchedule = instance.getBool(scheduleEnabled) ?? false; + if (!hasSchedule) { + return; + } + + final entry = instance.getString(scheduleEntry); + if (entry != null && entry.trim().isNotEmpty) { + return; + } + + final start = instance.getString(scheduleStart) ?? '08:00'; + final stop = instance.getString(scheduleStop) ?? '17:00'; + await instance.setString(scheduleEntry, '1-7 $start-$stop'); } static bg.Config geolocationConfig() { diff --git a/lib/schedule_service.dart b/lib/schedule_service.dart new file mode 100644 index 00000000..488d6d14 --- /dev/null +++ b/lib/schedule_service.dart @@ -0,0 +1,44 @@ +import 'dart:developer' as developer; + +import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' + as bg; + +import 'preferences.dart'; + +class ScheduleService { + static Future sync() async { + final enabled = Preferences.instance.getBool(Preferences.scheduleEnabled) ?? false; + final rawEntry = Preferences.instance.getString(Preferences.scheduleEntry)?.trim(); + + if (!enabled || rawEntry == null || rawEntry.isEmpty) { + await _clear(); + return; + } + + final entries = rawEntry + .split(RegExp(r'[\r\n]+')) + .map((value) => value.trim()) + .where((value) => value.isNotEmpty) + .toList(); + if (entries.isEmpty) { + await _clear(); + return; + } + + try { + await bg.BackgroundGeolocation.setConfig(bg.Config(schedule: entries)); + await bg.BackgroundGeolocation.startSchedule(); + } catch (error, stackTrace) { + developer.log('Failed to apply schedule', error: error, stackTrace: stackTrace); + } + } + + static Future _clear() async { + try { + await bg.BackgroundGeolocation.stopSchedule(); + await bg.BackgroundGeolocation.setConfig(bg.Config(schedule: const [])); + } catch (error, stackTrace) { + developer.log('Failed to clear schedule', error: error, stackTrace: stackTrace); + } + } +} diff --git a/lib/settings_screen.dart b/lib/settings_screen.dart index ccc8b1cc..4dfd112b 100644 --- a/lib/settings_screen.dart +++ b/lib/settings_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_background_geolocation/flutter_background_geolocation.da import 'package:traccar_client/main.dart'; import 'package:traccar_client/password_service.dart'; import 'package:traccar_client/qr_code_screen.dart'; +import 'package:traccar_client/schedule_service.dart'; import 'package:wakelock_partial_android/wakelock_partial_android.dart'; import 'l10n/app_localizations.dart'; @@ -19,8 +20,78 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { + static const _defaultScheduleEntry = '1-7 08:00-17:00'; + bool advanced = false; + String _schedulePreview() { + final entry = Preferences.instance.getString(Preferences.scheduleEntry)?.trim(); + if (entry == null || entry.isEmpty) { + return AppLocalizations.of(context)!.scheduleUnsetLabel; + } + return entry.split('\n').first; + } + + Future _editScheduleEntry() async { + final controller = TextEditingController( + text: Preferences.instance.getString(Preferences.scheduleEntry) ?? _defaultScheduleEntry, + ); + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + scrollable: true, + title: Text(AppLocalizations.of(context)!.scheduleEntryLabel), + content: TextField( + controller: controller, + minLines: 3, + maxLines: 5, + keyboardType: TextInputType.multiline, + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.scheduleEntryHint, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancelButton), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: Text(AppLocalizations.of(context)!.saveButton), + ), + ], + ), + ); + if (result != null) { + final trimmed = result.trim(); + if (trimmed.isEmpty) { + messengerKey.currentState?.showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context)!.invalidValue)), + ); + return; + } + await Preferences.instance.setString(Preferences.scheduleEntry, trimmed); + await ScheduleService.sync(); + if (mounted) setState(() {}); + } + } + + Future _toggleSchedule(bool value) async { + if (value) { + final entry = Preferences.instance.getString(Preferences.scheduleEntry); + if (entry == null || entry.trim().isEmpty) { + await Preferences.instance.setString(Preferences.scheduleEntry, _defaultScheduleEntry); + } + } else { + await Preferences.instance.remove(Preferences.scheduleEntry); + await Preferences.instance.remove(Preferences.scheduleStart); + await Preferences.instance.remove(Preferences.scheduleStop); + } + await Preferences.instance.setBool(Preferences.scheduleEnabled, value); + await ScheduleService.sync(); + if (mounted) setState(() {}); + } + String _getAccuracyLabel(String? key) { return switch (key) { 'highest' => AppLocalizations.of(context)!.highestAccuracyLabel, @@ -161,6 +232,7 @@ class _SettingsScreenState extends State { Widget build(BuildContext context) { final isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest'; final distance = Preferences.instance.getInt(Preferences.distance); + final scheduleEnabled = Preferences.instance.getBool(Preferences.scheduleEnabled) ?? false; return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.settingsTitle), @@ -192,6 +264,18 @@ class _SettingsScreenState extends State { setState(() => advanced = value); }, ), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.scheduleEnabledLabel), + value: scheduleEnabled, + onChanged: _toggleSchedule, + ), + if (scheduleEnabled) + ListTile( + contentPadding: const EdgeInsets.only(left: 32, right: 16), + title: Text(AppLocalizations.of(context)!.scheduleEntryLabel), + subtitle: Text(_schedulePreview()), + onTap: _editScheduleEntry, + ), if (advanced) _buildListTile(AppLocalizations.of(context)!.fastestIntervalLabel, Preferences.fastestInterval, true), if (advanced)