From 4a8d515f4d869671b953aabc5279bd67955e167c Mon Sep 17 00:00:00 2001 From: zontreck Date: Sun, 25 May 2025 12:56:07 -0700 Subject: [PATCH] Fix background service, add support for NBT instead of Json --- android/app/src/main/AndroidManifest.xml | 1 + lib/consts.dart | 2 +- lib/data.dart | 274 +++++++++++++++++++++-- lib/main.dart | 7 +- lib/pages/HomePage.dart | 26 +++ pubspec.yaml | 3 +- 6 files changed, 289 insertions(+), 24 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 94886ea..2986e0d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + diff --git a/lib/consts.dart b/lib/consts.dart index 942803f..4c7707b 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -9,7 +9,7 @@ class TTConsts { static get SESSION_SERVER => "https://api.zontreck.com/timetrack/$UPDATE_CHANNEL/timetrack.php"; - static const VERSION = "1.0.0-beta.16"; + static const VERSION = "1.0.0-beta.17"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.beta; diff --git a/lib/data.dart b/lib/data.dart index 05f8902..76d7bb3 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'dart:typed_data'; import 'dart:ui'; import 'package:dio/dio.dart'; @@ -8,9 +9,19 @@ import 'package:floating_window_android/floating_window_android.dart'; import 'package:flutter/material.dart'; import 'package:flutter_background/flutter_background.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:libac_dart/nbt/NbtIo.dart'; +import 'package:libac_dart/nbt/NbtUtils.dart'; +import 'package:libac_dart/nbt/SnbtIo.dart'; import 'package:libac_dart/nbt/Stream.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/nbt/impl/DoubleTag.dart'; +import 'package:libac_dart/nbt/impl/ListTag.dart'; +import 'package:libac_dart/nbt/impl/StringTag.dart'; +import 'package:libac_dart/utils/Converter.dart'; import 'package:libac_dart/utils/TimeUtils.dart'; +import 'package:libacflutter/nbt/nbtHelpers.dart'; import 'package:timetrack/consts.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; class SessionData { static DateTime StartTime = DateTime(0); @@ -163,10 +174,14 @@ class SessionData { "Background notification for keeping TimeTrack running in the background", notificationImportance: AndroidNotificationImportance.normal, ); + bool success = await FlutterBackground.initialize( androidConfig: androidConfig, ); + FlutterBackground.enableBackgroundExecution(); + WakelockPlus.enable(); + if (!success) { return false; } @@ -214,6 +229,7 @@ class SessionData { return; } positions.add(SmallPosition.fromPosition(pos)); + SessionData.SaveCacheState(); SessionData.Calls.dispatch(); }); @@ -229,27 +245,167 @@ class SessionData { EndTime = DateTime.now(); - var saveData = SaveData(); - print(saveData); + FlutterBackground.disableBackgroundExecution(); + WakelockPlus.disable(); + + var saveData = await _serializeToNBT(); + ResetAppSession(); + print(SnbtIo.writeToString(saveData)); + Uint8List nbtData = await NbtIo.writeToStream(saveData); Trips = []; positions = []; - Dio dio = Dio(); - Map payload = {"cmd": "create", "data": saveData}; + _upload(nbtData); + } - var reply = await dio.post( - TTConsts.SESSION_SERVER, - data: json.encode(payload), - ); - Map replyJs = json.decode(reply.data as String); - if (replyJs["status"] == "ok") { - print("Successful upload"); - LastSessionID = replyJs['session'] as String; + static Future _upload(List nbtData) async { + Dio dio = Dio(); + + Map payload = { + "cmd": "create", + "data": base64Encoder.encode(nbtData), + }; + + try { + var reply = await dio.post( + TTConsts.SESSION_SERVER, + data: json.encode(payload), + ); + if (reply.statusCode == null) { + throw Exception("Fatal error while uploading"); + } + if (reply.statusCode! != 200) { + throw Exception("Fatal error while uploading"); + } + + Map replyJs = json.decode(reply.data as String); + if (replyJs["status"] == "ok") { + print("Successful upload"); + LastSessionID = replyJs['session'] as String; + Calls.dispatch(); + } + } catch (E) { + // Retry in 2 seconds + DisplayError = + "Error: Something went wrong during upload. Retry in 5 seconds..."; Calls.dispatch(); + + Timer(Duration(seconds: 5), () { + _upload(nbtData); + }); } } + static void ResetAppSession() { + IsOnTheClock = false; + StartTime = DateTime.fromMillisecondsSinceEpoch(0); + Trips = []; + currentDelivery = null; + currentTrip = null; + positions = []; + EndTime = DateTime.fromMillisecondsSinceEpoch(0); + LastSessionID = ""; + DisplayError = ""; + IsReadOnly = false; + ContainsTripTimes = true; + + Calls.dispatch(); + } + + /// This function attempts to load the saved state from cache. + /// + /// This will return true when a session exists; false when no session exists, or is empty. + static Future LoadSavedCacheState() async { + CompoundTag ct = await NBTHelper.GetNBT(name: "appstate"); + // Restore various flags now. + if (ct.isEmpty) { + ResetAppSession(); + return false; + } + + await _deserialize(ct); + + return true; + } + + /// Saves the current session based on various factors. + static Future SaveCacheState() async { + CompoundTag ct = IsOnTheClock ? await _serializeToNBT() : CompoundTag(); + await NBTHelper.CommitNBT(data: ct, name: "appstate"); + } + + static Future _deserialize(CompoundTag ct) async { + IsOnTheClock = NbtUtils.readBoolean(ct, "inprog"); + StartTime = DateTime.parse(ct.get("start")!.asString()); + if (ct.containsKey("end")) { + EndTime = DateTime.parse(ct.get("end")!.asString()); + } else { + EndTime = DateTime(0); + } + TotalPay = ct.get("totalPay")!.asDouble(); + + ListTag poses = ct.get("pos")! as ListTag; + for (var pos in poses.value) { + positions.add(await SmallPosition.fromNBT(pos.asCompoundTag())); + } + + ListTag trips = ct.get("trips") as ListTag; + for (var trip in trips.value) { + Trips.add(await Trip.fromNBT(trip.asCompoundTag())); + } + + if (ct.containsKey("current_trip")) { + currentTrip = await Trip.fromNBT(ct.get("current_trip")!.asCompoundTag()); + } + + if (ct.containsKey("current_delivery")) { + currentDelivery = await Delivery.fromNBT( + ct.get("current_delivery")!.asCompoundTag(), + ); + } + } + + /// This private function will turn all the data into NBT, for both the cache state, and newer usage, for storing it on the server in a more compact format. + static Future _serializeToNBT() async { + CompoundTag ct = CompoundTag(); + + NbtUtils.writeBoolean(ct, "inprog", IsOnTheClock); + // No need to write the contains trip times flag, it is set during deserialization. For inprog sessions, it will be set to true by the system. + ct.put("start", StringTag.valueOf(StartTime.toIso8601String())); + if (EndTime.year < 2000) { + // We have a end time + ct.put("end", StringTag.valueOf(EndTime.toIso8601String())); + } + + ListTag posX = ListTag(); + for (var pos in positions) { + posX.add(await pos.toNBT()); + } + ct.put("pos", posX); + + ListTag myTrips = ListTag(); + for (var trip in Trips) { + myTrips.add(await trip.toNBT()); + } + ct.put("trips", myTrips); + + // This format supports saving current trip and current delivery. + if (currentDelivery != null) { + ct.put("current_delivery", await currentDelivery!.toNBT()); + } + + if (currentTrip != null) { + ct.put("current_trip", await currentTrip!.toNBT()); + } + + if (TotalPay != null) { + ct.put("totalPay", DoubleTag.valueOf(TotalPay!)); + } + + return ct; + } + static Map SaveData() { Map saveData = {}; @@ -283,7 +439,18 @@ class SessionData { data: json.encode(payload), ); - return LoadData(reply.data as Map); + String cType = reply.headers.value("Content-Type") ?? "application/json"; + + if (cType == "application/json") { + return LoadData(reply.data as Map); + } else if (cType == "application/nbt") { + Uint8List lst = base64Encoder.decode(reply.data as String); + // Convert this to a CompoundTag + CompoundTag ct = await NbtIo.readFromStream(lst) as CompoundTag; + _deserialize(ct); + return true; + } else + return false; } catch (E) { return false; } @@ -389,7 +556,7 @@ class Delivery { Map toJsonMap() { return { - "start": StartTime.toString(), + "start": StartTime.toIso8601String(), "endPos": endLocation?.toMap() ?? "incomplete", }; } @@ -407,6 +574,28 @@ class Delivery { return delivery; } + + Future toNBT() async { + CompoundTag ct = CompoundTag(); + ct.put("start", StringTag.valueOf(StartTime.toIso8601String())); + if (endLocation != null) { + ct.put("endPos", await endLocation!.toNBT()); + } + + return ct; + } + + static Future fromNBT(CompoundTag ct) async { + Delivery delivery = Delivery(); + delivery.StartTime = DateTime.parse(ct.get("start")!.asString()); + if (ct.containsKey("endPos")) { + delivery.endLocation = await SmallPosition.fromNBT( + ct.get("endPos") as CompoundTag, + ); + } + + return delivery; + } } class Trip { @@ -428,8 +617,8 @@ class Trip { Map toJsonMap() { Map trip = { - "start": StartTime.toString(), - "end": EndTime.toString(), + "start": StartTime.toIso8601String(), + "end": EndTime.toIso8601String(), }; List> dropOffs = []; for (var delivery in deliveries) { @@ -446,8 +635,9 @@ class Trip { trip.StartTime = DateTime.parse(jsx['start'] as String); if (jsx.containsKey("end")) { trip.EndTime = DateTime.parse(jsx['end'] as String); - } else + } else { SessionData.ContainsTripTimes = false; + } trip.deliveries = []; List dropOffs = jsx['deliveries'] as List; @@ -457,6 +647,41 @@ class Trip { return trip; } + + Future toNBT() async { + CompoundTag ct = CompoundTag(); + + ct.put("start", StringTag.valueOf(StartTime.toIso8601String())); + if (EndTime.year < 2000) { + ct.put("end", StringTag.valueOf(EndTime.toIso8601String())); + } + + ListTag drops = ListTag(); + for (var drop in deliveries) { + drops.add(await drop.toNBT()); + } + ct.put("deliveries", drops); + + return ct; + } + + static Future fromNBT(CompoundTag tag) async { + Trip trip = Trip(); + + // Deserialize the Trip + trip.StartTime = DateTime.parse(tag.get("start")!.asString()); + if (!tag.containsKey("end")) { + SessionData.ContainsTripTimes = false; + } + + ListTag drops = tag.get("deliveries")! as ListTag; + for (var drop in drops.value) { + Delivery del = await Delivery.fromNBT(drop.asCompoundTag()); + trip.deliveries.add(del); + } + + return trip; + } } class Callbacks { @@ -497,4 +722,19 @@ class SmallPosition { longitude: map['longitude'] as double, ); } + + Future toNBT() async { + CompoundTag ct = CompoundTag(); + ct.put("latitude", DoubleTag.valueOf(latitude)); + ct.put("longitude", DoubleTag.valueOf(longitude)); + + return ct; + } + + static Future fromNBT(CompoundTag ct) async { + return SmallPosition( + latitude: ct.get("latitude")?.asDouble() ?? 0, + longitude: ct.get("longitude")?.asDouble() ?? 0, + ); + } } diff --git a/lib/main.dart b/lib/main.dart index 63248e7..b1ab326 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,16 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:timetrack/consts.dart'; import 'package:timetrack/data.dart'; +import 'package:timetrack/pages/HomePage.dart'; import 'package:timetrack/pages/MainApp.dart'; @pragma("vm:entry-point") void serviceEntry() { - // This is where both the Background service and the Floating app window would enter the app. - Timer.periodic(Duration(seconds: 5), (timer) async { - // Run the location fetch, add to list, etc, etc. - }); - // Run the floater app here. + runApp(MaterialApp(debugShowCheckedModeBanner: false, home: OverlayWidget())); } Future main() async { diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 84a658d..4d8c71b 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -30,6 +30,18 @@ class _HomePageState extends State { void call() { setState(() {}); + + if (SessionData.DisplayError.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(SessionData.DisplayError), + elevation: 8, + duration: Duration(seconds: 5), + ), + ); + + SessionData.DisplayError = ""; + } } @override @@ -393,3 +405,17 @@ class _HomePageState extends State { ); } } + +class OverlayWidget extends StatefulWidget { + @override + State createState() { + return _Overlay(); + } +} + +class _Overlay extends State { + @override + Widget build(BuildContext context) { + return Scaffold(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index f8f56b8..3897d8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-beta.16 +version: 1.0.0-beta.17 environment: sdk: ^3.7.2 @@ -50,6 +50,7 @@ dependencies: flutter_background: ^1.3.0+1 floating_window_android: ^1.0.0 shared_preferences: ^2.5.3 + wakelock_plus: ^1.3.2 dev_dependencies: flutter_test: