Background location tracking

This commit is contained in:
2026-03-09 22:33:35 +08:00
parent 1e1c7d9552
commit ccc1c62262
10 changed files with 371 additions and 127 deletions
+68 -4
View File
@@ -24,6 +24,7 @@ import '../../providers/workforce_provider.dart';
import '../../theme/m3_motion.dart';
import '../../utils/app_time.dart';
import '../../utils/location_permission.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../widgets/face_verification_overlay.dart';
import '../../utils/snackbar.dart';
import '../../widgets/gemini_animated_text_field.dart';
@@ -42,6 +43,10 @@ class _AttendanceScreenState extends ConsumerState<AttendanceScreen>
late TabController _tabController;
bool _fabMenuOpen = false;
// (moved into _CheckInTabState) local tracking state for optimistic UI updates
// bool _trackingLocal = false;
// bool _trackingSaving = false;
@override
void initState() {
super.initState();
@@ -261,6 +266,9 @@ class _CheckInTab extends ConsumerStatefulWidget {
class _CheckInTabState extends ConsumerState<_CheckInTab> {
bool _loading = false;
// local tracking state for optimistic UI updates (optimistic toggle in UI)
bool _trackingLocal = false;
bool _trackingSaving = false;
final _justCheckedIn = <String>{};
final _checkInLogIds = <String, String>{};
String? _overtimeLogId;
@@ -352,6 +360,17 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
final schedulesAsync = ref.watch(dutySchedulesProvider);
final logsAsync = ref.watch(attendanceLogsProvider);
final allowTracking = profile?.allowTracking ?? false;
// local state for optimistic switch update and to avoid flicker while the
// profile stream lags. Once the network request completes we keep the
// local value until the provider returns the same value.
bool effectiveTracking;
if (_trackingSaving) {
effectiveTracking = _trackingLocal;
} else if (_trackingLocal != allowTracking) {
effectiveTracking = _trackingLocal;
} else {
effectiveTracking = allowTracking;
}
if (profile == null) {
return const Center(child: CircularProgressIndicator());
@@ -409,11 +428,14 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
),
),
Switch(
value: allowTracking,
value: effectiveTracking,
onChanged: (v) async {
if (_trackingSaving) return; // ignore while pending
if (v) {
final granted = await ensureLocationPermission();
if (!granted) {
// first ensure foreground & background location perms
final grantedFg = await ensureLocationPermission();
if (!grantedFg) {
if (context.mounted) {
showWarningSnackBar(
context,
@@ -422,8 +444,50 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
}
return;
}
final grantedBg =
await ensureBackgroundLocationPermission();
if (!grantedBg) {
// if permanently denied, open settings so the user can
// manually grant the permission; otherwise just warn.
if (await Permission
.locationAlways
.isPermanentlyDenied ||
await Permission.location.isPermanentlyDenied) {
openAppSettings();
}
if (context.mounted) {
showWarningSnackBar(
context,
'Background location permission is required.',
);
}
return;
}
}
// optimistically flip
setState(() {
_trackingLocal = v;
_trackingSaving = true;
});
try {
await ref
.read(whereaboutsControllerProvider)
.setTracking(v);
} catch (e) {
if (context.mounted) {
showWarningSnackBar(context, e.toString());
}
// revert to actual stored value
setState(() {
_trackingLocal = allowTracking;
});
} finally {
setState(() {
_trackingSaving = false;
});
}
ref.read(whereaboutsControllerProvider).setTracking(v);
},
),
],