backup.provider.dart 22 KB

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