Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
pissten marked this conversation as resolved.
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
Expand Down
64 changes: 64 additions & 0 deletions lib/configuration_service.dart
Original file line number Diff line number Diff line change
@@ -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<void> applyUri(Uri uri) async {
Expand All @@ -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<void> _applyStringParameter(
Expand Down Expand Up @@ -59,5 +63,65 @@ class ConfigurationService {
}
}
}

static Future<void> _applyScheduleParameters(
Map<String, String> 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<void> _applyServiceParameter(
Map<String, String> 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')}';
}
}

6 changes: 5 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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());
}
Expand Down
23 changes: 23 additions & 0 deletions lib/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -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<void> _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() {
Expand Down
44 changes: 44 additions & 0 deletions lib/schedule_service.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<void> _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);
}
}
}
84 changes: 84 additions & 0 deletions lib/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,8 +20,78 @@ class SettingsScreen extends StatefulWidget {
}

class _SettingsScreenState extends State<SettingsScreen> {
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<void> _editScheduleEntry() async {
final controller = TextEditingController(
text: Preferences.instance.getString(Preferences.scheduleEntry) ?? _defaultScheduleEntry,
);
final result = await showDialog<String>(
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<void> _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,
Expand Down Expand Up @@ -161,6 +232,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
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),
Expand Down Expand Up @@ -192,6 +264,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
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)
Expand Down