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