- Send config to native before starting tracking (SharedPreferences sync) - Register ConnectivityReceiver for network state detection - Fix Settings number fields using TextEditingControllers - Cancel stream subscription on widget dispose (memory leak) - Replace WorkManager with AlarmManager for heartbeat - Add log cleanup for logs older than 24 hours - Change HTTP method from GET to POST These fixes resolve the 'device always offline' issue where: 1. Config was not being sent to native service 2. ConnectivityReceiver was never registered 3. Settings number fields were not saving 4. Heartbeat never fired due to WorkManager process isolation 5. Server expected POST not GET
163 lines
5.5 KiB
Dart
163 lines
5.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:traccar_client/bridge/location_bridge.dart';
|
|
import 'package:traccar_client/preferences.dart';
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
late TextEditingController _serverUrlController;
|
|
late TextEditingController _deviceIdController;
|
|
late TextEditingController _distanceFilterController;
|
|
late TextEditingController _intervalController;
|
|
late TextEditingController _heartbeatController;
|
|
late int _accuracy;
|
|
late bool _offlineBuffer;
|
|
late bool _stopDetection;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_serverUrlController = TextEditingController(text: Preferences.serverUrl);
|
|
_deviceIdController = TextEditingController(text: Preferences.deviceId);
|
|
_distanceFilterController = TextEditingController(text: Preferences.distanceFilter.toString());
|
|
_intervalController = TextEditingController(text: Preferences.interval.toString());
|
|
_heartbeatController = TextEditingController(text: Preferences.heartbeat.toString());
|
|
_accuracy = Preferences.accuracy;
|
|
_offlineBuffer = Preferences.offlineBuffer;
|
|
_stopDetection = Preferences.stopDetection;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_serverUrlController.dispose();
|
|
_deviceIdController.dispose();
|
|
_distanceFilterController.dispose();
|
|
_intervalController.dispose();
|
|
_heartbeatController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _saveSettings() async {
|
|
final distanceFilter = int.tryParse(_distanceFilterController.text) ?? 75;
|
|
final interval = int.tryParse(_intervalController.text) ?? 300;
|
|
final heartbeat = int.tryParse(_heartbeatController.text) ?? 60;
|
|
|
|
await Preferences.setServerUrl(_serverUrlController.text);
|
|
await Preferences.setDeviceId(_deviceIdController.text);
|
|
await Preferences.setAccuracy(_accuracy);
|
|
await Preferences.setDistanceFilter(distanceFilter);
|
|
await Preferences.setInterval(interval);
|
|
await Preferences.setHeartbeat(heartbeat);
|
|
await Preferences.setOfflineBuffer(_offlineBuffer);
|
|
await Preferences.setStopDetection(_stopDetection);
|
|
|
|
await LocationBridge.updateConfig({
|
|
'serverUrl': _serverUrlController.text,
|
|
'deviceId': _deviceIdController.text,
|
|
'accuracy': _accuracy,
|
|
'distanceFilter': distanceFilter,
|
|
'interval': interval,
|
|
'heartbeat': heartbeat,
|
|
'offlineBuffer': _offlineBuffer,
|
|
'stopDetection': _stopDetection,
|
|
});
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('Settings saved')));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Settings')),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
_buildSectionHeader('Server'),
|
|
_buildTextField('Server URL', _serverUrlController),
|
|
_buildTextField('Device ID', _deviceIdController),
|
|
const SizedBox(height: 16),
|
|
_buildSectionHeader('Location'),
|
|
_buildAccuracyDropdown(),
|
|
_buildNumberField('Distance Filter (m)', _distanceFilterController, 0, 1000),
|
|
_buildNumberField('Update Interval (s)', _intervalController, 30, 3600),
|
|
_buildNumberField('Heartbeat (s)', _heartbeatController, 60, 3600),
|
|
const SizedBox(height: 16),
|
|
_buildSectionHeader('Advanced'),
|
|
SwitchListTile(
|
|
title: const Text('Offline Buffering'),
|
|
subtitle: const Text('Queue locations when offline'),
|
|
value: _offlineBuffer,
|
|
onChanged: (v) => setState(() => _offlineBuffer = v),
|
|
),
|
|
SwitchListTile(
|
|
title: const Text('Stop Detection'),
|
|
subtitle: const Text('Auto-stop when stationary'),
|
|
value: _stopDetection,
|
|
onChanged: (v) => setState(() => _stopDetection = v),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: _saveSettings,
|
|
child: const Text('Save Settings'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
|
child: Text(
|
|
title,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextField(
|
|
String label,
|
|
TextEditingController controller, {
|
|
bool obscure = false,
|
|
}) {
|
|
return TextField(
|
|
controller: controller,
|
|
decoration: InputDecoration(labelText: label),
|
|
obscureText: obscure,
|
|
);
|
|
}
|
|
|
|
Widget _buildAccuracyDropdown() {
|
|
return DropdownButtonFormField<int>(
|
|
// ignore: deprecated_member_use
|
|
value: _accuracy,
|
|
decoration: const InputDecoration(labelText: 'Accuracy'),
|
|
items: const [
|
|
DropdownMenuItem(value: 0, child: Text('High')),
|
|
DropdownMenuItem(value: 1, child: Text('High Accuracy')),
|
|
DropdownMenuItem(value: 2, child: Text('Balanced')),
|
|
DropdownMenuItem(value: 3, child: Text('Low')),
|
|
],
|
|
onChanged: (v) => setState(() => _accuracy = v!),
|
|
);
|
|
}
|
|
|
|
Widget _buildNumberField(String label, TextEditingController controller, int min, int max) {
|
|
return TextField(
|
|
controller: controller,
|
|
decoration: InputDecoration(labelText: label),
|
|
keyboardType: TextInputType.number,
|
|
);
|
|
}
|
|
}
|