feat: add send location button to force immediate report

This commit is contained in:
fiatcode 2026-04-30 13:23:07 +07:00
parent 4eade880b1
commit cd34c8bbf3
No known key found for this signature in database
5 changed files with 92 additions and 31 deletions

View file

@ -62,6 +62,10 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
"getStatus" -> { "getStatus" -> {
result.success(getTrackingStatus()) result.success(getTrackingStatus())
} }
"reportLocation" -> {
reportLocationNow()
result.success(true)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -107,4 +111,11 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
"lastTimestamp" to prefs.getLong("last_timestamp", 0L) "lastTimestamp" to prefs.getLong("last_timestamp", 0L)
) )
} }
private fun reportLocationNow() {
val intent = android.content.Intent(context, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_REPORT
}
context?.startService(intent)
}
} }

View file

@ -49,6 +49,7 @@ class LocationTrackingService : Service() {
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isTracking = false private var isTracking = false
private var isNetworkAvailable = true private var isNetworkAvailable = true
private var bypassFilterOnce = false
private var currentConfig: TrackingConfig? = null private var currentConfig: TrackingConfig? = null
@ -77,6 +78,7 @@ class LocationTrackingService : Service() {
when (intent?.action) { when (intent?.action) {
ACTION_START -> startTracking() ACTION_START -> startTracking()
ACTION_STOP -> stopTracking() ACTION_STOP -> stopTracking()
ACTION_REPORT -> reportLocationNow()
} }
return START_STICKY return START_STICKY
} }
@ -144,6 +146,19 @@ class LocationTrackingService : Service() {
logEvent("INFO", "Tracking stopped") logEvent("INFO", "Tracking stopped")
} }
fun reportLocationNow() {
if (!isTracking) {
logEvent("ERROR", "Cannot report: tracking not active")
return
}
bypassFilterOnce = true
fusedLocationProvider.getLastLocation { location ->
location?.let {
onLocationReceived(it)
} ?: logEvent("ERROR", "No location available for report")
}
}
fun updateConfig(config: TrackingConfig) { fun updateConfig(config: TrackingConfig) {
currentConfig = config currentConfig = config
if (isTracking) { if (isTracking) {
@ -161,10 +176,11 @@ class LocationTrackingService : Service() {
angleFilter = config.angleFilter angleFilter = config.angleFilter
) )
if (!distanceFilterProcessor.shouldAccept(location, filterConfig)) { if (!bypassFilterOnce && !distanceFilterProcessor.shouldAccept(location, filterConfig)) {
logEvent("FILTERED", "Location filtered by distance/interval/angle") logEvent("FILTERED", "Location filtered by distance/interval/angle")
return return
} }
bypassFilterOnce = false
val traccarLocation = TraccarLocation( val traccarLocation = TraccarLocation(
timestamp = location.time, timestamp = location.time,
@ -367,6 +383,7 @@ class LocationTrackingService : Service() {
companion object { companion object {
const val ACTION_START = "com.traccar.traccar_client.ACTION_START" const val ACTION_START = "com.traccar.traccar_client.ACTION_START"
const val ACTION_STOP = "com.traccar.traccar_client.ACTION_STOP" const val ACTION_STOP = "com.traccar.traccar_client.ACTION_STOP"
const val ACTION_REPORT = "com.traccar.traccar_client.ACTION_REPORT"
private const val CHANNEL_ID = "traccar_tracking_channel" private const val CHANNEL_ID = "traccar_tracking_channel"
private const val NOTIFICATION_ID = 1 private const val NOTIFICATION_ID = 1
} }

View file

@ -55,6 +55,16 @@ class LocationBridge {
} }
} }
static Future<bool> reportLocation() async {
try {
final result = await _methodChannel.invokeMethod<bool>('reportLocation');
return result ?? false;
} on PlatformException catch (e) {
debugPrint('Failed to report location: ${e.message}');
return false;
}
}
static Stream<Map<String, dynamic>> get locationUpdates { static Stream<Map<String, dynamic>> get locationUpdates {
_locationStream ??= _eventChannel.receiveBroadcastStream().map( _locationStream ??= _eventChannel.receiveBroadcastStream().map(
(event) => Map<String, dynamic>.from(event as Map), (event) => Map<String, dynamic>.from(event as Map),

View file

@ -154,32 +154,61 @@ class _MainScreenState extends State<MainScreen> {
} }
Widget _buildActionButtons() { Widget _buildActionButtons() {
return Row( return Column(
children: [ children: [
Expanded( if (_isTracking)
child: ElevatedButton.icon( Padding(
onPressed: () { padding: const EdgeInsets.only(bottom: 16),
Navigator.push( child: SizedBox(
context, width: double.infinity,
MaterialPageRoute(builder: (_) => const SettingsScreen()), child: ElevatedButton.icon(
); onPressed: () async {
}, final success = await LocationBridge.reportLocation();
icon: const Icon(Icons.settings), if (mounted) {
label: const Text('Settings'), ScaffoldMessenger.of(context).showSnackBar(
), SnackBar(
), content: Text(
const SizedBox(width: 16), success
Expanded( ? 'Location reported'
child: ElevatedButton.icon( : 'Failed to report location',
onPressed: () { ),
Navigator.push( ),
context, );
MaterialPageRoute(builder: (_) => const StatusScreen()), }
); },
}, icon: const Icon(Icons.send),
icon: const Icon(Icons.history), label: const Text('Send Location'),
label: const Text('Status/Logs'), ),
),
), ),
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'),
),
),
],
), ),
], ],
); );

View file

@ -12,7 +12,6 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
late TextEditingController _serverUrlController; late TextEditingController _serverUrlController;
late TextEditingController _deviceIdController; late TextEditingController _deviceIdController;
late TextEditingController _passwordController;
late int _accuracy; late int _accuracy;
late int _distanceFilter; late int _distanceFilter;
late int _interval; late int _interval;
@ -25,7 +24,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
super.initState(); super.initState();
_serverUrlController = TextEditingController(text: Preferences.serverUrl); _serverUrlController = TextEditingController(text: Preferences.serverUrl);
_deviceIdController = TextEditingController(text: Preferences.deviceId); _deviceIdController = TextEditingController(text: Preferences.deviceId);
_passwordController = TextEditingController(text: Preferences.password);
_accuracy = Preferences.accuracy; _accuracy = Preferences.accuracy;
_distanceFilter = Preferences.distanceFilter; _distanceFilter = Preferences.distanceFilter;
_interval = Preferences.interval; _interval = Preferences.interval;
@ -38,14 +36,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
void dispose() { void dispose() {
_serverUrlController.dispose(); _serverUrlController.dispose();
_deviceIdController.dispose(); _deviceIdController.dispose();
_passwordController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _saveSettings() async { Future<void> _saveSettings() async {
await Preferences.setServerUrl(_serverUrlController.text); await Preferences.setServerUrl(_serverUrlController.text);
await Preferences.setDeviceId(_deviceIdController.text); await Preferences.setDeviceId(_deviceIdController.text);
await Preferences.setPassword(_passwordController.text);
await Preferences.setAccuracy(_accuracy); await Preferences.setAccuracy(_accuracy);
await Preferences.setDistanceFilter(_distanceFilter); await Preferences.setDistanceFilter(_distanceFilter);
await Preferences.setInterval(_interval); await Preferences.setInterval(_interval);
@ -56,7 +52,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
await LocationBridge.updateConfig({ await LocationBridge.updateConfig({
'serverUrl': _serverUrlController.text, 'serverUrl': _serverUrlController.text,
'deviceId': _deviceIdController.text, 'deviceId': _deviceIdController.text,
'password': _passwordController.text,
'accuracy': _accuracy, 'accuracy': _accuracy,
'distanceFilter': _distanceFilter, 'distanceFilter': _distanceFilter,
'interval': _interval, 'interval': _interval,
@ -82,7 +77,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSectionHeader('Server'), _buildSectionHeader('Server'),
_buildTextField('Server URL', _serverUrlController), _buildTextField('Server URL', _serverUrlController),
_buildTextField('Device ID', _deviceIdController), _buildTextField('Device ID', _deviceIdController),
_buildTextField('Password', _passwordController, obscure: true),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildSectionHeader('Location'), _buildSectionHeader('Location'),
_buildAccuracyDropdown(), _buildAccuracyDropdown(),