backup.provider.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. import 'package:cancellation_token_http/http.dart';
  2. import 'package:collection/collection.dart';
  3. import 'package:flutter/widgets.dart';
  4. import 'package:hooks_riverpod/hooks_riverpod.dart';
  5. import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
  6. import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
  7. import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
  8. import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
  9. import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
  10. import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
  11. import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
  12. import 'package:immich_mobile/modules/backup/services/backup.service.dart';
  13. import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
  14. import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
  15. import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
  16. import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart';
  17. import 'package:immich_mobile/shared/models/store.dart';
  18. import 'package:immich_mobile/shared/providers/app_state.provider.dart';
  19. import 'package:immich_mobile/shared/providers/db.provider.dart';
  20. import 'package:immich_mobile/shared/services/server_info.service.dart';
  21. import 'package:immich_mobile/utils/diff.dart';
  22. import 'package:isar/isar.dart';
  23. import 'package:logging/logging.dart';
  24. import 'package:permission_handler/permission_handler.dart';
  25. import 'package:photo_manager/photo_manager.dart';
  26. class BackupNotifier extends StateNotifier<BackUpState> {
  27. BackupNotifier(
  28. this._backupService,
  29. this._serverInfoService,
  30. this._authState,
  31. this._backgroundService,
  32. this._galleryPermissionNotifier,
  33. this._db,
  34. this.ref,
  35. ) : super(
  36. BackUpState(
  37. backupProgress: BackUpProgressEnum.idle,
  38. allAssetsInDatabase: const [],
  39. progressInPercentage: 0,
  40. cancelToken: CancellationToken(),
  41. autoBackup: Store.get(StoreKey.autoBackup, false),
  42. backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
  43. backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
  44. backupRequireCharging:
  45. Store.get(StoreKey.backupRequireCharging, false),
  46. backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000),
  47. serverInfo: const ServerDiskInfo(
  48. diskAvailable: "0",
  49. diskSize: "0",
  50. diskUse: "0",
  51. diskUsagePercentage: 0,
  52. ),
  53. availableAlbums: const [],
  54. selectedBackupAlbums: const {},
  55. excludedBackupAlbums: const {},
  56. allUniqueAssets: const {},
  57. selectedAlbumsBackupAssetsIds: const {},
  58. currentUploadAsset: CurrentUploadAsset(
  59. id: '...',
  60. fileCreatedAt: DateTime.parse('2020-10-04'),
  61. fileName: '...',
  62. fileType: '...',
  63. ),
  64. ),
  65. );
  66. final log = Logger('BackupNotifier');
  67. final BackupService _backupService;
  68. final ServerInfoService _serverInfoService;
  69. final AuthenticationState _authState;
  70. final BackgroundService _backgroundService;
  71. final GalleryPermissionNotifier _galleryPermissionNotifier;
  72. final Isar _db;
  73. final Ref ref;
  74. ///
  75. /// UI INTERACTION
  76. ///
  77. /// Album selection
  78. /// Due to the overlapping assets across multiple albums on the device
  79. /// We have method to include and exclude albums
  80. /// The total unique assets will be used for backing mechanism
  81. ///
  82. void addAlbumForBackup(AvailableAlbum album) {
  83. if (state.excludedBackupAlbums.contains(album)) {
  84. removeExcludedAlbumForBackup(album);
  85. }
  86. state = state
  87. .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
  88. _updateBackupAssetCount();
  89. }
  90. void addExcludedAlbumForBackup(AvailableAlbum album) {
  91. if (state.selectedBackupAlbums.contains(album)) {
  92. removeAlbumForBackup(album);
  93. }
  94. state = state
  95. .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
  96. _updateBackupAssetCount();
  97. }
  98. void removeAlbumForBackup(AvailableAlbum album) {
  99. Set<AvailableAlbum> currentSelectedAlbums = state.selectedBackupAlbums;
  100. currentSelectedAlbums.removeWhere((a) => a == album);
  101. state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
  102. _updateBackupAssetCount();
  103. }
  104. void removeExcludedAlbumForBackup(AvailableAlbum album) {
  105. Set<AvailableAlbum> currentExcludedAlbums = state.excludedBackupAlbums;
  106. currentExcludedAlbums.removeWhere((a) => a == album);
  107. state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
  108. _updateBackupAssetCount();
  109. }
  110. void setAutoBackup(bool enabled) {
  111. Store.put(StoreKey.autoBackup, enabled);
  112. state = state.copyWith(autoBackup: enabled);
  113. }
  114. void configureBackgroundBackup({
  115. bool? enabled,
  116. bool? requireWifi,
  117. bool? requireCharging,
  118. int? triggerDelay,
  119. required void Function(String msg) onError,
  120. required void Function() onBatteryInfo,
  121. }) async {
  122. assert(
  123. enabled != null ||
  124. requireWifi != null ||
  125. requireCharging != null ||
  126. triggerDelay != null,
  127. );
  128. final bool wasEnabled = state.backgroundBackup;
  129. final bool wasWifi = state.backupRequireWifi;
  130. final bool wasCharging = state.backupRequireCharging;
  131. final int oldTriggerDelay = state.backupTriggerDelay;
  132. state = state.copyWith(
  133. backgroundBackup: enabled,
  134. backupRequireWifi: requireWifi,
  135. backupRequireCharging: requireCharging,
  136. backupTriggerDelay: triggerDelay,
  137. );
  138. if (state.backgroundBackup) {
  139. bool success = true;
  140. if (!wasEnabled) {
  141. if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
  142. onBatteryInfo();
  143. }
  144. success &= await _backgroundService.enableService(immediate: true);
  145. }
  146. success &= success &&
  147. await _backgroundService.configureService(
  148. requireUnmetered: state.backupRequireWifi,
  149. requireCharging: state.backupRequireCharging,
  150. triggerUpdateDelay: state.backupTriggerDelay,
  151. triggerMaxDelay: state.backupTriggerDelay * 10,
  152. );
  153. if (success) {
  154. await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi);
  155. await Store.put(
  156. StoreKey.backupRequireCharging,
  157. state.backupRequireCharging,
  158. );
  159. await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
  160. await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
  161. } else {
  162. state = state.copyWith(
  163. backgroundBackup: wasEnabled,
  164. backupRequireWifi: wasWifi,
  165. backupRequireCharging: wasCharging,
  166. backupTriggerDelay: oldTriggerDelay,
  167. );
  168. onError("backup_controller_page_background_configure_error");
  169. }
  170. } else {
  171. final bool success = await _backgroundService.disableService();
  172. if (!success) {
  173. state = state.copyWith(backgroundBackup: wasEnabled);
  174. onError("backup_controller_page_background_configure_error");
  175. }
  176. }
  177. }
  178. ///
  179. /// Get all album on the device
  180. /// Get all selected and excluded album from the user's persistent storage
  181. /// If this is the first time performing backup - set the default selected album to be
  182. /// the one that has all assets (`Recent` on Android, `Recents` on iOS)
  183. ///
  184. Future<void> _getBackupAlbumsInfo() async {
  185. Stopwatch stopwatch = Stopwatch()..start();
  186. // Get all albums on the device
  187. List<AvailableAlbum> availableAlbums = [];
  188. List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
  189. hasAll: true,
  190. type: RequestType.common,
  191. );
  192. // Map of id -> album for quick album lookup later on.
  193. Map<String, AssetPathEntity> albumMap = {};
  194. log.info('Found ${albums.length} local albums');
  195. for (AssetPathEntity album in albums) {
  196. AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
  197. final assetCountInAlbum = await album.assetCountAsync;
  198. if (assetCountInAlbum > 0) {
  199. final assetList = await album.getAssetListPaged(page: 0, size: 1);
  200. // Even though we check assetCountInAlbum to make sure that there are assets in album
  201. // The `getAssetListPaged` method still return empty list and cause not assets get rendered
  202. if (assetList.isEmpty) {
  203. continue;
  204. }
  205. final thumbnailAsset = assetList.first;
  206. try {
  207. final thumbnailData = await thumbnailAsset
  208. .thumbnailDataWithSize(const ThumbnailSize(512, 512));
  209. availableAlbum =
  210. availableAlbum.copyWith(thumbnailData: thumbnailData);
  211. } catch (e, stack) {
  212. log.severe(
  213. "Failed to get thumbnail for album ${album.name}",
  214. e.toString(),
  215. stack,
  216. );
  217. }
  218. availableAlbums.add(availableAlbum);
  219. albumMap[album.id] = album;
  220. }
  221. }
  222. state = state.copyWith(availableAlbums: availableAlbums);
  223. final List<BackupAlbum> excludedBackupAlbums =
  224. await _backupService.excludedAlbumsQuery().findAll();
  225. final List<BackupAlbum> selectedBackupAlbums =
  226. await _backupService.selectedAlbumsQuery().findAll();
  227. // First time backup - set isAll album is the default one for backup.
  228. if (selectedBackupAlbums.isEmpty) {
  229. log.info("First time backup; setup 'Recent(s)' album as default");
  230. // Get album that contains all assets
  231. final list = await PhotoManager.getAssetPathList(
  232. hasAll: true,
  233. onlyAll: true,
  234. type: RequestType.common,
  235. );
  236. if (list.isEmpty) {
  237. return;
  238. }
  239. AssetPathEntity albumHasAllAssets = list.first;
  240. final ba = BackupAlbum(
  241. albumHasAllAssets.id,
  242. DateTime.fromMillisecondsSinceEpoch(0),
  243. BackupSelection.select,
  244. );
  245. await _db.writeTxn(() => _db.backupAlbums.put(ba));
  246. }
  247. // Generate AssetPathEntity from id to add to local state
  248. final Set<AvailableAlbum> selectedAlbums = {};
  249. for (final BackupAlbum ba in selectedBackupAlbums) {
  250. final albumAsset = albumMap[ba.id];
  251. if (albumAsset != null) {
  252. selectedAlbums.add(
  253. AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
  254. );
  255. } else {
  256. log.severe('Selected album not found');
  257. }
  258. }
  259. final Set<AvailableAlbum> excludedAlbums = {};
  260. for (final BackupAlbum ba in excludedBackupAlbums) {
  261. final albumAsset = albumMap[ba.id];
  262. if (albumAsset != null) {
  263. excludedAlbums.add(
  264. AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
  265. );
  266. } else {
  267. log.severe('Excluded album not found');
  268. }
  269. }
  270. state = state.copyWith(
  271. selectedBackupAlbums: selectedAlbums,
  272. excludedBackupAlbums: excludedAlbums,
  273. );
  274. debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
  275. }
  276. ///
  277. /// From all the selected and albums assets
  278. /// Find the assets that are not overlapping between the two sets
  279. /// Those assets are unique and are used as the total assets
  280. ///
  281. Future<void> _updateBackupAssetCount() async {
  282. final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
  283. final Set<AssetEntity> assetsFromSelectedAlbums = {};
  284. final Set<AssetEntity> assetsFromExcludedAlbums = {};
  285. for (final album in state.selectedBackupAlbums) {
  286. final assets = await album.albumEntity.getAssetListRange(
  287. start: 0,
  288. end: await album.albumEntity.assetCountAsync,
  289. );
  290. assetsFromSelectedAlbums.addAll(assets);
  291. }
  292. for (final album in state.excludedBackupAlbums) {
  293. final assets = await album.albumEntity.getAssetListRange(
  294. start: 0,
  295. end: await album.albumEntity.assetCountAsync,
  296. );
  297. assetsFromExcludedAlbums.addAll(assets);
  298. }
  299. final Set<AssetEntity> allUniqueAssets =
  300. assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
  301. final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
  302. if (allAssetsInDatabase == null) {
  303. return;
  304. }
  305. // Find asset that were backup from selected albums
  306. final Set<String> selectedAlbumsBackupAssets =
  307. Set.from(allUniqueAssets.map((e) => e.id));
  308. selectedAlbumsBackupAssets
  309. .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
  310. // Remove duplicated asset from all unique assets
  311. allUniqueAssets.removeWhere(
  312. (asset) => duplicatedAssetIds.contains(asset.id),
  313. );
  314. if (allUniqueAssets.isEmpty) {
  315. log.info("Not found albums or assets on the device to backup");
  316. state = state.copyWith(
  317. backupProgress: BackUpProgressEnum.idle,
  318. allAssetsInDatabase: allAssetsInDatabase,
  319. allUniqueAssets: {},
  320. selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
  321. );
  322. return;
  323. } else {
  324. state = state.copyWith(
  325. allAssetsInDatabase: allAssetsInDatabase,
  326. allUniqueAssets: allUniqueAssets,
  327. selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
  328. );
  329. }
  330. // Save to persistent storage
  331. await _updatePersistentAlbumsSelection();
  332. return;
  333. }
  334. /// Get all necessary information for calculating the available albums,
  335. /// which albums are selected or excluded
  336. /// and then update the UI according to those information
  337. Future<void> getBackupInfo() async {
  338. final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
  339. state = state.copyWith(backgroundBackup: isEnabled);
  340. if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
  341. Store.put(StoreKey.backgroundBackup, isEnabled);
  342. }
  343. if (state.backupProgress != BackUpProgressEnum.inBackground) {
  344. await _getBackupAlbumsInfo();
  345. await updateServerInfo();
  346. await _updateBackupAssetCount();
  347. }
  348. }
  349. /// Save user selection of selected albums and excluded albums to database
  350. Future<void> _updatePersistentAlbumsSelection() {
  351. final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
  352. final selected = state.selectedBackupAlbums.map(
  353. (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
  354. );
  355. final excluded = state.excludedBackupAlbums.map(
  356. (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude),
  357. );
  358. final backupAlbums = selected.followedBy(excluded).toList();
  359. backupAlbums.sortBy((e) => e.id);
  360. return _db.writeTxn(() async {
  361. final dbAlbums = await _db.backupAlbums.where().sortById().findAll();
  362. final List<int> toDelete = [];
  363. final List<BackupAlbum> toUpsert = [];
  364. // stores the most recent `lastBackup` per album but always keeps the `selection` the user just made
  365. diffSortedListsSync(
  366. dbAlbums,
  367. backupAlbums,
  368. compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
  369. both: (BackupAlbum a, BackupAlbum b) {
  370. b.lastBackup =
  371. a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup;
  372. toUpsert.add(b);
  373. return true;
  374. },
  375. onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId),
  376. onlySecond: (BackupAlbum b) => toUpsert.add(b),
  377. );
  378. await _db.backupAlbums.deleteAll(toDelete);
  379. await _db.backupAlbums.putAll(toUpsert);
  380. });
  381. }
  382. /// Invoke backup process
  383. Future<void> startBackupProcess() async {
  384. debugPrint("Start backup process");
  385. assert(state.backupProgress == BackUpProgressEnum.idle);
  386. state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
  387. await getBackupInfo();
  388. final hasPermission = _galleryPermissionNotifier.hasPermission;
  389. if (hasPermission) {
  390. await PhotoManager.clearFileCache();
  391. if (state.allUniqueAssets.isEmpty) {
  392. log.info("No Asset On Device - Abort Backup Process");
  393. state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
  394. return;
  395. }
  396. Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
  397. // Remove item that has already been backed up
  398. for (final assetId in state.allAssetsInDatabase) {
  399. assetsWillBeBackup.removeWhere((e) => e.id == assetId);
  400. }
  401. if (assetsWillBeBackup.isEmpty) {
  402. state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
  403. }
  404. // Perform Backup
  405. state = state.copyWith(cancelToken: CancellationToken());
  406. await _backupService.backupAsset(
  407. assetsWillBeBackup,
  408. state.cancelToken,
  409. _onAssetUploaded,
  410. _onUploadProgress,
  411. _onSetCurrentBackupAsset,
  412. _onBackupError,
  413. );
  414. await notifyBackgroundServiceCanRun();
  415. } else {
  416. openAppSettings();
  417. }
  418. }
  419. void setAvailableAlbums(availableAlbums) {
  420. state = state.copyWith(
  421. availableAlbums: availableAlbums,
  422. );
  423. }
  424. void _onBackupError(ErrorUploadAsset errorAssetInfo) {
  425. ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
  426. }
  427. void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
  428. state = state.copyWith(currentUploadAsset: currentUploadAsset);
  429. }
  430. void cancelBackup() {
  431. if (state.backupProgress != BackUpProgressEnum.inProgress) {
  432. notifyBackgroundServiceCanRun();
  433. }
  434. state.cancelToken.cancel();
  435. state = state.copyWith(
  436. backupProgress: BackUpProgressEnum.idle,
  437. progressInPercentage: 0.0,
  438. );
  439. }
  440. void _onAssetUploaded(
  441. String deviceAssetId,
  442. String deviceId,
  443. bool isDuplicated,
  444. ) {
  445. if (isDuplicated) {
  446. state = state.copyWith(
  447. allUniqueAssets: state.allUniqueAssets
  448. .where((asset) => asset.id != deviceAssetId)
  449. .toSet(),
  450. );
  451. } else {
  452. state = state.copyWith(
  453. selectedAlbumsBackupAssetsIds: {
  454. ...state.selectedAlbumsBackupAssetsIds,
  455. deviceAssetId,
  456. },
  457. allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
  458. );
  459. }
  460. if (state.allUniqueAssets.length -
  461. state.selectedAlbumsBackupAssetsIds.length ==
  462. 0) {
  463. final latestAssetBackup =
  464. state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
  465. (v, e) => e.isAfter(v) ? e : v,
  466. );
  467. state = state.copyWith(
  468. selectedBackupAlbums: state.selectedBackupAlbums
  469. .map((e) => e.copyWith(lastBackup: latestAssetBackup))
  470. .toSet(),
  471. excludedBackupAlbums: state.excludedBackupAlbums
  472. .map((e) => e.copyWith(lastBackup: latestAssetBackup))
  473. .toSet(),
  474. backupProgress: BackUpProgressEnum.done,
  475. progressInPercentage: 0.0,
  476. );
  477. _updatePersistentAlbumsSelection();
  478. }
  479. updateServerInfo();
  480. }
  481. void _onUploadProgress(int sent, int total) {
  482. state = state.copyWith(
  483. progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
  484. );
  485. }
  486. Future<void> updateServerInfo() async {
  487. final serverInfo = await _serverInfoService.getServerInfo();
  488. // Update server info
  489. if (serverInfo != null) {
  490. state = state.copyWith(
  491. serverInfo: serverInfo,
  492. );
  493. }
  494. }
  495. Future<void> _resumeBackup() async {
  496. // Check if user is login
  497. final accessKey = Store.tryGet(StoreKey.accessToken);
  498. // User has been logged out return
  499. if (accessKey == null || !_authState.isAuthenticated) {
  500. log.info("[_resumeBackup] not authenticated - abort");
  501. return;
  502. }
  503. // Check if this device is enable backup by the user
  504. if (state.autoBackup) {
  505. // check if backup is already in process - then return
  506. if (state.backupProgress == BackUpProgressEnum.inProgress) {
  507. log.info("[_resumeBackup] Auto Backup is already in progress - abort");
  508. return;
  509. }
  510. if (state.backupProgress == BackUpProgressEnum.inBackground) {
  511. log.info("[_resumeBackup] Background backup is running - abort");
  512. return;
  513. }
  514. if (state.backupProgress == BackUpProgressEnum.manualInProgress) {
  515. log.info("[_resumeBackup] Manual upload is running - abort");
  516. return;
  517. }
  518. // Run backup
  519. log.info("[_resumeBackup] Start back up");
  520. await startBackupProcess();
  521. }
  522. return;
  523. }
  524. Future<void> resumeBackup() async {
  525. final List<BackupAlbum> selectedBackupAlbums = await _db.backupAlbums
  526. .filter()
  527. .selectionEqualTo(BackupSelection.select)
  528. .findAll();
  529. final List<BackupAlbum> excludedBackupAlbums = await _db.backupAlbums
  530. .filter()
  531. .selectionEqualTo(BackupSelection.exclude)
  532. .findAll();
  533. Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
  534. Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
  535. if (selectedAlbums.isNotEmpty) {
  536. selectedAlbums = _updateAlbumsBackupTime(
  537. selectedAlbums,
  538. selectedBackupAlbums,
  539. );
  540. }
  541. if (excludedAlbums.isNotEmpty) {
  542. excludedAlbums = _updateAlbumsBackupTime(
  543. excludedAlbums,
  544. excludedBackupAlbums,
  545. );
  546. }
  547. final BackUpProgressEnum previous = state.backupProgress;
  548. state = state.copyWith(
  549. backupProgress: BackUpProgressEnum.inBackground,
  550. selectedBackupAlbums: selectedAlbums,
  551. excludedBackupAlbums: excludedAlbums,
  552. );
  553. // assumes the background service is currently running
  554. // if true, waits until it has stopped to start the backup
  555. final bool hasLock = await _backgroundService.acquireLock();
  556. if (hasLock) {
  557. state = state.copyWith(backupProgress: previous);
  558. }
  559. return _resumeBackup();
  560. }
  561. Set<AvailableAlbum> _updateAlbumsBackupTime(
  562. Set<AvailableAlbum> albums,
  563. List<BackupAlbum> backupAlbums,
  564. ) {
  565. Set<AvailableAlbum> result = {};
  566. for (BackupAlbum ba in backupAlbums) {
  567. try {
  568. AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id);
  569. result.add(a.copyWith(lastBackup: ba.lastBackup));
  570. } on StateError {
  571. log.severe(
  572. "[_updateAlbumBackupTime] failed to find album in state",
  573. "State Error",
  574. StackTrace.current,
  575. );
  576. }
  577. }
  578. return result;
  579. }
  580. Future<void> notifyBackgroundServiceCanRun() async {
  581. const allowedStates = [
  582. AppStateEnum.inactive,
  583. AppStateEnum.paused,
  584. AppStateEnum.detached,
  585. ];
  586. if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
  587. _backgroundService.releaseLock();
  588. }
  589. }
  590. BackUpProgressEnum get backupProgress => state.backupProgress;
  591. void updateBackupProgress(BackUpProgressEnum backupProgress) {
  592. state = state.copyWith(backupProgress: backupProgress);
  593. }
  594. }
  595. final backupProvider =
  596. StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
  597. return BackupNotifier(
  598. ref.watch(backupServiceProvider),
  599. ref.watch(serverInfoServiceProvider),
  600. ref.watch(authenticationProvider),
  601. ref.watch(backgroundServiceProvider),
  602. ref.watch(galleryPermissionNotifier.notifier),
  603. ref.watch(dbProvider),
  604. ref,
  605. );
  606. });