feat: Implement notification and storage services, geofence management, and BLoC for geofence verification

- Add NotificationService for handling local notifications.
- Introduce StorageService for key-value storage using SharedPreferences.
- Create DeviceLocation model to represent geographic locations.
- Define LocationPermissionStatus enum for managing location permissions.
- Implement BackgroundGeofenceService for periodic geofence checks while clocked in.
- Develop GeofenceServiceImpl for geofence proximity verification using LocationService.
- Create GeofenceResult model to encapsulate geofence check results.
- Define GeofenceServiceInterface for geofence service abstraction.
- Implement GeofenceBloc to manage geofence verification and background tracking.
- Create events and states for GeofenceBloc to handle various geofence scenarios.
- Add GeofenceStatusBanner widget to display geofence verification status in the UI.
This commit is contained in:
Achintha Isuru
2026-03-13 16:01:26 -04:00
parent 2fc6b3139e
commit 7b576c0ed4
54 changed files with 2216 additions and 493 deletions

View File

@@ -33,3 +33,7 @@ export 'src/services/device/gallery/gallery_service.dart';
export 'src/services/device/file/file_picker_service.dart';
export 'src/services/device/file_upload/device_file_upload_service.dart';
export 'src/services/device/audio/audio_recorder_service.dart';
export 'src/services/device/location/location_service.dart';
export 'src/services/device/notification/notification_service.dart';
export 'src/services/device/storage/storage_service.dart';
export 'src/services/device/background_task/background_task_service.dart';

View File

@@ -48,5 +48,13 @@ class CoreModule extends Module {
apiUploadService: i.get<FileUploadService>(),
),
);
// 6. Register Geofence Device Services
i.addLazySingleton<LocationService>(() => const LocationService());
i.addLazySingleton<NotificationService>(() => NotificationService());
i.addLazySingleton<StorageService>(() => StorageService());
i.addLazySingleton<BackgroundTaskService>(
() => const BackgroundTaskService(),
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:krow_domain/krow_domain.dart';
import 'package:workmanager/workmanager.dart';
/// Service that wraps [Workmanager] for scheduling background tasks.
class BackgroundTaskService extends BaseDeviceService {
/// Creates a [BackgroundTaskService] instance.
const BackgroundTaskService();
/// Initializes the workmanager with the given [callbackDispatcher].
Future<void> initialize(Function callbackDispatcher) async {
return action(() async {
await Workmanager().initialize(callbackDispatcher);
});
}
/// Registers a periodic background task with the given [frequency].
Future<void> registerPeriodicTask({
required String uniqueName,
required String taskName,
Duration frequency = const Duration(minutes: 15),
Map<String, dynamic>? inputData,
}) async {
return action(() async {
await Workmanager().registerPeriodicTask(
uniqueName,
taskName,
frequency: frequency,
inputData: inputData,
existingWorkPolicy: ExistingPeriodicWorkPolicy.replace,
);
});
}
/// Registers a one-off background task.
Future<void> registerOneOffTask({
required String uniqueName,
required String taskName,
Map<String, dynamic>? inputData,
}) async {
return action(() async {
await Workmanager().registerOneOffTask(
uniqueName,
taskName,
inputData: inputData,
);
});
}
/// Cancels a registered task by its [uniqueName].
Future<void> cancelByUniqueName(String uniqueName) async {
return action(() => Workmanager().cancelByUniqueName(uniqueName));
}
/// Cancels all registered background tasks.
Future<void> cancelAll() async {
return action(() => Workmanager().cancelAll());
}
}

View File

@@ -0,0 +1,110 @@
import 'dart:async';
import 'package:geolocator/geolocator.dart';
import 'package:krow_domain/krow_domain.dart';
/// Service that wraps [Geolocator] to provide location access.
///
/// This is the only file in the core package that imports geolocator.
/// All location access across the app should go through this service.
class LocationService extends BaseDeviceService {
/// Creates a [LocationService] instance.
const LocationService();
/// Checks the current permission status and requests permission if needed.
Future<LocationPermissionStatus> checkAndRequestPermission() async {
return action(() async {
final bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return LocationPermissionStatus.serviceDisabled;
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
return _mapPermission(permission);
});
}
/// Requests upgrade to "Always" permission for background location access.
Future<LocationPermissionStatus> requestAlwaysPermission() async {
return action(() async {
// On Android, requesting permission again after whileInUse prompts
// for Always.
final LocationPermission permission = await Geolocator.requestPermission();
return _mapPermission(permission);
});
}
/// Returns the device's current location.
Future<DeviceLocation> getCurrentLocation() async {
return action(() async {
final Position position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
),
);
return _toDeviceLocation(position);
});
}
/// Emits location updates as a stream, filtered by [distanceFilter] meters.
Stream<DeviceLocation> watchLocation({int distanceFilter = 10}) {
return Geolocator.getPositionStream(
locationSettings: LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: distanceFilter,
),
).map(_toDeviceLocation);
}
/// Whether device location services are currently enabled.
Future<bool> isServiceEnabled() async {
return action(() => Geolocator.isLocationServiceEnabled());
}
/// Stream that emits when location service status changes.
///
/// Emits `true` when enabled, `false` when disabled.
Stream<bool> get onServiceStatusChanged {
return Geolocator.getServiceStatusStream().map(
(ServiceStatus status) => status == ServiceStatus.enabled,
);
}
/// Opens the app settings page for the user to manually grant permissions.
Future<bool> openAppSettings() async {
return action(() => Geolocator.openAppSettings());
}
/// Opens the device location settings page.
Future<bool> openLocationSettings() async {
return action(() => Geolocator.openLocationSettings());
}
/// Maps a [LocationPermission] to a [LocationPermissionStatus].
LocationPermissionStatus _mapPermission(LocationPermission permission) {
switch (permission) {
case LocationPermission.always:
return LocationPermissionStatus.granted;
case LocationPermission.whileInUse:
return LocationPermissionStatus.whileInUse;
case LocationPermission.denied:
return LocationPermissionStatus.denied;
case LocationPermission.deniedForever:
return LocationPermissionStatus.deniedForever;
case LocationPermission.unableToDetermine:
return LocationPermissionStatus.denied;
}
}
/// Converts a geolocator [Position] to a [DeviceLocation].
DeviceLocation _toDeviceLocation(Position position) {
return DeviceLocation(
latitude: position.latitude,
longitude: position.longitude,
accuracy: position.accuracy,
timestamp: position.timestamp,
);
}
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:krow_domain/krow_domain.dart';
/// Service that wraps [FlutterLocalNotificationsPlugin] for local notifications.
class NotificationService extends BaseDeviceService {
/// Creates a [NotificationService] with the given [plugin] instance.
///
/// If no plugin is provided, a default instance is created.
NotificationService({FlutterLocalNotificationsPlugin? plugin})
: _plugin = plugin ?? FlutterLocalNotificationsPlugin();
/// The underlying notification plugin instance.
final FlutterLocalNotificationsPlugin _plugin;
/// Initializes notification channels and requests permissions.
Future<void> initialize() async {
return action(() async {
const AndroidInitializationSettings androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _plugin.initialize(settings: settings);
});
}
/// Displays a local notification with the given [title] and [body].
Future<void> showNotification({
required String title,
required String body,
int id = 0,
}) async {
return action(() async {
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
'krow_geofence',
'Geofence Notifications',
channelDescription: 'Notifications for geofence events',
importance: Importance.high,
priority: Priority.high,
);
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails();
const NotificationDetails details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.show(id: id, title: title, body: body, notificationDetails: details);
});
}
/// Cancels a specific notification by [id].
Future<void> cancelNotification(int id) async {
return action(() => _plugin.cancel(id: id));
}
/// Cancels all active notifications.
Future<void> cancelAll() async {
return action(() => _plugin.cancelAll());
}
}

View File

@@ -0,0 +1,81 @@
import 'package:krow_domain/krow_domain.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Service that wraps [SharedPreferences] for key-value storage.
class StorageService extends BaseDeviceService {
/// Creates a [StorageService] instance.
StorageService();
/// Cached preferences instance.
SharedPreferences? _prefs;
/// Returns the [SharedPreferences] instance, initializing lazily.
Future<SharedPreferences> get _preferences async {
_prefs ??= await SharedPreferences.getInstance();
return _prefs!;
}
/// Retrieves a string value for the given [key].
Future<String?> getString(String key) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.getString(key);
});
}
/// Stores a string [value] for the given [key].
Future<bool> setString(String key, String value) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.setString(key, value);
});
}
/// Retrieves a double value for the given [key].
Future<double?> getDouble(String key) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.getDouble(key);
});
}
/// Stores a double [value] for the given [key].
Future<bool> setDouble(String key, double value) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.setDouble(key, value);
});
}
/// Retrieves a boolean value for the given [key].
Future<bool?> getBool(String key) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.getBool(key);
});
}
/// Stores a boolean [value] for the given [key].
Future<bool> setBool(String key, bool value) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.setBool(key, value);
});
}
/// Removes the value for the given [key].
Future<bool> remove(String key) async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.remove(key);
});
}
/// Clears all stored values.
Future<bool> clear() async {
return action(() async {
final SharedPreferences prefs = await _preferences;
return prefs.clear();
});
}
}

View File

@@ -27,3 +27,7 @@ dependencies:
file_picker: ^8.1.7
record: ^6.2.0
firebase_auth: ^6.1.4
geolocator: ^14.0.2
flutter_local_notifications: ^21.0.0
shared_preferences: ^2.5.4
workmanager: ^0.9.0+3