feat: Implement location tracking service and haptic feedback for device advertising

This commit is contained in:
Janez T
2025-10-16 21:27:22 +02:00
parent c6c56f858d
commit 58d8ef0dae
18 changed files with 203 additions and 16 deletions

View File

@@ -1776,5 +1776,10 @@
"placeholders": {
"power": { "type": "int" }
}
},
"you": "You",
"@you": {
"description": "Label for the current user in message bubbles"
}
}

View File

@@ -590,5 +590,7 @@
"spreadingFactor": "Faktor širenja",
"codingRate": "Omjer kodiranja",
"txPowerDbm": "TX snaga (dBm)",
"maxPowerDbm": "Maks: {power} dBm"
"maxPowerDbm": "Maks: {power} dBm",
"you": "Ti"
}

View File

@@ -1898,6 +1898,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Max: {power} dBm'**
String maxPowerDbm(int power);
/// Label for the current user in message bubbles
///
/// In en, this message translates to:
/// **'You'**
String get you;
}
class _AppLocalizationsDelegate

View File

@@ -1035,4 +1035,7 @@ class AppLocalizationsEn extends AppLocalizations {
String maxPowerDbm(int power) {
return 'Max: $power dBm';
}
@override
String get you => 'You';
}

View File

@@ -1037,4 +1037,7 @@ class AppLocalizationsHr extends AppLocalizations {
String maxPowerDbm(int power) {
return 'Maks: $power dBm';
}
@override
String get you => 'Ti';
}

View File

@@ -1037,4 +1037,7 @@ class AppLocalizationsSl extends AppLocalizations {
String maxPowerDbm(int power) {
return 'Največ: $power dBm';
}
@override
String get you => 'Ti';
}

View File

@@ -590,5 +590,7 @@
"spreadingFactor": "Faktor razširitve",
"codingRate": "Razmerje kodiranja",
"txPowerDbm": "Izhodna moč (dBm)",
"maxPowerDbm": "Največ: {power} dBm"
"maxPowerDbm": "Največ: {power} dBm",
"you": "Ti"
}

View File

@@ -6,6 +6,7 @@ import 'contacts_provider.dart';
import 'messages_provider.dart';
import 'drawing_provider.dart';
import '../services/tile_cache_service.dart';
import '../services/location_tracking_service.dart';
import '../models/contact.dart';
import '../utils/drawing_message_parser.dart';
@@ -16,6 +17,7 @@ class AppProvider with ChangeNotifier {
final MessagesProvider messagesProvider;
final DrawingProvider drawingProvider;
final TileCacheService tileCacheService;
final LocationTrackingService locationTrackingService = LocationTrackingService();
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
@@ -29,6 +31,7 @@ class AppProvider with ChangeNotifier {
}) {
_setupCallbacks();
_initializeTileCache();
_initializeLocationTracking();
_isInitialized = true;
}
@@ -42,8 +45,39 @@ class AppProvider with ChangeNotifier {
}
}
/// Initialize location tracking service
Future<void> _initializeLocationTracking() async {
try {
// Initialize location tracking with BLE service
await locationTrackingService.initialize(connectionProvider.bleService);
// Setup callbacks
locationTrackingService.onPositionUpdate = (position) {
debugPrint('📍 [AppProvider] Position updated: ${position.latitude}, ${position.longitude}');
};
locationTrackingService.onBroadcastSent = (position) {
debugPrint('📡 [AppProvider] Position broadcast to mesh network');
};
locationTrackingService.onError = (error) {
debugPrint('❌ [AppProvider] Location tracking error: $error');
};
locationTrackingService.onTrackingStateChanged = (isTracking) {
debugPrint('🔄 [AppProvider] Location tracking state: ${isTracking ? "started" : "stopped"}');
};
debugPrint('✅ [AppProvider] Location tracking service initialized');
} catch (e) {
debugPrint('❌ [AppProvider] Error initializing location tracking: $e');
}
}
/// Setup callbacks between providers
void _setupCallbacks() {
// Monitor connection state changes to start/stop location tracking
connectionProvider.addListener(_handleConnectionStateChange);
// When a contact is received from BLE
connectionProvider.onContactReceived = (contact) {
// Pass device public key to filter out our own contact
@@ -339,6 +373,46 @@ class AppProvider with ChangeNotifier {
}
}
/// Handle connection state changes to manage location tracking
void _handleConnectionStateChange() {
final isConnected = connectionProvider.deviceInfo.isConnected;
final wasTracking = locationTrackingService.isTracking;
if (isConnected && !wasTracking) {
// Connection established - start location tracking
debugPrint('🔵 [AppProvider] BLE connected - starting location tracking');
_startLocationTracking();
} else if (!isConnected && wasTracking) {
// Connection lost - stop location tracking
debugPrint('🔴 [AppProvider] BLE disconnected - stopping location tracking');
_stopLocationTracking();
}
}
/// Start location tracking
Future<void> _startLocationTracking() async {
try {
final started = await locationTrackingService.startTracking();
if (started) {
debugPrint('✅ [AppProvider] Location tracking started successfully');
} else {
debugPrint('⚠️ [AppProvider] Failed to start location tracking');
}
} catch (e) {
debugPrint('❌ [AppProvider] Error starting location tracking: $e');
}
}
/// Stop location tracking
Future<void> _stopLocationTracking() async {
try {
await locationTrackingService.stopTracking();
debugPrint('✅ [AppProvider] Location tracking stopped');
} catch (e) {
debugPrint('❌ [AppProvider] Error stopping location tracking: $e');
}
}
/// Clear all data
void clearAllData() {
contactsProvider.clearContacts();

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:vibration/vibration.dart';
import '../providers/connection_provider.dart';
import '../providers/app_provider.dart';
import '../providers/messages_provider.dart';
@@ -765,7 +767,30 @@ class _HomeScreenState extends State<HomeScreen>
// CENTER: Broadcast button
const SizedBox(width: 8),
FilledButton(
onPressed: () => _advertiseDevice(context),
onPressed: () async {
// iOS: Use haptic feedback (always works)
// Android: Try vibration package for better control
try {
if (Theme.of(context).platform == TargetPlatform.iOS) {
// iOS: Try multiple haptic types for reliability
await HapticFeedback.lightImpact();
await Future.delayed(const Duration(milliseconds: 50));
await HapticFeedback.lightImpact();
} else {
// Android vibration
if (await Vibration.hasVibrator() ?? false) {
await Vibration.vibrate(duration: 50);
} else {
await HapticFeedback.mediumImpact();
}
}
} catch (e) {
// Fallback if anything fails
print('Haptic feedback error: $e');
await HapticFeedback.vibrate();
}
_advertiseDevice(context);
},
style: FilledButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,

View File

@@ -714,7 +714,7 @@ class _MessageBubble extends StatelessWidget {
// Get rich display name (with emoji if available)
final displayName = isOwnMessage
? 'You'
? AppLocalizations.of(context)!.you
: message.getRichDisplayName(senderContact);
// For sent direct messages, look up recipient contact

View File

@@ -53,7 +53,7 @@ class LocationTrackingService {
double maxDistanceMeters = 100.0;
/// Minimum time interval in seconds between broadcasts
int minTimeIntervalSeconds = 155;
int minTimeIntervalSeconds = 30;
/// GPS update distance filter for position stream
double gpsUpdateDistance = 10.0;
@@ -380,6 +380,18 @@ class LocationTrackingService {
return;
}
// CRITICAL: Enforce minimum time between broadcasts to prevent mesh flooding
// This protects the mesh network from being overwhelmed by position updates
if (_lastBroadcastTime != null) {
final timeSinceLastBroadcast = DateTime.now().difference(_lastBroadcastTime!);
if (timeSinceLastBroadcast.inSeconds < minTimeIntervalSeconds) {
debugPrint(
'⏸️ [LocationTracking] Skipping broadcast: only ${timeSinceLastBroadcast.inSeconds}s since last broadcast (minimum: ${minTimeIntervalSeconds}s)',
);
return;
}
}
try {
debugPrint('📤 [LocationTracking] Broadcasting position to mesh network');
@@ -389,10 +401,10 @@ class LocationTrackingService {
longitude: position.longitude,
);
// Send advertisement to mesh network
// Send advertisement to mesh network (only ONE advert per position update)
await _bleService!.sendSelfAdvert(floodMode: true);
// Update broadcast tracking
// Update broadcast tracking IMMEDIATELY to prevent duplicate broadcasts
_lastBroadcastPosition = position;
_lastBroadcastTime = DateTime.now();
@@ -402,6 +414,7 @@ class LocationTrackingService {
await prefs.setDouble(_prefKeyLastLon, position.longitude);
debugPrint('✅ [LocationTracking] Position broadcast successful');
debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
onBroadcastSent?.call(position);
} catch (e) {
debugPrint('❌ [LocationTracking] Failed to broadcast position: $e');
@@ -412,6 +425,9 @@ class LocationTrackingService {
/// Manually broadcast current location immediately
///
/// Useful for "Send Location Now" button functionality.
/// Note: Manual broadcasts bypass automatic throttling and can be sent anytime.
/// However, they still update the last broadcast time to maintain proper spacing
/// for subsequent automatic broadcasts.
Future<bool> broadcastLocationNow() async {
if (!_isInitialized || _bleService == null) {
onError?.call('Location tracking service not initialized');
@@ -431,7 +447,9 @@ class LocationTrackingService {
return false;
}
// Broadcast regardless of thresholds
debugPrint('📤 [LocationTracking] Manual broadcast requested');
// Broadcast regardless of automatic throttling thresholds
await _bleService!.setAdvertLatLon(
latitude: position.latitude,
longitude: position.longitude,
@@ -439,11 +457,12 @@ class LocationTrackingService {
await _bleService!.sendSelfAdvert(floodMode: true);
// Update broadcast tracking
// Update broadcast tracking to maintain proper spacing for automatic broadcasts
_lastBroadcastPosition = position;
_lastBroadcastTime = DateTime.now();
debugPrint('✅ [LocationTracking] Manual broadcast successful');
debugPrint(' Automatic broadcasts will resume after ${minTimeIntervalSeconds}s');
onBroadcastSent?.call(position);
return true;