2025-02-28 01:48:49 +05:30
|
|
|
import 'dart:async';
|
|
|
|
|
|
2024-12-05 09:11:48 -06:00
|
|
|
import 'package:flutter/foundation.dart';
|
2022-02-03 10:06:44 -06:00
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
2025-09-04 22:14:33 +05:30
|
|
|
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
2025-07-17 21:42:29 +05:30
|
|
|
import 'package:immich_mobile/entities/store.entity.dart';
|
2024-04-30 21:36:40 -05:00
|
|
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
2025-07-21 15:30:51 -05:00
|
|
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
|
|
|
|
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';
|
2024-05-02 15:59:14 -05:00
|
|
|
import 'package:immich_mobile/providers/backup/backup.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/backup/ios_background_settings.provider.dart';
|
|
|
|
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
|
|
|
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
2024-05-02 15:59:14 -05:00
|
|
|
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
|
|
|
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
|
|
|
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
2025-07-23 19:43:15 +05:30
|
|
|
import 'package:immich_mobile/providers/user.provider.dart';
|
2024-05-02 15:59:14 -05:00
|
|
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
2025-07-21 15:30:51 -05:00
|
|
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
2025-02-28 01:48:49 +05:30
|
|
|
import 'package:immich_mobile/services/background.service.dart';
|
2025-03-05 19:58:40 +05:30
|
|
|
import 'package:isar/isar.dart';
|
2025-07-17 21:42:29 +05:30
|
|
|
import 'package:logging/logging.dart';
|
2023-08-12 21:02:58 +00:00
|
|
|
import 'package:permission_handler/permission_handler.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
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-05 09:11:48 -06:00
|
|
|
void handleAppResume() async {
|
2024-05-02 15:59:14 -05:00
|
|
|
state = AppLifeCycleEnum.resumed;
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2023-11-13 20:51:16 +01:00
|
|
|
// no need to resume because app was never really paused
|
|
|
|
|
if (!_wasPaused) return;
|
|
|
|
|
_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();
|
2024-12-05 09:11:48 -06:00
|
|
|
if (kDebugMode) {
|
|
|
|
|
debugPrint("Using server URL: $endpoint");
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
if (!Store.isBetaTimelineEnabled) {
|
|
|
|
|
final permission = _ref.watch(galleryPermissionNotifier);
|
|
|
|
|
if (permission.isGranted || permission.isLimited) {
|
|
|
|
|
await _ref.read(backupProvider.notifier).resumeBackup();
|
|
|
|
|
await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
|
|
|
|
}
|
2023-11-14 21:30:27 +01:00
|
|
|
}
|
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
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
if (!Store.isBetaTimelineEnabled) {
|
|
|
|
|
switch (_ref.read(tabProvider)) {
|
|
|
|
|
case TabEnum.home:
|
|
|
|
|
await _ref.read(assetProvider.notifier).getAllAsset();
|
|
|
|
|
|
|
|
|
|
case TabEnum.albums:
|
|
|
|
|
await _ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
2025-07-21 15:30:51 -05:00
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
case TabEnum.library:
|
2025-07-21 15:30:51 -05:00
|
|
|
case TabEnum.search:
|
2025-07-17 21:42:29 +05:30
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
_ref.read(backupProvider.notifier).cancelBackup();
|
2025-09-04 22:14:33 +05:30
|
|
|
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
|
|
|
|
|
|
|
|
|
lockManager.requestHolderToClose();
|
|
|
|
|
debugPrint("Requested lock holder to close on resume");
|
|
|
|
|
await lockManager.acquireLock();
|
|
|
|
|
debugPrint("Lock acquired for background sync on resume");
|
2025-07-17 21:42:29 +05:30
|
|
|
|
|
|
|
|
final backgroundManager = _ref.read(backgroundSyncProvider);
|
|
|
|
|
// Ensure proper cleanup before starting new background tasks
|
|
|
|
|
try {
|
|
|
|
|
await Future.wait([
|
2025-08-04 22:41:44 +01:00
|
|
|
Future(() async {
|
|
|
|
|
await backgroundManager.syncLocal();
|
2025-07-29 00:34:03 +05:30
|
|
|
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
|
|
|
|
// Check if app is still active before hashing
|
2025-08-04 22:41:44 +01:00
|
|
|
if ([AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state)) {
|
|
|
|
|
await backgroundManager.hashAssets();
|
2025-07-29 00:34:03 +05:30
|
|
|
}
|
|
|
|
|
}),
|
2025-07-17 21:42:29 +05:30
|
|
|
backgroundManager.syncRemote(),
|
2025-07-21 15:30:51 -05:00
|
|
|
]).then((_) async {
|
2025-07-25 08:07:22 +05:30
|
|
|
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
2025-07-21 15:30:51 -05:00
|
|
|
|
2025-09-04 13:44:10 -05:00
|
|
|
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
|
|
|
|
|
2025-07-21 15:30:51 -05:00
|
|
|
if (isEnableBackup) {
|
2025-07-23 19:43:15 +05:30
|
|
|
final currentUser = _ref.read(currentUserProvider);
|
|
|
|
|
if (currentUser == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
2025-07-21 15:30:51 -05:00
|
|
|
}
|
2025-09-04 13:44:10 -05:00
|
|
|
|
|
|
|
|
if (isAlbumLinkedSyncEnable) {
|
|
|
|
|
await backgroundManager.syncLinkedAlbum();
|
|
|
|
|
}
|
2025-07-21 15:30:51 -05:00
|
|
|
});
|
2025-07-17 21:42:29 +05:30
|
|
|
} catch (e, stackTrace) {
|
2025-07-29 00:34:03 +05:30
|
|
|
Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace);
|
2025-07-17 21:42:29 +05:30
|
|
|
}
|
2023-08-12 21:02:58 +00:00
|
|
|
}
|
|
|
|
|
|
2023-11-13 20:51:16 +01:00
|
|
|
_ref.read(websocketProvider.notifier).connect();
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission();
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
|
2024-12-05 09:11:48 -06:00
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
if (!Store.isBetaTimelineEnabled) {
|
|
|
|
|
await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
|
2023-08-12 21:02:58 +00:00
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
_ref.invalidate(memoryFutureProvider);
|
|
|
|
|
}
|
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-07-17 12:08:32 -05:00
|
|
|
if (_ref.read(authProvider).isAuthenticated) {
|
|
|
|
|
if (!Store.isBetaTimelineEnabled) {
|
|
|
|
|
// Do not cancel backup if manual upload is in progress
|
2025-07-25 08:07:22 +05:30
|
|
|
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
2025-07-17 12:08:32 -05:00
|
|
|
_ref.read(backupProvider.notifier).cancelBackup();
|
|
|
|
|
}
|
2025-09-04 22:14:33 +05:30
|
|
|
} else {
|
|
|
|
|
final backgroundManager = _ref.read(backgroundSyncProvider);
|
|
|
|
|
await backgroundManager.cancel();
|
|
|
|
|
await backgroundManager.cancelLocal();
|
|
|
|
|
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
|
|
|
|
debugPrint("Lock released on app pause");
|
2024-09-27 09:40:55 +07:00
|
|
|
}
|
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
|
|
|
|
2025-07-17 21:42:29 +05:30
|
|
|
try {
|
|
|
|
|
LogService.I.flush();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Ignore flush errors during pause
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
// Flush logs before closing database
|
|
|
|
|
try {
|
|
|
|
|
LogService.I.flush();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Ignore flush errors during shutdown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close Isar database safely
|
|
|
|
|
try {
|
|
|
|
|
final isar = Isar.getInstance();
|
|
|
|
|
if (isar != null && isar.isOpen) {
|
|
|
|
|
await isar.close();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Ignore close errors during shutdown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Store.isBetaTimelineEnabled) {
|
2025-09-04 22:14:33 +05:30
|
|
|
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
2025-07-17 21:42:29 +05:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 20:51:16 +01:00
|
|
|
// no guarantee this is called at all
|
2025-07-17 21:42:29 +05:30
|
|
|
try {
|
|
|
|
|
_ref.read(manualUploadProvider.notifier).cancelBackup();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Ignore errors during shutdown
|
|
|
|
|
}
|
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
|
|
|
});
|