Update AndroidManifest to include foreground service permissions and implement tracking service in excursion screens. Refactor tracking logic to utilize TrackingService for better state management and streamline position updates.

This commit is contained in:
Nico
2025-06-04 20:59:50 +02:00
parent 0a737f5153
commit 9261abfc57
6 changed files with 404 additions and 377 deletions

View File

@@ -2,8 +2,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application <application
android:label="LUPUS" android:label="LUPUS"
android:name="${applicationName}" android:name="${applicationName}"
@@ -37,11 +37,12 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- <service --> <service
<!-- android:name="com.dexterous.flutterlocalnotifications.ForegroundService" --> android:name=".TrackingService"
<!-- android:exported="false" --> android:exported="false"
<!-- android:stopWithTask="false" --> android:stopWithTask="false"
<!-- android:foregroundServiceType="<location>"> --> android:foregroundServiceType="location">
</service>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -19,6 +19,7 @@ import 'package:fforte/screens/sharedMethods/save_template.dart';
import 'package:fforte/screens/sharedWidgets/datum.dart'; import 'package:fforte/screens/sharedWidgets/datum.dart';
import 'package:fforte/screens/sharedWidgets/var_text_field.dart'; import 'package:fforte/screens/sharedWidgets/var_text_field.dart';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/services/tracking_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -147,7 +148,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
return currentPosition; return currentPosition;
}); });
if (widget.existingData?.isNotEmpty ?? false) { if (widget.existingData?.isNotEmpty ?? false) {
for (var key in widget.existingData!.keys) { for (var key in widget.existingData!.keys) {
rmap[key]!["controller"]!.text = rmap[key]!["controller"]!.text =
@@ -188,268 +188,288 @@ class _ExcursionMainState extends State<ExcursionMain> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Step> getSteps() => [ List<Step> getSteps() => [
Step( Step(
title: Text(AppLocalizations.of(context)!.dateandtime), title: Text(AppLocalizations.of(context)!.dateandtime),
content: Column( content: Column(
children: [ children: [
// ---------- Date // ---------- Date
Datum( Datum(
initDatum: DateTime.now(), initDatum: DateTime.now(),
onDateChanged: (date) { onDateChanged: (date) {
rmap["Datum"]!["controller"]!.text = date.toString(); rmap["Datum"]!["controller"]!.text = date.toString();
}, },
name: AppLocalizations.of(context)!.date, name: AppLocalizations.of(context)!.date,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Pack // ---------- Pack
VarTextField( VarTextField(
textController: rmap["Rudel"]!["controller"]!, textController: rmap["Rudel"]!["controller"]!,
localization: AppLocalizations.of(context)!.rudel, localization: AppLocalizations.of(context)!.rudel,
dbName: "Rudel", dbName: "Rudel",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Participants // ---------- Participants
VarTextField( VarTextField(
textController: rmap["Teilnehmer"]!["controller"]!, textController: rmap["Teilnehmer"]!["controller"]!,
localization: AppLocalizations.of(context)!.teilnehmer, localization: AppLocalizations.of(context)!.teilnehmer,
dbName: "Teilnehmer", dbName: "Teilnehmer",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Duration // ---------- Duration
VarTextField( VarTextField(
textController: rmap["Dauer"]!["controller"]!, textController: rmap["Dauer"]!["controller"]!,
localization: AppLocalizations.of(context)!.dauer, localization: AppLocalizations.of(context)!.dauer,
dbName: "Dauer", dbName: "Dauer",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// ---------- Dog(leash) // ---------- Dog(leash)
HundULeine( HundULeine(
mHund: rmap["MHund"]!["controller"]!, mHund: rmap["MHund"]!["controller"]!,
mLeine: rmap["MLeine"]!["controller"]!, mLeine: rmap["MLeine"]!["controller"]!,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- State // ---------- State
VarTextField( VarTextField(
textController: rmap["BLand"]!["controller"]!, textController: rmap["BLand"]!["controller"]!,
localization: AppLocalizations.of(context)!.bland, localization: AppLocalizations.of(context)!.bland,
dbName: "BLand", dbName: "BLand",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Country // ---------- Country
VarTextField( VarTextField(
textController: rmap["Lkr"]!["controller"]!, textController: rmap["Lkr"]!["controller"]!,
localization: AppLocalizations.of(context)!.lkr, localization: AppLocalizations.of(context)!.lkr,
dbName: "Lkr", dbName: "Lkr",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- By State // ---------- By State
VarTextField( VarTextField(
textController: rmap["BeiOrt"]!["controller"]!, textController: rmap["BeiOrt"]!["controller"]!,
localization: AppLocalizations.of(context)!.beiort, localization: AppLocalizations.of(context)!.beiort,
dbName: "BeiOrt", dbName: "BeiOrt",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Bima number // ---------- Bima number
const Divider(), const Divider(),
const SizedBox(height: 10), const SizedBox(height: 10),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),
child: ExpansionPanelList( child: ExpansionPanelList(
expansionCallback: expansionCallback: ((int index, bool isExpanded) =>
((int index, bool isExpanded) =>
setState(() => bimaExtended = isExpanded)), setState(() => bimaExtended = isExpanded)),
expandedHeaderPadding: EdgeInsets.all(0), expandedHeaderPadding: EdgeInsets.all(0),
children: [ children: [
ExpansionPanel( ExpansionPanel(
isExpanded: bimaExtended, isExpanded: bimaExtended,
canTapOnHeader: true, canTapOnHeader: true,
headerBuilder: (context, bool isOpen) => Padding( headerBuilder: (context, bool isOpen) => Padding(
padding: const EdgeInsets.only(left: 15), padding: const EdgeInsets.only(left: 15),
child: Align(alignment: Alignment.centerLeft, child: Text("BImA", style: Theme.of(context).textTheme.bodyLarge,),), child: Align(
), alignment: Alignment.centerLeft,
body: Padding( child: Text(
padding: const EdgeInsets.all(15), "BImA",
child: Column( style: Theme.of(context).textTheme.bodyLarge,
children: [ ),
const SizedBox(height: 10),
VarTextField(
textController: rmap["BimaNr"]!["controller"]!,
localization: AppLocalizations.of(context)!.bimaNr,
dbName: "BimaNr",
required: false,
dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), ),
// ---------- Bima name body: Padding(
VarTextField( padding: const EdgeInsets.all(15),
textController: rmap["BimaName"]!["controller"]!, child: Column(
localization: AppLocalizations.of(context)!.bimaName, children: [
dbName: "BimaName", const SizedBox(height: 10),
required: false, VarTextField(
dbDesignation: DatabasesEnum.excursion, textController: rmap["BimaNr"]!["controller"]!,
localization:
AppLocalizations.of(context)!.bimaNr,
dbName: "BimaNr",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
const SizedBox(height: 10),
// ---------- Bima name
VarTextField(
textController:
rmap["BimaName"]!["controller"]!,
localization:
AppLocalizations.of(context)!.bimaName,
dbName: "BimaName",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
const SizedBox(height: 10),
// ---------- Bima user
BimaNutzer(
onBimaNutzerChanged: (value) {
setState(() {
rmap["BimaNutzer"]!["controller"]!.text =
value;
});
},
),
const SizedBox(height: 10),
// ---------- Bima AGV
VarTextField(
textController: rmap["BimaAGV"]!["controller"]!,
localization:
AppLocalizations.of(context)!.bimaAGV,
dbName: "BimaAGV",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
],
), ),
const SizedBox(height: 10), ),
// ---------- Bima user
BimaNutzer(
onBimaNutzerChanged: (value) {
setState(() {
rmap["BimaNutzer"]!["controller"]!.text = value;
});
},
),
const SizedBox(height: 10),
// ---------- Bima AGV
VarTextField(
textController: rmap["BimaAGV"]!["controller"]!,
localization: AppLocalizations.of(context)!.bimaAGV,
dbName: "BimaAGV",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
],
), ),
), ],
), ),
], ),
), ],
), ),
], ),
), Step(
), title: Text(AppLocalizations.of(context)!.umstaendeUndAktionen),
Step( content: Column(
title: Text(AppLocalizations.of(context)!.umstaendeUndAktionen), children: [
content: Column( // ---------- Tracking
children: [ ElevatedButton(
// ---------- Tracking onPressed: () async {
ElevatedButton( await Navigator.push(context, MaterialPageRoute(
onPressed:
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) { builder: (context) {
return Tracking( return Tracking(
weg: rmap["Weg"]!["controller"]!, weg: rmap["Weg"]!["controller"]!,
startPosition: currentPosition, startPosition: currentPosition,
); );
}, },
), ));
), setState(() {});
child: Text(AppLocalizations.of(context)!.trackingAnAusschalten), },
), child:
const SizedBox(height: 10), Text(AppLocalizations.of(context)!.trackingAnAusschalten),
),
const SizedBox(height: 10),
// ---------- Weather // ---------- Weather
VarTextField( VarTextField(
textController: rmap["Wetter"]!["controller"]!, textController: rmap["Wetter"]!["controller"]!,
localization: AppLocalizations.of(context)!.wetter, localization: AppLocalizations.of(context)!.wetter,
dbName: "Wetter", dbName: "Wetter",
required: false, required: false,
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
),
const SizedBox(height: 10),
// ---------- Temperature
VarTextField(
textController: rmap["Temperat"]!["controller"]!,
localization: AppLocalizations.of(context)!.temperatur,
dbName: "Temperat",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
const SizedBox(height: 10),
// ---------- Last precipitation
LetzterNiederschlag(
controller: rmap["RegenVor"]!["controller"]!),
const SizedBox(height: 20),
// ---------- Track conditions
StreckeUSpurbedingungen(
kmAutoController: rmap["KmAuto"]!["controller"]!,
kmFussController: rmap["KmFuss"]!["controller"]!,
kmRadController: rmap["KmRad"]!["controller"]!,
spGutController: rmap["SpGut"]!["controller"]!,
spMittelController: rmap["SpMittel"]!["controller"]!,
spSchlechtController: rmap["SpSchlecht"]!["controller"]!,
),
const SizedBox(height: 20),
const Divider(),
// ---------- Track found
SpurGefunden(
spurFund: rmap["SpurFund"]!["controller"]!,
spurLang: rmap["SpurLang"]!["controller"]!,
spurTiere: rmap["SpurTiere"]!["controller"]!,
spSicher: rmap["SpSicher"]!["controller"]!,
welpenSp: rmap["WelpenSp"]!["controller"]!,
welpenAnz: rmap["WelpenAnz"]!["controller"]!,
wpSicher: rmap["WpSicher"]!["controller"]!,
),
const Divider(),
const SizedBox(height: 20),
// ---------- Counts
Anzahlen(
losungAnz: rmap["LosungAnz"]!["controller"]!,
losungGes: rmap["LosungGes"]!["controller"]!,
losungGen: rmap["LosungGen"]!["controller"]!,
urinAnz: rmap["UrinAnz"]!["controller"]!,
urinGen: rmap["UrinGen"]!["controller"]!,
oestrAnz: rmap["OestrAnz"]!["controller"]!,
oestrGen: rmap["OestrGen"]!["controller"]!,
haarAnz: rmap["HaarAnz"]!["controller"]!,
haarGen: rmap["HaarGen"]!["controller"]!,
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
// ---------- Clues
Align(
alignment: Alignment.bottomLeft,
child: Text(
AppLocalizations.of(context)!.hinweise,
style: Theme.of(context).textTheme.titleMedium,
),
),
Hinweise(hinweise: rmap["Hinweise"]!["controller"]!),
],
), ),
const SizedBox(height: 10), ),
// ---------- Temperature Step(
VarTextField( title: Text(AppLocalizations.of(context)!.intkomm),
textController: rmap["Temperat"]!["controller"]!, content: Column(
localization: AppLocalizations.of(context)!.temperatur, children: [
dbName: "Temperat", // ---------- Remarks
required: false, VarTextField(
dbDesignation: DatabasesEnum.excursion, textController: rmap["Bemerk"]!["controller"]!,
localization: AppLocalizations.of(context)!.sonstbemerkungen,
dbName: "Bemerk",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
const SizedBox(height: 20),
// ---------- Internal communication
VarTextField(
textController: rmap["IntKomm"]!["controller"]!,
localization: AppLocalizations.of(context)!.intkomm,
dbName: "IntKomm",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
],
), ),
const SizedBox(height: 10), ),
// ---------- Last precipitation ];
LetzterNiederschlag(controller: rmap["RegenVor"]!["controller"]!),
const SizedBox(height: 20),
// ---------- Track conditions
StreckeUSpurbedingungen(
kmAutoController: rmap["KmAuto"]!["controller"]!,
kmFussController: rmap["KmFuss"]!["controller"]!,
kmRadController: rmap["KmRad"]!["controller"]!,
spGutController: rmap["SpGut"]!["controller"]!,
spMittelController: rmap["SpMittel"]!["controller"]!,
spSchlechtController: rmap["SpSchlecht"]!["controller"]!,
),
const SizedBox(height: 20),
const Divider(),
// ---------- Track found
SpurGefunden(
spurFund: rmap["SpurFund"]!["controller"]!,
spurLang: rmap["SpurLang"]!["controller"]!,
spurTiere: rmap["SpurTiere"]!["controller"]!,
spSicher: rmap["SpSicher"]!["controller"]!,
welpenSp: rmap["WelpenSp"]!["controller"]!,
welpenAnz: rmap["WelpenAnz"]!["controller"]!,
wpSicher: rmap["WpSicher"]!["controller"]!,
),
const Divider(),
const SizedBox(height: 20),
// ---------- Counts
Anzahlen(
losungAnz: rmap["LosungAnz"]!["controller"]!,
losungGes: rmap["LosungGes"]!["controller"]!,
losungGen: rmap["LosungGen"]!["controller"]!,
urinAnz: rmap["UrinAnz"]!["controller"]!,
urinGen: rmap["UrinGen"]!["controller"]!,
oestrAnz: rmap["OestrAnz"]!["controller"]!,
oestrGen: rmap["OestrGen"]!["controller"]!,
haarAnz: rmap["HaarAnz"]!["controller"]!,
haarGen: rmap["HaarGen"]!["controller"]!,
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
// ---------- Clues
Align(
alignment: Alignment.bottomLeft,
child: Text(
AppLocalizations.of(context)!.hinweise,
style: Theme.of(context).textTheme.titleMedium,
),
),
Hinweise(hinweise: rmap["Hinweise"]!["controller"]!),
],
),
),
Step(
title: Text(AppLocalizations.of(context)!.intkomm),
content: Column(
children: [
// ---------- Remarks
VarTextField(
textController: rmap["Bemerk"]!["controller"]!,
localization: AppLocalizations.of(context)!.sonstbemerkungen,
dbName: "Bemerk",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
const SizedBox(height: 20),
// ---------- Internal communication
VarTextField(
textController: rmap["IntKomm"]!["controller"]!,
localization: AppLocalizations.of(context)!.intkomm,
dbName: "IntKomm",
required: false,
dbDesignation: DatabasesEnum.excursion,
),
],
),
),
];
// Begin of widget tree // Begin of widget tree
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.excursion)), appBar: AppBar(
title: Text(AppLocalizations.of(context)!.excursion),
actions: [
// Text(TrackingService().isTracking ? "Tracking" : "Not tracking")
Image.asset(
TrackingService().isTracking ? "assets/icons/tracking_on.png" : "assets/icons/tracking_off.png",
width: 40,
),
],
),
body: PageTransitionSwitcher( body: PageTransitionSwitcher(
duration: const Duration(microseconds: 800), duration: const Duration(microseconds: 800),
transitionBuilder: ( transitionBuilder: (

View File

@@ -1,11 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/screens/addCam/services/geolocator_service.dart'; import 'package:fforte/screens/addCam/services/geolocator_service.dart';
import 'package:fforte/screens/helper/add_entries_dialog_helper.dart'; import 'package:fforte/screens/helper/add_entries_dialog_helper.dart';
import 'package:fforte/screens/helper/snack_bar_helper.dart'; import 'package:fforte/screens/helper/snack_bar_helper.dart';
import 'package:fforte/services/notification_service.dart'; import 'package:fforte/services/tracking_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
@@ -22,20 +21,14 @@ class Tracking extends StatefulWidget {
} }
class _TrackingState extends State<Tracking> { class _TrackingState extends State<Tracking> {
List<LatLng> pathList = []; final TrackingService _trackingService = TrackingService();
StreamSubscription<Position>? positionStream;
LocationMarkerPosition? locationMarkerPosition; LocationMarkerPosition? locationMarkerPosition;
bool positionStreamRunning = false;
MapController mapController = MapController(); MapController mapController = MapController();
StreamSubscription? _positionSubscription;
Random rand = Random();
@override @override
void initState() { void initState() {
// debugging (i guess) super.initState();
// pathList.add(
// LatLng(widget.startPosition.latitude, widget.startPosition.longitude),
// );
if (widget.weg.text.isNotEmpty) { if (widget.weg.text.isNotEmpty) {
for (var element in widget.weg.text.split(";")) { for (var element in widget.weg.text.split(";")) {
@@ -47,7 +40,7 @@ class _TrackingState extends State<Tracking> {
// ignore because the double is short enough then // ignore because the double is short enough then
} }
pathList.add( _trackingService.pathList.add(
LatLng(double.parse(posSplit.first), double.parse(posSplit[1])), LatLng(double.parse(posSplit.first), double.parse(posSplit[1])),
); );
} }
@@ -59,7 +52,15 @@ class _TrackingState extends State<Tracking> {
accuracy: widget.startPosition.accuracy, accuracy: widget.startPosition.accuracy,
); );
super.initState(); _positionSubscription = _trackingService.positionStream$.listen((position) {
setState(() {
locationMarkerPosition = LocationMarkerPosition(
latitude: position.latitude,
longitude: position.longitude,
accuracy: position.accuracy,
);
});
});
GeolocatorService.alwaysPositionEnabled().then((value) { GeolocatorService.alwaysPositionEnabled().then((value) {
if (!value && mounted) { if (!value && mounted) {
@@ -71,101 +72,30 @@ class _TrackingState extends State<Tracking> {
@override @override
void dispose() { void dispose() {
if (positionStream != null) positionStream!.cancel(); _positionSubscription?.cancel();
bool isFirst = true; widget.weg.text = _trackingService.getPathAsString();
if (pathList.isNotEmpty) {
for (var pos in pathList) {
if (!isFirst) {
widget.weg.text += ";";
} else {
isFirst = false;
}
widget.weg.text += "${pos.latitude},${pos.longitude}";
}
}
NotificationService().deleteNotification();
super.dispose(); super.dispose();
} }
final LocationSettings streamLocationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10,
);
void onTrackingStart() async {
// notification handling for tracking in background notification
await NotificationService().initNotification();
if (mounted) {
NotificationService().showNotification(
title: AppLocalizations.of(context)!.trackingRunningInBackground,
);
}
positionStream = Geolocator.getPositionStream(
locationSettings: AndroidSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 0,
foregroundNotificationConfig:
mounted
? ForegroundNotificationConfig(
notificationTitle:
AppLocalizations.of(context)!.trackingRunningInBackground,
notificationText: "",
)
: null,
),
).listen((Position? position) {
if (position != null) {
setState(() {
pathList.add(LatLng(position.latitude, position.longitude));
// Random value for debugging
// pathList.add(LatLng(rand.nextInt(5) + 40, position.longitude));
locationMarkerPosition = LocationMarkerPosition(
latitude: position.latitude,
longitude: position.longitude,
accuracy: position.accuracy,
);
});
} else {
if (mounted) {
SnackBarHelper.showSnackBarMessage(
context,
AppLocalizations.of(context)!.couldntDeterminePosition,
);
}
}
});
positionStream!.onError((e) {
NotificationService().deleteNotification();
NotificationService().showNotification(title: "ERROR: $e");
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.tracking), title: Text(AppLocalizations.of(context)!.tracking),
leading: IconButton(onPressed: () { leading: IconButton(
Navigator.pop(context); onPressed: () {
}, icon: Icon(Icons.arrow_back_rounded)), Navigator.pop(context);
},
icon: Icon(Icons.arrow_back_rounded)
),
actions: [ actions: [
if (!positionStreamRunning) if (!_trackingService.isTracking)
IconButton( IconButton(
onPressed: () async { onPressed: () async {
bool delete = bool delete = await AddEntriesDialogHelper.deleteCompleteRouteDialog(context);
await AddEntriesDialogHelper.deleteCompleteRouteDialog(
context,
);
if (delete) { if (delete) {
setState(() { setState(() {
pathList = []; _trackingService.clearPath();
}); });
} }
}, },
@@ -174,32 +104,32 @@ class _TrackingState extends State<Tracking> {
color: Theme.of(context).colorScheme.errorContainer, color: Theme.of(context).colorScheme.errorContainer,
), ),
), ),
if (positionStreamRunning) if (_trackingService.isTracking)
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
positionStreamRunning = false; _trackingService.stopTracking();
positionStream!.cancel();
NotificationService().deleteNotification();
}); });
}, },
child: Text(AppLocalizations.of(context)!.trackingStop), child: Text(AppLocalizations.of(context)!.trackingStop),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
if (positionStreamRunning) { setState(() {
positionStreamRunning = false; if (_trackingService.isTracking) {
positionStream?.pause(); _trackingService.pauseTracking();
} else { } else {
positionStreamRunning = true; if (_trackingService.positionStream == null) {
onTrackingStart(); _trackingService.startTracking(context);
} } else {
setState(() {}); _trackingService.resumeTracking();
}
}
});
}, },
child: child: _trackingService.isTracking
positionStreamRunning ? Text(AppLocalizations.of(context)!.trackingPause)
? Text(AppLocalizations.of(context)!.trackingPause) : Text(AppLocalizations.of(context)!.trackingStart),
: Text(AppLocalizations.of(context)!.trackingStart)
), ),
], ],
), ),
@@ -219,8 +149,7 @@ class _TrackingState extends State<Tracking> {
mapController: mapController, mapController: mapController,
options: MapOptions( options: MapOptions(
interactionOptions: const InteractionOptions( interactionOptions: const InteractionOptions(
flags: flags: InteractiveFlag.pinchZoom |
InteractiveFlag.pinchZoom |
InteractiveFlag.drag | InteractiveFlag.drag |
InteractiveFlag.pinchMove, InteractiveFlag.pinchMove,
), ),
@@ -235,22 +164,16 @@ class _TrackingState extends State<Tracking> {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'de.lupus.apps', userAgentPackageName: 'de.lupus.apps',
), ),
if (pathList.isNotEmpty) if (_trackingService.pathList.isNotEmpty)
PolylineLayer( PolylineLayer(
polylines: [ polylines: [
Polyline(strokeWidth: 2.0, points: pathList, color: Colors.red), Polyline(
strokeWidth: 2.0,
points: _trackingService.pathList,
color: Colors.red
),
], ],
), ),
// CircleLayer(
// circles: [
// CircleMarker(
// color: Colors.blue,
// point: pathList.isEmpty ? widget.startPosition : pathList.last,
// radius: 5,
// useRadiusInMeter: true,
// ),
// ],
// ),
CurrentLocationLayer(), CurrentLocationLayer(),
], ],
), ),

View File

@@ -0,0 +1,83 @@
import 'dart:async';
import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/services/notification_service.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
class TrackingService {
static final TrackingService _instance = TrackingService._internal();
factory TrackingService() => _instance;
TrackingService._internal();
List<LatLng> pathList = [];
StreamSubscription<Position>? positionStream;
bool isTracking = false;
final _positionController = StreamController<Position>.broadcast();
Stream<Position> get positionStream$ => _positionController.stream;
Future<void> startTracking(BuildContext context) async {
if (isTracking) return;
await NotificationService().initNotification();
if (context.mounted) {
NotificationService().showNotification(
title: AppLocalizations.of(context)!.trackingRunningInBackground,
);
}
positionStream = Geolocator.getPositionStream(
locationSettings: AndroidSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 0,
foregroundNotificationConfig: ForegroundNotificationConfig(
notificationTitle: context.mounted ? AppLocalizations.of(context)!.trackingRunningInBackground : "Tracking",
notificationText: "",
),
),
).listen((Position? position) {
if (position != null) {
pathList.add(LatLng(position.latitude, position.longitude));
_positionController.add(position);
}
});
positionStream!.onError((e) {
NotificationService().deleteNotification();
NotificationService().showNotification(title: "ERROR: $e");
});
isTracking = true;
}
void pauseTracking() {
positionStream?.pause();
isTracking = false;
}
void resumeTracking() {
positionStream?.resume();
isTracking = true;
}
void stopTracking() {
positionStream?.cancel();
NotificationService().deleteNotification();
isTracking = false;
}
void clearPath() {
pathList.clear();
}
String getPathAsString() {
return pathList.map((pos) => "${pos.latitude},${pos.longitude}").join(";");
}
void dispose() {
stopTracking();
_positionController.close();
}
}