Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions lib/core/background/schedulers/expired_batch_work_scheduler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

import 'package:dawarich/core/background/workmanager/expired_batch_upload_worker.dart';
import 'package:flutter/foundation.dart';
import 'package:workmanager/workmanager.dart';

final class ExpiredBatchWorkScheduler {
static bool _initialized = false;

static Future<void> initialize() async {
if (_initialized) {
return;
}

await Workmanager().initialize(
ExpiredBatchUploadWorker.callbackDispatcher,
);

_initialized = true;
}

static Future<void> register(int expirationMinutes) async {
await initialize();

final frequency = _getWorkerFrequency(expirationMinutes);

if (kDebugMode) {
debugPrint(
'[ExpiredBatchWorker] Registering periodic work every '
'${frequency.inMinutes} minutes.',
);
}

await Workmanager().registerPeriodicTask(
ExpiredBatchUploadWorker.uniqueWorkName,
ExpiredBatchUploadWorker.uniqueWorkName,
frequency: frequency,
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
constraints: Constraints(
networkType: NetworkType.connected,
),
);
}

static Future<void> cancel() async {
await initialize();

if (kDebugMode) {
debugPrint('[ExpiredBatchWorker] Cancelling periodic work.');
}

await Workmanager().cancelByUniqueName(
ExpiredBatchUploadWorker.uniqueWorkName,
);
}

static Duration _getWorkerFrequency(int expirationMinutes) {
final minutes = expirationMinutes < 15 ? 15 : expirationMinutes;
return Duration(minutes: minutes);
}
}
69 changes: 69 additions & 0 deletions lib/core/background/workmanager/expired_batch_upload_worker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

import 'package:dawarich/core/di/providers/core_providers.dart';
import 'package:dawarich/core/di/providers/session_providers.dart';
import 'package:dawarich/core/di/providers/usecase_providers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:option_result/result.dart';
import 'package:workmanager/workmanager.dart';

final class ExpiredBatchUploadWorker {
static const String uniqueWorkName = 'expired-batch-upload-check';

@pragma('vm:entry-point')
static void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
if (task != uniqueWorkName) {
return true;
}

ProviderContainer? container;

try {
if (kDebugMode) {
debugPrint('[ExpiredBatchWorker] Starting worker...');
}

container = ProviderContainer();
await container.read(coreProvider.future);

final session = await container.read(sessionBoxProvider.future);
final user = await session.refreshSession();

if (user == null) {
if (kDebugMode) {
debugPrint('[ExpiredBatchWorker] No user session, skipping.');
}
return true;
}

final checkExpiredBatch =
await container.read(checkAndUploadExpiredBatchUseCaseProvider.future);

final result = await checkExpiredBatch(user.id);

if (result case Ok(value: final didUpload)) {
if (kDebugMode) {
if (didUpload) {
debugPrint('[ExpiredBatchWorker] Expired batch uploaded.');
} else {
debugPrint('[ExpiredBatchWorker] No expired batch to upload.');
}
}
} else if (result case Err(value: final err)) {
debugPrint('[ExpiredBatchWorker] Expired batch check failed: $err');
}

return true;
} catch (e, s) {
debugPrint('[ExpiredBatchWorker] Fatal worker error: $e\n$s');

// This is an opportunistic check, not a mission-critical exact job.
// Returning true avoids retry storms.
return true;
} finally {
container?.dispose();
}
});
}
}
10 changes: 10 additions & 0 deletions lib/core/di/providers/usecase_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dawarich/core/application/usecases/api/delete_point_usecase.dart
import 'package:dawarich/core/application/usecases/api/get_points_usecase.dart';
import 'package:dawarich/core/application/usecases/api/get_total_pages_usecase.dart';
import 'package:dawarich/features/batch/application/usecases/batch_upload_workflow_usecase.dart';
import 'package:dawarich/features/batch/application/usecases/check_and_upload_expired_batch_usecase.dart';
import 'package:dawarich/features/batch/application/usecases/check_batch_threshold_usecase.dart';
import 'package:dawarich/features/batch/application/usecases/get_current_batch_usecase.dart';
import 'package:dawarich/features/stats/application/repositories/countries_repository_interfaces.dart';
Expand Down Expand Up @@ -212,6 +213,15 @@ final getBatchPointCountUseCaseProvider = FutureProvider<GetBatchPointCountUseCa
return GetBatchPointCountUseCase(repo);
});

final checkAndUploadExpiredBatchUseCaseProvider = FutureProvider<CheckAndUploadExpiredBatchUseCase>((ref) async {
final getSettings = await ref.watch(getTrackerSettingsUseCaseProvider.future);
final localRepo = await ref.watch(pointLocalRepositoryProvider.future);
final getCurrentBatch = await ref.watch(getCurrentBatchUseCaseProvider.future);
final batchUploadWorkflow = await ref.watch(batchUploadWorkflowUseCaseProvider.future);

return CheckAndUploadExpiredBatchUseCase(getSettings, localRepo, getCurrentBatch, batchUploadWorkflow);
});

final getLastPointUseCaseProvider = FutureProvider<GetLastPointUseCase>((ref) async {
final repo = await ref.watch(pointLocalRepositoryProvider.future);
return GetLastPointUseCase(repo);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

import 'package:dawarich/core/data/repositories/local_point_repository_interfaces.dart';
import 'package:dawarich/features/batch/application/usecases/batch_upload_workflow_usecase.dart';
import 'package:dawarich/features/batch/application/usecases/get_current_batch_usecase.dart';
import 'package:dawarich/features/tracking/application/usecases/settings/get_tracker_settings_usecase.dart';
import 'package:option_result/result.dart';

final class CheckAndUploadExpiredBatchUseCase {
final GetTrackerSettingsUseCase _getTrackerSettings;
final IPointLocalRepository _localPointRepository;
final GetCurrentBatchUseCase _getCurrentBatch;
final BatchUploadWorkflowUseCase _batchUploadWorkflow;

CheckAndUploadExpiredBatchUseCase(
this._getTrackerSettings,
this._localPointRepository,
this._getCurrentBatch,
this._batchUploadWorkflow,
);

Future<Result<bool, String>> call(int userId) async {
try {
final settings = await _getTrackerSettings(userId);

if (!settings.isBatchExpirationEnabled ||
settings.batchExpirationMinutes == null) {
return const Ok(false);
}

final oldest =
await _localPointRepository.getOldestUnUploadedPointTimestamp(userId);

if (oldest == null) {
return const Ok(false);
}

final threshold = DateTime.now().subtract(
Duration(minutes: settings.batchExpirationMinutes!),
);

if (!oldest.isBefore(threshold)) {
return const Ok(false);
}

final batch = await _getCurrentBatch(userId);
if (batch.isEmpty) {
return const Ok(false);
}

final uploadResult = await _batchUploadWorkflow(batch, userId);

if (uploadResult case Ok()) {
return const Ok(true);
}

if (uploadResult case Err(value: final err)) {
return Err(err);
}

return const Err('Unknown upload result');
} catch (e) {
return Err('Failed to check expired batch: $e');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,43 @@ class BackgroundTrackingEntry {
}

static Future<void> _startBackgroundTracking(
ServiceInstance backgroundService,
ProviderContainer container,
int userId,
) async {
ServiceInstance backgroundService,
ProviderContainer container,
int userId,
) async {
if (kDebugMode) {
debugPrint('[Background] Starting background tracking...');
}

final automation = await container.read(pointAutomationServiceProvider.future);
await automation.startTracking(userId);

try {
final checkExpiredBatch =
await container.read(checkAndUploadExpiredBatchUseCaseProvider.future);

final expirationResult = await checkExpiredBatch(userId);

if (expirationResult case Ok(value: final didUpload)) {
if (kDebugMode && didUpload) {
debugPrint('[Background] Expired batch found and uploaded.');
}
} else if (expirationResult case Err(value: final err)) {
debugPrint('[Background] Expired batch check failed: $err');
}
} catch (e, s) {
debugPrint('[Background] Error during expired batch check: $e\n$s');
}

try {
final getLastPoint = await container.read(getLastPointUseCaseProvider.future);
final getBatchCount = await container.read(getBatchPointCountUseCaseProvider.future);
await setInitialForegroundNotification(getLastPoint, getBatchCount, backgroundService, userId);
await setInitialForegroundNotification(
getLastPoint,
getBatchCount,
backgroundService,
userId,
);
} catch (_) {
// ignore
}
Expand Down
Loading
Loading