2025-02-28 01:48:49 +05:30
|
|
|
import 'dart:async';
|
|
|
|
|
|
2022-02-03 10:06:44 -06:00
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
2025-09-15 10:07:41 -05:00
|
|
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
2025-07-17 21:42:29 +05:30
|
|
|
import 'package:immich_mobile/entities/store.entity.dart';
|
2025-12-03 14:22:07 -06:00
|
|
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
2025-07-17 21:42:29 +05:30
|
|
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
2025-07-21 15:30:51 -05:00
|
|
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
2024-05-02 15:59:14 -05:00
|
|
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
2026-06-18 21:16:20 +06:00
|
|
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
2025-09-22 07:15:47 +05:30
|
|
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
2026-05-30 20:57:55 +05:30
|
|
|
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
2026-06-04 17:54:39 +05:30
|
|
|
import 'package:immich_mobile/providers/permission.provider.dart';
|
2024-05-02 15:59:14 -05:00
|
|
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
|
|
|
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
2025-07-17 21:42:29 +05:30
|
|
|
import 'package:logging/logging.dart';
|
2022-02-03 10:06:44 -06:00
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden }
|
2022-02-03 10:06:44 -06:00
|
|
|
|
2024-05-02 15:59:14 -05:00
|
|
|
class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
2023-11-13 20:51:16 +01:00
|
|
|
final Ref _ref;
|
|
|
|
|
bool _wasPaused = false;
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
// Add operation coordination
|
|
|
|
|
Completer<void>? _resumeOperation;
|
|
|
|
|
Completer<void>? _pauseOperation;
|
|
|
|
|
|
|
|
|
|
final _log = Logger("AppLifeCycleNotifier");
|
|
|
|
|
|
2024-05-02 15:59:14 -05:00
|
|
|
AppLifeCycleNotifier(this._ref) : super(AppLifeCycleEnum.active);
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2024-05-02 15:59:14 -05:00
|
|
|
AppLifeCycleEnum getAppState() {
|
2023-08-12 21:02:58 +00:00
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 01:56:46 -07:00
|
|
|
Future<void> handleAppResume() async {
|
2024-05-02 15:59:14 -05:00
|
|
|
state = AppLifeCycleEnum.resumed;
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
// Prevent overlapping resume operations
|
|
|
|
|
if (_resumeOperation != null && !_resumeOperation!.isCompleted) {
|
|
|
|
|
await _resumeOperation!.future;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel any ongoing pause operation
|
|
|
|
|
if (_pauseOperation != null && !_pauseOperation!.isCompleted) {
|
|
|
|
|
_pauseOperation!.complete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_resumeOperation = Completer<void>();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await _performResume();
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
_log.severe("Error during app resume", e, stackTrace);
|
|
|
|
|
} finally {
|
|
|
|
|
if (!_resumeOperation!.isCompleted) {
|
|
|
|
|
_resumeOperation!.complete();
|
|
|
|
|
}
|
|
|
|
|
_resumeOperation = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _performResume() async {
|
2023-11-13 20:51:16 +01:00
|
|
|
// no need to resume because app was never really paused
|
2026-05-12 00:47:24 +07:00
|
|
|
if (!_wasPaused) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-11-13 20:51:16 +01:00
|
|
|
_wasPaused = false;
|
|
|
|
|
|
2024-11-26 12:43:44 -06:00
|
|
|
final isAuthenticated = _ref.read(authProvider).isAuthenticated;
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2023-11-14 21:30:27 +01:00
|
|
|
// Needs to be logged in
|
|
|
|
|
if (isAuthenticated) {
|
2024-12-05 09:11:48 -06:00
|
|
|
// switch endpoint if needed
|
2025-07-25 08:07:22 +05:30
|
|
|
final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
2025-09-05 14:39:38 -05:00
|
|
|
_log.info("Using server URL: $endpoint");
|
2024-12-05 09:11:48 -06:00
|
|
|
|
|
|
|
|
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
2025-03-24 11:56:18 -05:00
|
|
|
}
|
2024-12-05 09:11:48 -06:00
|
|
|
|
2026-08-05 00:51:28 +06:00
|
|
|
if (!_shouldContinueOperation()) {
|
|
|
|
|
_wasPaused = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-15 23:00:27 +05:30
|
|
|
_ref.read(websocketProvider.notifier).connect();
|
|
|
|
|
await _handleBetaTimelineResume();
|
2025-09-04 22:14:33 +05:30
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission();
|
2025-07-17 21:42:29 +05:30
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
|
|
|
|
|
}
|
2025-07-23 19:43:15 +05:30
|
|
|
|
2026-08-05 21:22:31 +05:30
|
|
|
Future<void> _safeRun(Future<void> Function() action, String debugName) async {
|
2025-09-10 11:50:31 -05:00
|
|
|
if (!_shouldContinueOperation()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-08-05 21:22:31 +05:30
|
|
|
await action();
|
2025-09-10 11:50:31 -05:00
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
_log.warning("Error during $debugName operation", e, stackTrace);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
Future<void> _handleBetaTimelineResume() async {
|
2025-09-22 07:15:47 +05:30
|
|
|
unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock());
|
2025-09-04 13:44:10 -05:00
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
// Give isolates time to complete any ongoing database transactions
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
|
|
|
|
|
|
final backgroundManager = _ref.read(backgroundSyncProvider);
|
2026-05-30 20:57:55 +05:30
|
|
|
final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums;
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
try {
|
2025-09-29 20:40:00 +05:30
|
|
|
bool syncSuccess = false;
|
2025-09-15 10:07:41 -05:00
|
|
|
await Future.wait([
|
2026-08-05 21:22:31 +05:30
|
|
|
_safeRun(() => backgroundManager.syncLocal(full: CurrentPlatform.isAndroid), "syncLocal"),
|
|
|
|
|
_safeRun(() async {
|
|
|
|
|
syncSuccess = await backgroundManager.syncRemote();
|
|
|
|
|
}, "syncRemote"),
|
2025-09-15 10:07:41 -05:00
|
|
|
]);
|
2026-06-18 21:16:20 +06:00
|
|
|
_ref.invalidate(driftMemoryFutureProvider);
|
2025-09-29 20:40:00 +05:30
|
|
|
if (syncSuccess) {
|
|
|
|
|
await Future.wait([
|
2026-08-05 21:22:31 +05:30
|
|
|
_safeRun(backgroundManager.hashAssets, "hashAssets").then((_) {
|
2026-07-30 12:39:24 -07:00
|
|
|
unawaited(_resumeBackup());
|
2025-09-29 20:40:00 +05:30
|
|
|
}),
|
|
|
|
|
_resumeBackup(),
|
2026-01-26 20:42:00 +05:30
|
|
|
// TODO: Bring back when the soft freeze issue is addressed
|
|
|
|
|
// _safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"),
|
2025-09-29 20:40:00 +05:30
|
|
|
]);
|
|
|
|
|
} else {
|
2026-08-05 21:22:31 +05:30
|
|
|
await _safeRun(backgroundManager.hashAssets, "hashAssets");
|
2025-09-29 20:40:00 +05:30
|
|
|
}
|
2025-09-15 10:07:41 -05:00
|
|
|
|
2025-09-10 16:17:37 -05:00
|
|
|
if (isAlbumLinkedSyncEnable) {
|
2026-08-05 21:22:31 +05:30
|
|
|
await _safeRun(backgroundManager.syncLinkedAlbum, "syncLinkedAlbum");
|
2025-09-10 16:17:37 -05:00
|
|
|
}
|
2025-09-05 14:39:38 -05:00
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
_log.severe("Error during background sync", e, stackTrace);
|
2025-07-17 21:42:29 +05:30
|
|
|
}
|
2023-08-12 21:02:58 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-15 10:07:41 -05:00
|
|
|
Future<void> _resumeBackup() async {
|
2026-05-30 20:57:55 +05:30
|
|
|
final isEnableBackup = _ref.read(appConfigProvider).backup.enabled;
|
2025-09-15 10:07:41 -05:00
|
|
|
|
|
|
|
|
if (isEnableBackup) {
|
|
|
|
|
final currentUser = Store.tryGet(StoreKey.currentUser);
|
|
|
|
|
if (currentUser != null) {
|
|
|
|
|
await _safeRun(
|
2026-08-05 21:22:31 +05:30
|
|
|
() => _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id),
|
2025-09-15 10:07:41 -05:00
|
|
|
"handleBackupResume",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
// Helper method to check if operations should continue
|
|
|
|
|
bool _shouldContinueOperation() {
|
|
|
|
|
return [AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state) &&
|
|
|
|
|
(_resumeOperation?.isCompleted == false || _resumeOperation == null);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-12 21:02:58 +00:00
|
|
|
void handleAppInactivity() {
|
2024-05-02 15:59:14 -05:00
|
|
|
state = AppLifeCycleEnum.inactive;
|
2023-11-13 20:51:16 +01:00
|
|
|
// do not stop/clean up anything on inactivity: issued on every orientation change
|
2023-08-12 21:02:58 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 22:14:33 +05:30
|
|
|
Future<void> handleAppPause() async {
|
2024-05-02 15:59:14 -05:00
|
|
|
state = AppLifeCycleEnum.paused;
|
2023-11-13 20:51:16 +01:00
|
|
|
_wasPaused = true;
|
2024-09-27 09:40:55 +07:00
|
|
|
|
2025-09-05 14:39:38 -05:00
|
|
|
// Prevent overlapping pause operations
|
|
|
|
|
if (_pauseOperation != null && !_pauseOperation!.isCompleted) {
|
|
|
|
|
await _pauseOperation!.future;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel any ongoing resume operation
|
|
|
|
|
if (_resumeOperation != null && !_resumeOperation!.isCompleted) {
|
|
|
|
|
_resumeOperation!.complete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_pauseOperation = Completer<void>();
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-15 23:00:27 +05:30
|
|
|
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
2025-09-05 14:39:38 -05:00
|
|
|
await _performPause();
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
_log.severe("Error during app pause", e, stackTrace);
|
|
|
|
|
} finally {
|
|
|
|
|
if (!_pauseOperation!.isCompleted) {
|
|
|
|
|
_pauseOperation!.complete();
|
|
|
|
|
}
|
|
|
|
|
_pauseOperation = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 12:04:45 -05:00
|
|
|
Future<void> _performPause() {
|
2025-07-17 12:08:32 -05:00
|
|
|
if (_ref.read(authProvider).isAuthenticated) {
|
2026-08-05 20:18:27 +05:30
|
|
|
_ref.read(driftBackupProvider.notifier).stopForegroundBackup(reason: "the app being sent to the background");
|
2025-07-17 12:08:32 -05:00
|
|
|
|
2024-09-27 09:40:55 +07:00
|
|
|
_ref.read(websocketProvider.notifier).disconnect();
|
2023-11-13 20:51:16 +01:00
|
|
|
}
|
2024-09-27 09:40:55 +07:00
|
|
|
|
2026-03-05 12:04:45 -05:00
|
|
|
return LogService.I.flush().catchError((_) {});
|
2023-08-12 21:02:58 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-05 19:58:40 +05:30
|
|
|
Future<void> handleAppDetached() async {
|
2024-05-02 15:59:14 -05:00
|
|
|
state = AppLifeCycleEnum.detached;
|
2025-07-17 21:42:29 +05:30
|
|
|
|
2026-04-15 23:00:27 +05:30
|
|
|
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
2025-09-22 07:15:47 +05:30
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
// Flush logs before closing database
|
|
|
|
|
try {
|
2025-10-27 20:02:52 +05:30
|
|
|
await LogService.I.flush();
|
2025-09-05 14:39:38 -05:00
|
|
|
} catch (_) {}
|
2023-08-12 21:02:58 +00:00
|
|
|
}
|
2023-08-18 18:52:40 -04:00
|
|
|
|
|
|
|
|
void handleAppHidden() {
|
2024-05-02 15:59:14 -05:00
|
|
|
state = AppLifeCycleEnum.hidden;
|
2023-11-13 20:51:16 +01:00
|
|
|
// do not stop/clean up anything on inactivity: issued on every orientation change
|
2023-08-18 18:52:40 -04:00
|
|
|
}
|
2023-08-12 21:02:58 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
final appStateProvider = StateNotifierProvider<AppLifeCycleNotifier, AppLifeCycleEnum>((ref) {
|
2024-05-02 15:59:14 -05:00
|
|
|
return AppLifeCycleNotifier(ref);
|
2022-02-03 10:06:44 -06:00
|
|
|
});
|