feat: add background services and map status management

This commit is contained in:
Nedifinita
2025-09-24 23:36:55 +08:00
parent 10825171fd
commit 23ab5ec746
10 changed files with 694 additions and 26 deletions

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation"/>
@@ -14,6 +15,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<application <application
@@ -47,6 +49,15 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- 前台服务配置 -->
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="connectedDevice|dataSync"
android:exported="false"
android:stopWithTask="false"
android:enabled="true"
tools:replace="android:exported"/>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

View File

@@ -1,14 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:lbjconsole/screens/main_screen.dart'; import 'package:lbjconsole/screens/main_screen.dart';
import 'package:lbjconsole/util/train_type_util.dart'; import 'package:lbjconsole/util/train_type_util.dart';
import 'package:lbjconsole/util/loco_info_util.dart'; import 'package:lbjconsole/util/loco_info_util.dart';
import 'package:lbjconsole/util/loco_type_util.dart'; import 'package:lbjconsole/util/loco_type_util.dart';
import 'package:lbjconsole/services/loco_type_service.dart'; import 'package:lbjconsole/services/loco_type_service.dart';
import 'package:lbjconsole/services/database_service.dart'; import 'package:lbjconsole/services/database_service.dart';
import 'package:lbjconsole/services/background_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await _initializeNotifications();
await BackgroundService.initialize();
await Future.wait([ await Future.wait([
TrainTypeUtil.initialize(), TrainTypeUtil.initialize(),
LocoInfoUtil.initialize(), LocoInfoUtil.initialize(),
@@ -18,6 +24,19 @@ void main() async {
runApp(const LBJReceiverApp()); runApp(const LBJReceiverApp());
} }
Future<void> _initializeNotifications() async {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
class LBJReceiverApp extends StatelessWidget { class LBJReceiverApp extends StatelessWidget {
const LBJReceiverApp({super.key}); const LBJReceiverApp({super.key});

65
lib/models/map_state.dart Normal file
View File

@@ -0,0 +1,65 @@
class MapState {
final double zoom;
final double centerLat;
final double centerLng;
final double bearing;
MapState({
required this.zoom,
required this.centerLat,
required this.centerLng,
required this.bearing,
});
Map<String, dynamic> toJson() {
return {
'zoom': zoom,
'centerLat': centerLat,
'centerLng': centerLng,
'bearing': bearing,
};
}
factory MapState.fromJson(Map<String, dynamic> json) {
return MapState(
zoom: json['zoom']?.toDouble() ?? 10.0,
centerLat: json['centerLat']?.toDouble() ?? 39.9042,
centerLng: json['centerLng']?.toDouble() ?? 116.4074,
bearing: json['bearing']?.toDouble() ?? 0.0,
);
}
MapState copyWith({
double? zoom,
double? centerLat,
double? centerLng,
double? bearing,
}) {
return MapState(
zoom: zoom ?? this.zoom,
centerLat: centerLat ?? this.centerLat,
centerLng: centerLng ?? this.centerLng,
bearing: bearing ?? this.bearing,
);
}
@override
String toString() {
return 'MapState(zoom: ' + zoom.toString() + ', centerLat: ' + centerLat.toString() + ', centerLng: ' + centerLng.toString() + ', bearing: ' + bearing.toString() + ')';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is MapState &&
other.zoom == zoom &&
other.centerLat == centerLat &&
other.centerLng == centerLng &&
other.bearing == bearing;
}
@override
int get hashCode {
return zoom.hashCode ^ centerLat.hashCode ^ centerLng.hashCode ^ bearing.hashCode;
}
}

View File

@@ -7,6 +7,8 @@ import '../models/merged_record.dart';
import '../services/database_service.dart'; import '../services/database_service.dart';
import '../models/train_record.dart'; import '../models/train_record.dart';
import '../services/merge_service.dart'; import '../services/merge_service.dart';
import '../models/map_state.dart';
import '../services/map_state_service.dart';
class HistoryScreen extends StatefulWidget { class HistoryScreen extends StatefulWidget {
final Function(bool isEditing) onEditModeChanged; final Function(bool isEditing) onEditModeChanged;
@@ -53,7 +55,6 @@ class HistoryScreenState extends State<HistoryScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadRecords();
_scrollController.addListener(() { _scrollController.addListener(() {
if (_scrollController.position.atEdge) { if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0) { if (_scrollController.position.pixels == 0) {
@@ -63,6 +64,9 @@ class HistoryScreenState extends State<HistoryScreen> {
if (_isAtTop) setState(() => _isAtTop = false); if (_isAtTop) setState(() => _isAtTop = false);
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) loadRecords();
});
} }
@override @override
@@ -72,7 +76,6 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
Future<void> loadRecords({bool scrollToTop = true}) async { Future<void> loadRecords({bool scrollToTop = true}) async {
if (mounted) setState(() => _isLoading = true);
try { try {
final allRecords = await DatabaseService.instance.getAllRecords(); final allRecords = await DatabaseService.instance.getAllRecords();
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {}; final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
@@ -80,15 +83,22 @@ class HistoryScreenState extends State<HistoryScreen> {
final items = MergeService.getMixedList(allRecords, _mergeSettings); final items = MergeService.getMixedList(allRecords, _mergeSettings);
if (mounted) { if (mounted) {
setState(() { final hasDataChanged = _hasDataChanged(items);
_displayItems.clear();
_displayItems.addAll(items); if (hasDataChanged) {
_isLoading = false; setState(() {
}); _displayItems.clear();
if (scrollToTop && (_isAtTop) && _scrollController.hasClients) { _displayItems.addAll(items);
_scrollController.animateTo(0.0, _isLoading = false;
duration: const Duration(milliseconds: 300), });
curve: Curves.easeOut);
if (scrollToTop && _isAtTop && _scrollController.hasClients) {
_scrollController.jumpTo(0.0);
}
} else {
if (_isLoading) {
setState(() => _isLoading = false);
}
} }
} }
} catch (e) { } catch (e) {
@@ -96,9 +106,63 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
} }
Future<void> addNewRecord(TrainRecord newRecord) async {
try {
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
_mergeSettings = MergeSettings.fromMap(settingsMap);
final isNewRecord = !_displayItems.any((item) {
if (item is TrainRecord) {
return item.uniqueId == newRecord.uniqueId;
} else if (item is MergedTrainRecord) {
return item.records.any((r) => r.uniqueId == newRecord.uniqueId);
}
return false;
});
if (!isNewRecord) return;
if (mounted) {
final allRecords = await DatabaseService.instance.getAllRecords();
final items = MergeService.getMixedList(allRecords, _mergeSettings);
setState(() {
_displayItems.clear();
_displayItems.addAll(items);
});
if (_isAtTop && _scrollController.hasClients) {
_scrollController.jumpTo(0.0);
}
}
} catch (e) {
print('添加新纪录失败: $e');
}
}
bool _hasDataChanged(List<Object> newItems) {
if (_displayItems.length != newItems.length) return true;
for (int i = 0; i < _displayItems.length; i++) {
final oldItem = _displayItems[i];
final newItem = newItems[i];
if (oldItem.runtimeType != newItem.runtimeType) return true;
if (oldItem is TrainRecord && newItem is TrainRecord) {
if (oldItem.uniqueId != newItem.uniqueId) return true;
} else if (oldItem is MergedTrainRecord && newItem is MergedTrainRecord) {
if (oldItem.groupKey != newItem.groupKey) return true;
if (oldItem.records.length != newItem.records.length) return true;
}
}
return false;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading && _displayItems.isEmpty) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (_displayItems.isEmpty) { if (_displayItems.isEmpty) {
@@ -197,7 +261,7 @@ class HistoryScreenState extends State<HistoryScreen> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildExpandedMapForAll(mergedRecord.records), _buildExpandedMapForAll(mergedRecord.records, mergedRecord.groupKey),
const Divider(color: Colors.white24, height: 24), const Divider(color: Colors.white24, height: 24),
...mergedRecord.records.map((record) => _buildSubRecordItem( ...mergedRecord.records.map((record) => _buildSubRecordItem(
record, mergedRecord.latestRecord, _mergeSettings.groupBy)), record, mergedRecord.latestRecord, _mergeSettings.groupBy)),
@@ -353,7 +417,7 @@ class HistoryScreenState extends State<HistoryScreen> {
return parts.join(' '); return parts.join(' ');
} }
Widget _buildExpandedMapForAll(List<TrainRecord> records) { Widget _buildExpandedMapForAll(List<TrainRecord> records, String groupKey) {
final positions = records final positions = records
.map((record) => _parsePosition(record.positionInfo)) .map((record) => _parsePosition(record.positionInfo))
.whereType<LatLng>() .whereType<LatLng>()
@@ -410,6 +474,7 @@ class HistoryScreenState extends State<HistoryScreen> {
positions: positions, positions: positions,
center: bounds.center, center: bounds.center,
zoom: zoomLevel, zoom: zoomLevel,
groupKey: groupKey,
)) ))
]); ]);
} }
@@ -664,6 +729,7 @@ class HistoryScreenState extends State<HistoryScreen> {
key: ValueKey('map_${mapId}_$zoomLevel'), key: ValueKey('map_${mapId}_$zoomLevel'),
position: position, position: position,
zoom: zoomLevel, zoom: zoomLevel,
recordId: record.uniqueId,
)) ))
]); ]);
} }
@@ -803,11 +869,13 @@ _BoundaryBox _calculateBoundaryBoxIsolate(List<LatLng> positions) {
class _DelayedMapWithMarker extends StatefulWidget { class _DelayedMapWithMarker extends StatefulWidget {
final LatLng position; final LatLng position;
final double zoom; final double zoom;
final String recordId;
const _DelayedMapWithMarker({ const _DelayedMapWithMarker({
Key? key, Key? key,
required this.position, required this.position,
required this.zoom, required this.zoom,
required this.recordId,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -815,11 +883,90 @@ class _DelayedMapWithMarker extends StatefulWidget {
} }
class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> { class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
late final MapController _mapController;
late final String _mapKey;
bool _isInitializing = true;
@override
void initState() {
super.initState();
_mapController = MapController();
_mapKey = MapStateService.instance.getSingleRecordMapKey(widget.recordId);
_initializeMapState();
}
Future<void> _initializeMapState() async {
final savedState = await MapStateService.instance.getMapState(_mapKey);
if (savedState != null && mounted) {
_mapController.move(
LatLng(savedState.centerLat, savedState.centerLng),
savedState.zoom,
);
if (savedState.bearing != 0.0) {
_mapController.rotate(savedState.bearing);
}
}
setState(() {
_isInitializing = false;
});
}
void _onCameraMove() {
if (_isInitializing) return;
final camera = _mapController.camera;
final state = MapState(
zoom: camera.zoom,
centerLat: camera.center.latitude,
centerLng: camera.center.longitude,
bearing: camera.rotation,
);
MapStateService.instance.saveMapState(_mapKey, state);
}
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isInitializing) {
return FlutterMap(
options: MapOptions(
initialCenter: widget.position,
initialZoom: widget.zoom,
onPositionChanged: (position, hasGesture) => _onCameraMove(),
),
mapController: _mapController,
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'org.noxylva.lbjconsole'),
MarkerLayer(markers: [
Marker(
point: widget.position,
width: 40,
height: 40,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white, width: 2)),
child:
const Icon(Icons.train, color: Colors.white, size: 20)))
])
],
);
}
return FlutterMap( return FlutterMap(
options: options: MapOptions(
MapOptions(initialCenter: widget.position, initialZoom: widget.zoom), onPositionChanged: (position, hasGesture) => _onCameraMove(),
),
mapController: _mapController,
children: [ children: [
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
@@ -846,12 +993,14 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
final List<LatLng> positions; final List<LatLng> positions;
final LatLng center; final LatLng center;
final double zoom; final double zoom;
final String groupKey;
const _DelayedMultiMarkerMap({ const _DelayedMultiMarkerMap({
Key? key, Key? key,
required this.positions, required this.positions,
required this.center, required this.center,
required this.zoom, required this.zoom,
required this.groupKey,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -859,15 +1008,65 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
} }
class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> { class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
late final MapController _mapController;
late final String _mapKey;
bool _isInitializing = true;
@override
void initState() {
super.initState();
_mapController = MapController();
_mapKey = MapStateService.instance.getMergedRecordMapKey(widget.groupKey);
_initializeMapState();
}
Future<void> _initializeMapState() async {
final savedState = await MapStateService.instance.getMapState(_mapKey);
if (savedState != null && mounted) {
_mapController.move(
LatLng(savedState.centerLat, savedState.centerLng),
savedState.zoom,
);
if (savedState.bearing != 0.0) {
_mapController.rotate(savedState.bearing);
}
} else if (mounted) {
_mapController.move(widget.center, widget.zoom);
}
setState(() {
_isInitializing = false;
});
}
void _onCameraMove() {
if (_isInitializing) return;
final camera = _mapController.camera;
final state = MapState(
zoom: camera.zoom,
centerLat: camera.center.latitude,
centerLng: camera.center.longitude,
bearing: camera.rotation,
);
MapStateService.instance.saveMapState(_mapKey, state);
}
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlutterMap( return FlutterMap(
options: MapOptions( options: MapOptions(
initialCenter: widget.center, onPositionChanged: (position, hasGesture) => _onCameraMove(),
initialZoom: widget.zoom,
minZoom: 5, minZoom: 5,
maxZoom: 18, maxZoom: 18,
), ),
mapController: _mapController,
children: [ children: [
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',

View File

@@ -10,6 +10,7 @@ import 'package:lbjconsole/screens/settings_screen.dart';
import 'package:lbjconsole/services/ble_service.dart'; import 'package:lbjconsole/services/ble_service.dart';
import 'package:lbjconsole/services/database_service.dart'; import 'package:lbjconsole/services/database_service.dart';
import 'package:lbjconsole/services/notification_service.dart'; import 'package:lbjconsole/services/notification_service.dart';
import 'package:lbjconsole/services/background_service.dart';
import 'package:lbjconsole/themes/app_theme.dart'; import 'package:lbjconsole/themes/app_theme.dart';
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
@@ -39,6 +40,16 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
_bleService = BLEService(); _bleService = BLEService();
_bleService.initialize(); _bleService.initialize();
_initializeServices(); _initializeServices();
_checkAndStartBackgroundService();
}
Future<void> _checkAndStartBackgroundService() async {
final settings = await DatabaseService.instance.getAllSettings() ?? {};
final backgroundServiceEnabled = (settings['backgroundServiceEnabled'] ?? 0) == 1;
if (backgroundServiceEnabled) {
await BackgroundService.startService();
}
} }
@override @override
@@ -66,7 +77,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
_dataSubscription = _bleService.dataStream.listen((record) { _dataSubscription = _bleService.dataStream.listen((record) {
_notificationService.showTrainNotification(record); _notificationService.showTrainNotification(record);
if (_historyScreenKey.currentState != null) { if (_historyScreenKey.currentState != null) {
_historyScreenKey.currentState!.loadRecords(scrollToTop: true); _historyScreenKey.currentState!.addNewRecord(record);
} }
}); });
} }

View File

@@ -5,6 +5,7 @@ import 'dart:io';
import 'package:lbjconsole/models/merged_record.dart'; import 'package:lbjconsole/models/merged_record.dart';
import 'package:lbjconsole/services/database_service.dart'; import 'package:lbjconsole/services/database_service.dart';
import 'package:lbjconsole/services/ble_service.dart'; import 'package:lbjconsole/services/ble_service.dart';
import 'package:lbjconsole/services/background_service.dart';
import 'package:lbjconsole/themes/app_theme.dart'; import 'package:lbjconsole/themes/app_theme.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -196,11 +197,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
Switch( Switch(
value: _backgroundServiceEnabled, value: _backgroundServiceEnabled,
onChanged: (value) { onChanged: (value) async {
setState(() { setState(() {
_backgroundServiceEnabled = value; _backgroundServiceEnabled = value;
}); });
_saveSettings(); await _saveSettings();
if (value) {
await BackgroundService.startService();
} else {
await BackgroundService.stopService();
}
}, },
activeColor: Theme.of(context).colorScheme.primary, activeColor: Theme.of(context).colorScheme.primary,
), ),
@@ -503,8 +510,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
Future<void> _shareData() async { Future<void> _shareData() async {
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
@@ -530,7 +535,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (exportedPath != null) { if (exportedPath != null) {
final file = File(exportedPath); final file = File(exportedPath);
final fileName = file.path.split(Platform.pathSeparator).last; final fileName = file.path.split(Platform.pathSeparator).last;
await Share.shareXFiles( await Share.shareXFiles(
[XFile(file.path)], [XFile(file.path)],
subject: 'LBJ Console Data', subject: 'LBJ Console Data',
@@ -735,7 +740,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (snapshot.hasData) { if (snapshot.hasData) {
return Text(snapshot.data!, style: AppTheme.bodyMedium); return Text(snapshot.data!, style: AppTheme.bodyMedium);
} else { } else {
return const Text('v0.1.3-flutter', style: AppTheme.bodyMedium); return const Text('v0.1.3-flutter',
style: AppTheme.bodyMedium);
} }
}, },
), ),

View File

@@ -0,0 +1,211 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:lbjconsole/services/ble_service.dart';
const String _notificationChannelId = 'lbj_console_channel';
const String _notificationChannelName = 'LBJ Console 后台服务';
const String _notificationChannelDescription = '保持蓝牙连接稳定';
const int _notificationId = 114514;
class BackgroundService {
static final FlutterBackgroundService _service = FlutterBackgroundService();
static bool _isInitialized = false;
static Future<void> initialize() async {
if (_isInitialized) return;
final service = FlutterBackgroundService();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
if (Platform.isAndroid) {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
_notificationChannelId,
_notificationChannelName,
description: _notificationChannelDescription,
importance: Importance.low,
enableLights: false,
enableVibration: false,
playSound: false,
);
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
}
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: _onStart,
autoStart: true,
isForegroundMode: true,
notificationChannelId: _notificationChannelId,
initialNotificationTitle: 'LBJ Console',
initialNotificationContent: '蓝牙连接监控中',
foregroundServiceNotificationId: _notificationId,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: _onStart,
onBackground: _onIosBackground,
),
);
_isInitialized = true;
}
@pragma('vm:entry-point')
static void _onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
if (service is AndroidServiceInstance) {
service.on('setAsForeground').listen((event) {
service.setAsForegroundService();
});
service.on('setAsBackground').listen((event) {
service.setAsBackgroundService();
});
}
service.on('stopService').listen((event) {
service.stopSelf();
});
BLEService().initialize();
if (service is AndroidServiceInstance) {
await Future.delayed(const Duration(seconds: 1));
if (await service.isForegroundService()) {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
try {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
_notificationChannelId,
_notificationChannelName,
description: _notificationChannelDescription,
importance: Importance.low,
enableLights: false,
enableVibration: false,
playSound: false,
);
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
await flutterLocalNotificationsPlugin.show(
_notificationId,
'LBJ Console',
'蓝牙连接监控中',
NotificationDetails(
android: AndroidNotificationDetails(
_notificationChannelId,
_notificationChannelName,
channelDescription: _notificationChannelDescription,
icon: '@mipmap/ic_launcher',
ongoing: true,
autoCancel: false,
importance: Importance.low,
priority: Priority.low,
enableLights: false,
enableVibration: false,
playSound: false,
onlyAlertOnce: true,
setAsGroupSummary: false,
groupKey: 'lbj_console_group',
visibility: NotificationVisibility.public,
category: AndroidNotificationCategory.service,
),
),
);
print('前台服务通知显示成功');
} catch (e) {
print('前台服务通知显示失败: $e');
}
}
}
Timer.periodic(const Duration(seconds: 30), (timer) async {
if (service is AndroidServiceInstance) {
if (await service.isForegroundService()) {
try {
final bleService = BLEService();
final isConnected = bleService.isConnected;
final deviceStatus = bleService.deviceStatus;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin.show(
_notificationId,
'LBJ Console',
isConnected ? '蓝牙已连接 - $deviceStatus' : '蓝牙未连接 - 自动重连中',
NotificationDetails(
android: AndroidNotificationDetails(
_notificationChannelId,
_notificationChannelName,
channelDescription: _notificationChannelDescription,
icon: '@mipmap/ic_launcher',
ongoing: true,
autoCancel: false,
importance: Importance.low,
priority: Priority.low,
enableLights: false,
enableVibration: false,
playSound: false,
onlyAlertOnce: true,
setAsGroupSummary: false,
groupKey: 'lbj_console_group',
visibility: NotificationVisibility.public,
category: AndroidNotificationCategory.service,
),
),
);
} catch (e) {
print('前台服务通知更新失败: $e');
}
}
}
});
}
@pragma('vm:entry-point')
static Future<bool> _onIosBackground(ServiceInstance service) async {
return true;
}
static Future<void> startService() async {
await initialize();
final service = FlutterBackgroundService();
if (Platform.isAndroid) {
final isRunning = await service.isRunning();
if (!isRunning) {
service.startService();
}
} else if (Platform.isIOS) {
service.startService();
}
}
static Future<void> stopService() async {
final service = FlutterBackgroundService();
service.invoke('stopService');
}
static Future<bool> isRunning() async {
final service = FlutterBackgroundService();
return await service.isRunning();
}
static void setForegroundMode(bool isForeground) {
final service = FlutterBackgroundService();
if (isForeground) {
service.invoke('setAsForeground');
} else {
service.invoke('setAsBackground');
}
}
}

View File

@@ -0,0 +1,113 @@
import 'dart:convert';
import 'package:sqflite/sqflite.dart';
import 'package:lbjconsole/models/map_state.dart';
import 'database_service.dart';
class MapStateService {
static final MapStateService instance = MapStateService._internal();
factory MapStateService() => instance;
MapStateService._internal();
static const String _tableName = 'record_map_states';
final Map<String, MapState> _memoryCache = {};
Future<void> _ensureTableExists() async {
final db = await DatabaseService.instance.database;
await db.execute('''
CREATE TABLE IF NOT EXISTS $_tableName (
key TEXT PRIMARY KEY,
state TEXT NOT NULL,
updated_at INTEGER NOT NULL
)
''');
}
String getSingleRecordMapKey(String recordId) {
return "${recordId}_record_map";
}
String getMergedRecordMapKey(String groupKey) {
return "${groupKey}_group_map";
}
Future<void> saveMapState(String key, MapState state) async {
try {
_memoryCache[key] = state;
final db = await DatabaseService.instance.database;
await _ensureTableExists();
await db.insert(
_tableName,
{
'key': key,
'state': jsonEncode(state.toJson()),
'updated_at': DateTime.now().millisecondsSinceEpoch,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
} catch (e) {
print('保存地图状态失败: $e');
}
}
Future<MapState?> getMapState(String key) async {
if (_memoryCache.containsKey(key)) {
return _memoryCache[key];
}
try {
final db = await DatabaseService.instance.database;
await _ensureTableExists();
final result = await db.query(
_tableName,
where: 'key = ?',
whereArgs: [key],
limit: 1,
);
if (result.isNotEmpty) {
final stateJson = jsonDecode(result.first['state'] as String);
final state = MapState.fromJson(stateJson);
_memoryCache[key] = state;
return state;
}
} catch (e) {
print('读取地图状态失败: $e');
}
return null;
}
Future<void> deleteMapState(String key) async {
_memoryCache.remove(key);
try {
final db = await DatabaseService.instance.database;
await db.delete(
_tableName,
where: 'key = ?',
whereArgs: [key],
);
} catch (e) {
print('删除地图状态失败: $e');
}
}
Future<void> clearAllMapStates() async {
_memoryCache.clear();
try {
final db = await DatabaseService.instance.database;
await db.delete(_tableName);
} catch (e) {
print('清空地图状态失败: $e');
}
}
void clearMemoryCache() {
_memoryCache.clear();
}
}

View File

@@ -278,6 +278,38 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_background_service:
dependency: "direct main"
description:
name: flutter_background_service
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.0"
flutter_background_service_android:
dependency: transitive
description:
name: flutter_background_service_android
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.1"
flutter_background_service_ios:
dependency: transitive
description:
name: flutter_background_service_ios
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.3"
flutter_background_service_platform_interface:
dependency: transitive
description:
name: flutter_background_service_platform_interface
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.2"
flutter_blue_plus: flutter_blue_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -52,6 +52,7 @@ dependencies:
file_picker: ^8.1.2 file_picker: ^8.1.2
package_info_plus: ^8.1.2 package_info_plus: ^8.1.2
msix: ^3.16.12 msix: ^3.16.12
flutter_background_service: ^5.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: