createTrackingMiddleware function
Implementation
List<Middleware<AppState>> createTrackingMiddleware(TrackingDeps deps) {
StreamSubscription<Position>? posSub;
BatchingUploader? uploader;
SampleBuilder? builder;
LiveSubscriber? live;
Future<String> ensurePermission(Store<AppState> store) async {
bool enabled = await Geolocator.isLocationServiceEnabled();
if (!enabled) {
store.dispatch(StatusChanged('Location services are disabled.'));
return 'denied';
}
var perm = await Geolocator.checkPermission();
if (perm == LocationPermission.denied) {
perm = await Geolocator.requestPermission();
}
if (perm == LocationPermission.denied) {
store.dispatch(PermissionChanged('denied'));
return 'denied';
}
if (perm == LocationPermission.deniedForever) {
store.dispatch(PermissionChanged('foreverDenied'));
return 'foreverDenied';
}
store.dispatch(PermissionChanged('granted'));
return 'granted';
}
Future<void> start(Store<AppState> store, StartTrackingAction action) async {
final perm = await ensurePermission(store);
if (perm != 'granted') return;
// Build gRPC client + uploader
final factory = GrpcClientFactory(
host: deps.host,
port: deps.port,
userTls: deps.useTls,
tokenProvider: deps.tokenProvider,
);
uploader = BatchingUploader(factory, maxBatch: 50, flushEvery: const Duration(seconds: 5));
await uploader!.start();
final did = await DeviceIdentity.getOrCreate();
final sid = DateTime.now().millisecondsSinceEpoch.toString();
builder = SampleBuilder(deviceId: did, employeeId: action.employeeId, sessionId: sid);
// Position stream
const settings = LocationSettings(
accuracy: LocationAccuracy.bestForNavigation,
distanceFilter: 10,
);
posSub = Geolocator.getPositionStream(locationSettings: settings).listen(
(pos) async {
final sample = await builder!.fromPosition(pos);
uploader?.enqueue(sample);
store.dispatch(LastSampleUpdated(sample));
},
onError: (e) => store.dispatch(StatusChanged('Location error: $e')),
cancelOnError: false,
);
store.dispatch(TrackingStarted(did, sid, action.employeeId));
}
Future<void> stop(Store<AppState> store) async {
await posSub?.cancel();
posSub = null;
if (uploader != null) {
await uploader!.stop();
uploader = null;
}
builder = null;
store.dispatch(TrackingStopped());
}
Future<void> subscribeLive(Store<AppState> store, SubscribeLiveAction a) async {
// Optional live stream of processed server updates (for supervisor UI etc.)
final factory = GrpcClientFactory(
host: deps.host,
port: deps.port,
userTls: deps.useTls,
tokenProvider: deps.tokenProvider,
);
live = LiveSubscriber(factory);
await live!.start(
deviceIds: a.deviceIds,
employeeIds: a.employeeIds,
onUpdate: (u) => store.dispatch(LiveUpdateReceived(u)),
);
}
Future<void> unsubscribeLive() async {
await live?.stop();
live = null;
}
return [
TypedMiddleware<AppState, StartTrackingAction>((store, action, next) async {
next(action);
await start(store, action);
}).call,
TypedMiddleware<AppState, StopTrackingAction>((store, action, next) async {
next(action);
await stop(store);
}).call,
TypedMiddleware<AppState, SubscribeLiveAction>((store, action, next) async {
next(action);
await subscribeLive(store, action);
}).call,
TypedMiddleware<AppState, UnsubscribeLiveAction>((store, action, next) async {
next(action);
await unsubscribeLive();
}).call,
];
}