Code changes by azmat
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:permission_handler/permission_handler.dart' as ph;
|
||||
|
||||
import 'attendance_math.dart';
|
||||
import 'attendance_storage.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background attendance tracking (wake every 5 min -> one GPS fix -> sleep).
|
||||
//
|
||||
// The task handler runs in a background isolate. It reads the open session id
|
||||
// that the UI saved, takes one fix, applies the accuracy/drift filters and
|
||||
// stores accepted dots in sqflite. It also pings the UI with live totals.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Entry point for the background isolate. Must stay top-level.
|
||||
@pragma('vm:entry-point')
|
||||
void attendanceCallback() {
|
||||
FlutterForegroundTask.setTaskHandler(AttendanceTaskHandler());
|
||||
}
|
||||
|
||||
class AttendanceTaskHandler extends TaskHandler {
|
||||
String? _sessionId;
|
||||
|
||||
@override
|
||||
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
|
||||
try {
|
||||
_sessionId =
|
||||
await FlutterForegroundTask.getData(key: 'attendance_session_id')
|
||||
as String?;
|
||||
} catch (_) {
|
||||
_sessionId = null;
|
||||
}
|
||||
// Fallback: SharedPreferences is visible from this isolate too.
|
||||
_sessionId ??= await AttendanceStorage.getActiveSession();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRepeatEvent(DateTime timestamp) async {
|
||||
final String? sessionId = _sessionId ?? await _refreshSessionId();
|
||||
if (sessionId == null) return; // punched out already, nothing to do
|
||||
|
||||
try {
|
||||
final Position pos = await Geolocator.getCurrentPosition(
|
||||
locationSettings: LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
timeLimit: kFixTimeout,
|
||||
),
|
||||
);
|
||||
|
||||
final List<Map<String, dynamic>> existing =
|
||||
await AttendanceStorage.getPoints(sessionId);
|
||||
final bool isFirst = existing.isEmpty;
|
||||
double distanceFromLast = 0;
|
||||
if (!isFirst) {
|
||||
final last = existing.last;
|
||||
distanceFromLast = haversineMeters(
|
||||
(last['lat'] as num).toDouble(),
|
||||
(last['lng'] as num).toDouble(),
|
||||
pos.latitude,
|
||||
pos.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
final bool keep = shouldStorePoint(
|
||||
accuracy: pos.accuracy,
|
||||
distanceFromLastMeters: distanceFromLast,
|
||||
isFirstPoint: isFirst,
|
||||
);
|
||||
|
||||
if (keep) {
|
||||
await AttendanceStorage.insertPoint(
|
||||
sessionId: sessionId,
|
||||
lat: pos.latitude,
|
||||
lng: pos.longitude,
|
||||
accuracy: pos.accuracy,
|
||||
isMocked: pos.isMocked,
|
||||
timestamp: pos.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
// Live totals for the open punch page (harmless if nobody listens).
|
||||
final updated = await AttendanceStorage.getPoints(sessionId);
|
||||
final double km = _totalKm(updated);
|
||||
FlutterForegroundTask.sendDataToMain({
|
||||
'sessionId': sessionId,
|
||||
'points': updated.length,
|
||||
'km': km,
|
||||
});
|
||||
try {
|
||||
await FlutterForegroundTask.updateService(
|
||||
notificationTitle: 'Duty tracking ON',
|
||||
notificationText:
|
||||
'${updated.length} points • ${km.toStringAsFixed(2)} km',
|
||||
);
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
// GPS off / timeout / no fix this round - just wait for next 5-min tick.
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _refreshSessionId() async {
|
||||
_sessionId = await AttendanceStorage.getActiveSession();
|
||||
return _sessionId;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
|
||||
|
||||
double _totalKm(List<Map<String, dynamic>> points) {
|
||||
double meters = 0;
|
||||
for (int i = 1; i < points.length; i++) {
|
||||
meters += haversineMeters(
|
||||
(points[i - 1]['lat'] as num).toDouble(),
|
||||
(points[i - 1]['lng'] as num).toDouble(),
|
||||
(points[i]['lat'] as num).toDouble(),
|
||||
(points[i]['lng'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
return meters / 1000.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Small result for the permission flow so the UI can explain, not just fail.
|
||||
class PermissionResult {
|
||||
final bool ok;
|
||||
final String message;
|
||||
PermissionResult(this.ok, this.message);
|
||||
}
|
||||
|
||||
class AttendanceService {
|
||||
static bool _initialized = false;
|
||||
|
||||
/// Call once from main() before runApp().
|
||||
static void initService() {
|
||||
if (_initialized) return;
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'attendance_tracking',
|
||||
channelName: 'Duty tracking',
|
||||
channelDescription:
|
||||
'Shows while your duty location is being recorded.',
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(
|
||||
showNotification: true,
|
||||
),
|
||||
foregroundTaskOptions: ForegroundTaskOptions(
|
||||
eventAction: ForegroundTaskEventAction.repeat(
|
||||
kFixInterval.inMilliseconds,
|
||||
),
|
||||
allowWakeLock: true,
|
||||
allowWifiLock: false,
|
||||
autoRunOnBoot: false,
|
||||
// Swiping the app away from recents must NOT stop duty tracking.
|
||||
stopWithTask: false,
|
||||
),
|
||||
);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// Asks (in order): location -> always/background -> notifications + GPS on.
|
||||
static Future<PermissionResult> ensurePermissions() async {
|
||||
final bool serviceOn = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceOn) {
|
||||
return PermissionResult(
|
||||
false, 'Please switch ON location (GPS) and try again.');
|
||||
}
|
||||
|
||||
ph.PermissionStatus loc = await ph.Permission.location.request();
|
||||
if (!loc.isGranted) {
|
||||
return PermissionResult(
|
||||
false, 'Location permission is needed for punch in.');
|
||||
}
|
||||
|
||||
// Android 10+: background needs its own grant (may open Settings).
|
||||
ph.PermissionStatus always = await ph.Permission.locationAlways.request();
|
||||
if (!always.isGranted) {
|
||||
return PermissionResult(true,
|
||||
'Background not granted - tracking works only while the app is open.');
|
||||
}
|
||||
|
||||
await ph.Permission.notification.request(); // for the tracking notice
|
||||
return PermissionResult(true, ' granted');
|
||||
}
|
||||
|
||||
static Future<bool> get isPunchedIn async {
|
||||
if (!await FlutterForegroundTask.isRunningService) {
|
||||
// Service dead but prefs say open (reboot/crash) -> still show open.
|
||||
return await AttendanceStorage.getActiveSession() != null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Starts a duty session + background tracking. Returns the session id.
|
||||
static Future<String> punchIn() async {
|
||||
final String sessionId =
|
||||
'SES-${DateTime.now().millisecondsSinceEpoch}';
|
||||
final DateTime now = DateTime.now();
|
||||
await AttendanceStorage.createSession(sessionId, now);
|
||||
await AttendanceStorage.setActiveSession(sessionId);
|
||||
await FlutterForegroundTask.saveData(
|
||||
key: 'attendance_session_id', value: sessionId);
|
||||
|
||||
await FlutterForegroundTask.startService(
|
||||
serviceId: 1001,
|
||||
serviceTypes: [ForegroundServiceTypes.location],
|
||||
notificationTitle: 'Duty tracking ON',
|
||||
notificationText: 'Punched in at ${_hhmm(now)} • recording location',
|
||||
callback: attendanceCallback,
|
||||
);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/// Stops tracking and closes the session. Returns a summary for the UI.
|
||||
static Future<Map<String, dynamic>> punchOut() async {
|
||||
final String? sessionId = await AttendanceStorage.getActiveSession();
|
||||
try {
|
||||
await FlutterForegroundTask.stopService();
|
||||
} catch (_) {}
|
||||
await FlutterForegroundTask.clearAllData();
|
||||
await AttendanceStorage.clearActiveSession();
|
||||
|
||||
if (sessionId == null) {
|
||||
return {'points': 0, 'km': 0.0};
|
||||
}
|
||||
final List<Map<String, dynamic>> points =
|
||||
await AttendanceStorage.getPoints(sessionId);
|
||||
double meters = 0;
|
||||
for (int i = 1; i < points.length; i++) {
|
||||
meters += haversineMeters(
|
||||
(points[i - 1]['lat'] as num).toDouble(),
|
||||
(points[i - 1]['lng'] as num).toDouble(),
|
||||
(points[i]['lat'] as num).toDouble(),
|
||||
(points[i]['lng'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
final double km = meters / 1000.0;
|
||||
final Map<String, dynamic>? session =
|
||||
await AttendanceStorage.getSession(sessionId);
|
||||
await AttendanceStorage.closeSession(sessionId, DateTime.now(), km);
|
||||
return {
|
||||
'sessionId': sessionId,
|
||||
'punchIn': session?['punch_in'],
|
||||
'points': points.length,
|
||||
'km': km,
|
||||
};
|
||||
}
|
||||
|
||||
static String _hhmm(DateTime t) =>
|
||||
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
/// Nudge to exempt the app from battery optimization (OEMs kill trackers).
|
||||
static Future<void> maybeAskBatteryExemption(BuildContext context) async {
|
||||
try {
|
||||
final bool ignoring =
|
||||
await FlutterForegroundTask.isIgnoringBatteryOptimizations;
|
||||
if (ignoring || !context.mounted) return;
|
||||
final bool? go = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Keep tracking running'),
|
||||
content: const Text(
|
||||
'Xiaomi, Oppo, Vivo and Samsung phones stop background tracking '
|
||||
'to save battery. Allow "Unrestricted" battery use for this app '
|
||||
'so duty tracking keeps working when the screen is off?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(c).pop(false),
|
||||
child: const Text('Later')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(c).pop(true),
|
||||
child: const Text('Open settings')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (go == true) {
|
||||
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user