From efbddc883151e2fa1b50fab8612e5474b03b8f19 Mon Sep 17 00:00:00 2001 From: Janez T Date: Mon, 16 Mar 2026 20:37:04 +0100 Subject: [PATCH] Add wakelock plus package --- lib/main.dart | 1 + lib/providers/sensors_provider.dart | 50 +++++++++---- lib/screens/home_screen.dart | 4 ++ lib/screens/map_tab.dart | 2 +- .../profile_workspace_coordinator.dart | 4 ++ test/providers/sensors_provider_test.dart | 70 +++++++++++++++++++ .../profile_workspace_coordinator_test.dart | 10 +++ 7 files changed, 128 insertions(+), 13 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index da425e6..3c2bd3e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -365,6 +365,7 @@ class _MeshCoreSarAppState extends State { connectionProvider: connectionProvider, contactsProvider: contactsProvider, messagesProvider: messagesProvider, + sensorsProvider: context.read(), mapProvider: mapProvider, drawingProvider: context.read(), channelsProvider: context.read(), diff --git a/lib/providers/sensors_provider.dart b/lib/providers/sensors_provider.dart index f9a08e1..4e30cc1 100644 --- a/lib/providers/sensors_provider.dart +++ b/lib/providers/sensors_provider.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/contact.dart'; +import '../services/profiles_feature_service.dart'; import '../widgets/sensors/sensor_telemetry_card.dart'; import 'connection_provider.dart'; import 'contacts_provider.dart'; @@ -68,6 +69,8 @@ class SensorsProvider with ChangeNotifier { unawaited(_loadWatchedSensors()); } + String _key(String baseKey) => ProfileStorageScope.scopedKey(baseKey); + List get watchedSensorKeys => List.unmodifiable(_watchedSensorKeys); bool get isLoaded => _isLoaded; bool get isRefreshingAll => _isRefreshingAll; @@ -78,20 +81,26 @@ class SensorsProvider with ChangeNotifier { Future _loadWatchedSensors() async { try { final prefs = await SharedPreferences.getInstance(); - final stored = prefs.getStringList(_watchedSensorsKey) ?? []; - final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey); - final storedSpansJson = prefs.getString(_fieldSpanKey); - final storedLabelsJson = prefs.getString(_metricLabelKey); - final storedOrderJson = prefs.getString(_metricOrderKey); - final storedAutoRefreshJson = prefs.getString(_autoRefreshMinutesKey); + final stored = + prefs.getStringList(_key(_watchedSensorsKey)) ?? []; + final storedMetricsJson = prefs.getString(_key(_visibleSensorMetricsKey)); + final storedSpansJson = prefs.getString(_key(_fieldSpanKey)); + final storedLabelsJson = prefs.getString(_key(_metricLabelKey)); + final storedOrderJson = prefs.getString(_key(_metricOrderKey)); + final storedAutoRefreshJson = prefs.getString( + _key(_autoRefreshMinutesKey), + ); _watchedSensorKeys ..clear() ..addAll(stored); + _refreshStates.clear(); + _refreshStateUpdatedAt.clear(); _visibleFieldsBySensor.clear(); _fieldSpansBySensor.clear(); _metricLabelsBySensor.clear(); _metricOrderBySensor.clear(); _autoRefreshMinutesBySensor.clear(); + _lastRefreshAttemptAt.clear(); if (storedMetricsJson != null && storedMetricsJson.isNotEmpty) { final decoded = jsonDecode(storedMetricsJson) as Map; for (final entry in decoded.entries) { @@ -159,10 +168,15 @@ class SensorsProvider with ChangeNotifier { } } + Future reloadProfileScopedState() async { + _isLoaded = false; + await _loadWatchedSensors(); + } + Future _persistWatchedSensors() async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList(_watchedSensorsKey, _watchedSensorKeys); + await prefs.setStringList(_key(_watchedSensorsKey), _watchedSensorKeys); } catch (e) { debugPrint('Error saving watched sensors: $e'); } @@ -175,7 +189,10 @@ class SensorsProvider with ChangeNotifier { for (final entry in _visibleFieldsBySensor.entries) { encoded[entry.key] = entry.value.toList(); } - await prefs.setString(_visibleSensorMetricsKey, jsonEncode(encoded)); + await prefs.setString( + _key(_visibleSensorMetricsKey), + jsonEncode(encoded), + ); } catch (e) { debugPrint('Error saving visible sensor metrics: $e'); } @@ -184,7 +201,10 @@ class SensorsProvider with ChangeNotifier { Future _persistFieldSpans() async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_fieldSpanKey, jsonEncode(_fieldSpansBySensor)); + await prefs.setString( + _key(_fieldSpanKey), + jsonEncode(_fieldSpansBySensor), + ); } catch (e) { debugPrint('Error saving sensor field spans: $e'); } @@ -193,7 +213,10 @@ class SensorsProvider with ChangeNotifier { Future _persistMetricLabels() async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_metricLabelKey, jsonEncode(_metricLabelsBySensor)); + await prefs.setString( + _key(_metricLabelKey), + jsonEncode(_metricLabelsBySensor), + ); } catch (e) { debugPrint('Error saving sensor metric labels: $e'); } @@ -202,7 +225,10 @@ class SensorsProvider with ChangeNotifier { Future _persistMetricOrder() async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_metricOrderKey, jsonEncode(_metricOrderBySensor)); + await prefs.setString( + _key(_metricOrderKey), + jsonEncode(_metricOrderBySensor), + ); } catch (e) { debugPrint('Error saving sensor metric order: $e'); } @@ -212,7 +238,7 @@ class SensorsProvider with ChangeNotifier { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString( - _autoRefreshMinutesKey, + _key(_autoRefreshMinutesKey), jsonEncode(_autoRefreshMinutesBySensor), ); } catch (e) { diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index dbd67ec..677db94 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -597,6 +597,9 @@ class _HomeScreenState extends State } context.watch(); + final activeProfileId = context.select( + (manager) => manager.activeProfileId, + ); final enabledTabs = _enabledTabs; final isMapTabActive = _currentTab == _HomeTab.map; @@ -844,6 +847,7 @@ class _HomeScreenState extends State ); case _HomeTab.map: return MapTab( + key: ValueKey('map:$activeProfileId'), onFullscreenChanged: (isFullscreen) { setState(() { _isMapFullscreen = isFullscreen; diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index e2921ed..5c1163e 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -1965,7 +1965,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ? _customMapMaxZoom : _currentLayer.maxZoom, cameraConstraint: isCustomMapMode - ? CameraConstraint.contain( + ? CameraConstraint.containCenter( bounds: customMapConfig.displayBounds, ) : const CameraConstraint.unconstrained(), diff --git a/lib/services/profile_workspace_coordinator.dart b/lib/services/profile_workspace_coordinator.dart index 4e65f6e..90846f0 100644 --- a/lib/services/profile_workspace_coordinator.dart +++ b/lib/services/profile_workspace_coordinator.dart @@ -13,6 +13,7 @@ import '../providers/contacts_provider.dart'; import '../providers/drawing_provider.dart'; import '../providers/map_provider.dart'; import '../providers/messages_provider.dart'; +import '../providers/sensors_provider.dart'; import 'app_config_snapshot_service.dart'; import 'contact_storage_service.dart'; import 'device_config_applicator.dart'; @@ -27,6 +28,7 @@ class ProfileWorkspaceCoordinator { required this.connectionProvider, required this.contactsProvider, required this.messagesProvider, + required this.sensorsProvider, required this.mapProvider, required this.drawingProvider, required this.channelsProvider, @@ -51,6 +53,7 @@ class ProfileWorkspaceCoordinator { final ConnectionProvider connectionProvider; final ContactsProvider contactsProvider; final MessagesProvider messagesProvider; + final SensorsProvider sensorsProvider; final MapProvider mapProvider; final DrawingProvider drawingProvider; final ChannelsProvider channelsProvider; @@ -280,6 +283,7 @@ class ProfileWorkspaceCoordinator { namespace: ProfileStorageScope.effectiveNamespace, devicePublicKey: connectionProvider.deviceInfo.publicKey, ); + await sensorsProvider.reloadProfileScopedState(); await drawingProvider.reloadProfileScopedState(); await mapProvider.reloadProfileScopedState(); await appProvider.reloadProfileScopedSettings(); diff --git a/test/providers/sensors_provider_test.dart b/test/providers/sensors_provider_test.dart index f74a107..6f0c653 100644 --- a/test/providers/sensors_provider_test.dart +++ b/test/providers/sensors_provider_test.dart @@ -6,6 +6,7 @@ import 'package:meshcore_sar_app/models/device_info.dart'; import 'package:meshcore_sar_app/providers/connection_provider.dart'; import 'package:meshcore_sar_app/providers/contacts_provider.dart'; import 'package:meshcore_sar_app/providers/sensors_provider.dart'; +import 'package:meshcore_sar_app/services/profiles_feature_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class _FakeContactsProvider extends ContactsProvider { @@ -50,6 +51,13 @@ class _FakeConnectionProvider extends ConnectionProvider { void main() { TestWidgetsFlutterBinding.ensureInitialized(); + setUp(() { + ProfileStorageScope.setScope( + profilesEnabled: false, + activeProfileId: 'default', + ); + }); + Future waitUntilLoaded(SensorsProvider provider) async { for (var i = 0; i < 20 && !provider.isLoaded; i++) { await Future.delayed(Duration.zero); @@ -194,6 +202,68 @@ void main() { expect(reloadedProvider.autoRefreshMinutesFor(contact.publicKeyHex), 1440); }); + test('watched sensors and preferences are isolated per profile', () async { + SharedPreferences.setMockInitialValues({}); + final contact = buildSensorContact(); + + ProfileStorageScope.setScope( + profilesEnabled: false, + activeProfileId: 'default', + ); + final defaultProvider = SensorsProvider(); + await waitUntilLoaded(defaultProvider); + await defaultProvider.addSensor(contact); + await defaultProvider.setMetricLabel( + contact.publicKeyHex, + 'voltage', + 'Default Voltage', + ); + + ProfileStorageScope.setScope( + profilesEnabled: true, + activeProfileId: 'alpha', + ); + final customProvider = SensorsProvider(); + await waitUntilLoaded(customProvider); + + expect(customProvider.watchedSensorKeys, isEmpty); + expect( + customProvider.labelOverrideFor(contact.publicKeyHex, 'voltage'), + isNull, + ); + + await customProvider.addSensor(contact); + await customProvider.setMetricLabel( + contact.publicKeyHex, + 'voltage', + 'Alpha Voltage', + ); + + ProfileStorageScope.setScope( + profilesEnabled: false, + activeProfileId: 'default', + ); + final reloadedDefaultProvider = SensorsProvider(); + await waitUntilLoaded(reloadedDefaultProvider); + expect(reloadedDefaultProvider.watchedSensorKeys, [contact.publicKeyHex]); + expect( + reloadedDefaultProvider.labelOverrideFor(contact.publicKeyHex, 'voltage'), + 'Default Voltage', + ); + + ProfileStorageScope.setScope( + profilesEnabled: true, + activeProfileId: 'alpha', + ); + final reloadedCustomProvider = SensorsProvider(); + await waitUntilLoaded(reloadedCustomProvider); + expect(reloadedCustomProvider.watchedSensorKeys, [contact.publicKeyHex]); + expect( + reloadedCustomProvider.labelOverrideFor(contact.publicKeyHex, 'voltage'), + 'Alpha Voltage', + ); + }); + test( 'unsupported auto refresh minutes normalize to nearest option', () async { diff --git a/test/services/profile_workspace_coordinator_test.dart b/test/services/profile_workspace_coordinator_test.dart index d49ab54..e4f1cb8 100644 --- a/test/services/profile_workspace_coordinator_test.dart +++ b/test/services/profile_workspace_coordinator_test.dart @@ -13,6 +13,7 @@ import 'package:meshcore_sar_app/providers/contacts_provider.dart'; import 'package:meshcore_sar_app/providers/drawing_provider.dart'; import 'package:meshcore_sar_app/providers/map_provider.dart'; import 'package:meshcore_sar_app/providers/messages_provider.dart'; +import 'package:meshcore_sar_app/providers/sensors_provider.dart'; import 'package:meshcore_sar_app/services/app_config_snapshot_service.dart'; import 'package:meshcore_sar_app/services/contact_storage_service.dart'; import 'package:meshcore_sar_app/services/device_config_applicator.dart'; @@ -196,6 +197,7 @@ ProfileWorkspaceCoordinator _buildCoordinator({ connectionProvider: connectionProvider ?? _FakeConnectionProvider(), contactsProvider: _FakeContactsProvider(), messagesProvider: _FakeMessagesProvider(), + sensorsProvider: _FakeSensorsProvider(), mapProvider: _FakeMapProvider(), drawingProvider: _FakeDrawingProvider(), channelsProvider: _FakeChannelsProvider(), @@ -319,6 +321,14 @@ class _FakeDrawingProvider implements DrawingProvider { dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } +class _FakeSensorsProvider implements SensorsProvider { + @override + Future reloadProfileScopedState() async {} + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + class _FakeChannelsProvider implements ChannelsProvider { @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);