- 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
249 lines
7.2 KiB
Dart
249 lines
7.2 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:traccar_client/bridge/location_bridge.dart';
|
|
import 'package:traccar_client/preferences.dart';
|
|
import 'package:traccar_client/settings_screen.dart';
|
|
import 'package:traccar_client/status_screen.dart';
|
|
|
|
class MainScreen extends StatefulWidget {
|
|
const MainScreen({super.key});
|
|
|
|
@override
|
|
State<MainScreen> createState() => _MainScreenState();
|
|
}
|
|
|
|
class _MainScreenState extends State<MainScreen> {
|
|
StreamSubscription<Map<String, dynamic>>? _locationSubscription;
|
|
bool _isTracking = false;
|
|
String _lastLat = '--';
|
|
String _lastLon = '--';
|
|
String _lastSpeed = '--';
|
|
String _lastTime = '--';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initLocationStream();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_locationSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _initLocationStream() async {
|
|
final status = await LocationBridge.getStatus();
|
|
if (mounted) {
|
|
setState(() {
|
|
_isTracking = status?['isTracking'] == true;
|
|
final lat = status?['lastLatitude'];
|
|
final lon = status?['lastLongitude'];
|
|
if (lat != null && lat != 0.0) {
|
|
_lastLat = lat.toStringAsFixed(4);
|
|
}
|
|
if (lon != null && lon != 0.0) {
|
|
_lastLon = lon.toStringAsFixed(4);
|
|
}
|
|
final speed = status?['lastSpeed'];
|
|
if (speed != null) {
|
|
_lastSpeed = '${(speed * 3.6).toStringAsFixed(0)} km/h';
|
|
}
|
|
final timestamp = status?['lastTimestamp'];
|
|
if (timestamp != null) {
|
|
_lastTime = _formatTime(
|
|
DateTime.fromMillisecondsSinceEpoch(timestamp),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
_locationSubscription = LocationBridge.locationUpdates.listen((location) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_lastLat = location['latitude']?.toStringAsFixed(4) ?? '--';
|
|
_lastLon = location['longitude']?.toStringAsFixed(4) ?? '--';
|
|
_lastSpeed = location['speed'] != null
|
|
? '${(location['speed'] * 3.6).toStringAsFixed(0)} km/h'
|
|
: '--';
|
|
_lastTime = location['timestamp'] != null
|
|
? _formatTime(
|
|
DateTime.fromMillisecondsSinceEpoch(location['timestamp']),
|
|
)
|
|
: '--';
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
String _formatTime(DateTime dt) {
|
|
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
Future<void> _toggleTracking() async {
|
|
final previousState = _isTracking;
|
|
final newState = !_isTracking;
|
|
|
|
setState(() {
|
|
_isTracking = newState;
|
|
});
|
|
|
|
bool success;
|
|
if (newState) {
|
|
success = await LocationBridge.startTracking(config: {
|
|
'serverUrl': Preferences.serverUrl,
|
|
'deviceId': Preferences.deviceId,
|
|
'password': Preferences.password,
|
|
'accuracy': Preferences.accuracy,
|
|
'distanceFilter': Preferences.distanceFilter,
|
|
'interval': Preferences.interval,
|
|
'heartbeat': Preferences.heartbeat,
|
|
'offlineBuffer': Preferences.offlineBuffer,
|
|
'stopDetection': Preferences.stopDetection,
|
|
});
|
|
} else {
|
|
success = await LocationBridge.stopTracking();
|
|
}
|
|
|
|
if (!success && mounted) {
|
|
setState(() {
|
|
_isTracking = previousState;
|
|
});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(previousState ? 'Failed to stop tracking' : 'Failed to start tracking'),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Traccar Client'), centerTitle: true),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildStatusCard(),
|
|
const SizedBox(height: 24),
|
|
_buildTrackingToggle(),
|
|
const Spacer(),
|
|
_buildActionButtons(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusCard() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Last Location',
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInfoRow('Lat', _lastLat),
|
|
_buildInfoRow('Lon', _lastLon),
|
|
_buildInfoRow('Speed', _lastSpeed),
|
|
_buildInfoRow('Time', _lastTime),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label),
|
|
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTrackingToggle() {
|
|
return SwitchListTile(
|
|
title: Text(_isTracking ? 'Tracking: ON' : 'Tracking: OFF'),
|
|
subtitle: Text(
|
|
_isTracking ? 'Location updates active' : 'Tap to start tracking',
|
|
),
|
|
value: _isTracking,
|
|
onChanged: (_) => _toggleTracking(),
|
|
activeTrackColor: Colors.green,
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButtons() {
|
|
return Column(
|
|
children: [
|
|
if (_isTracking)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () async {
|
|
final success = await LocationBridge.reportLocation();
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
success
|
|
? 'Location sent to server'
|
|
: 'Failed - check logs',
|
|
),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
icon: const Icon(Icons.send),
|
|
label: const Text('Send Location'),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.settings),
|
|
label: const Text('Settings'),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const StatusScreen()),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.history),
|
|
label: const Text('Status/Logs'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|