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:
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user