Fix background service, add support for NBT instead of Json

This commit is contained in:
zontreck 2025-05-25 12:56:07 -07:00
parent a686412ec7
commit 4a8d515f4d
6 changed files with 289 additions and 24 deletions

View file

@ -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<String, dynamic> payload = {"cmd": "create", "data": saveData};
_upload(nbtData);
}
var reply = await dio.post(
TTConsts.SESSION_SERVER,
data: json.encode(payload),
);
Map<String, dynamic> replyJs = json.decode(reply.data as String);
if (replyJs["status"] == "ok") {
print("Successful upload");
LastSessionID = replyJs['session'] as String;
static Future<void> _upload(List<int> nbtData) async {
Dio dio = Dio();
Map<String, dynamic> 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<String, dynamic> 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<bool> 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<void> SaveCacheState() async {
CompoundTag ct = IsOnTheClock ? await _serializeToNBT() : CompoundTag();
await NBTHelper.CommitNBT(data: ct, name: "appstate");
}
static Future<void> _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<CompoundTag> _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<String, dynamic> SaveData() {
Map<String, dynamic> saveData = {};
@ -283,7 +439,18 @@ class SessionData {
data: json.encode(payload),
);
return LoadData(reply.data as Map<String, dynamic>);
String cType = reply.headers.value("Content-Type") ?? "application/json";
if (cType == "application/json") {
return LoadData(reply.data as Map<String, dynamic>);
} 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<String, dynamic> toJsonMap() {
return {
"start": StartTime.toString(),
"start": StartTime.toIso8601String(),
"endPos": endLocation?.toMap() ?? "incomplete",
};
}
@ -407,6 +574,28 @@ class Delivery {
return delivery;
}
Future<CompoundTag> 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<Delivery> 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<String, dynamic> toJsonMap() {
Map<String, Object> trip = {
"start": StartTime.toString(),
"end": EndTime.toString(),
"start": StartTime.toIso8601String(),
"end": EndTime.toIso8601String(),
};
List<Map<String, dynamic>> 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<dynamic> dropOffs = jsx['deliveries'] as List<dynamic>;
@ -457,6 +647,41 @@ class Trip {
return trip;
}
Future<CompoundTag> 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<Trip> 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<CompoundTag> toNBT() async {
CompoundTag ct = CompoundTag();
ct.put("latitude", DoubleTag.valueOf(latitude));
ct.put("longitude", DoubleTag.valueOf(longitude));
return ct;
}
static Future<SmallPosition> fromNBT(CompoundTag ct) async {
return SmallPosition(
latitude: ct.get("latitude")?.asDouble() ?? 0,
longitude: ct.get("longitude")?.asDouble() ?? 0,
);
}
}