backup.provider.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import 'package:dio/dio.dart';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:hive_flutter/hive_flutter.dart';
  4. import 'package:hooks_riverpod/hooks_riverpod.dart';
  5. import 'package:immich_mobile/constants/hive_box.dart';
  6. import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
  7. import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
  8. import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
  9. import 'package:immich_mobile/shared/services/server_info.service.dart';
  10. import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
  11. import 'package:immich_mobile/shared/models/server_info.model.dart';
  12. import 'package:immich_mobile/modules/backup/services/backup.service.dart';
  13. import 'package:photo_manager/photo_manager.dart';
  14. class BackupNotifier extends StateNotifier<BackUpState> {
  15. BackupNotifier({this.ref})
  16. : super(
  17. BackUpState(
  18. backupProgress: BackUpProgressEnum.idle,
  19. allAssetOnDatabase: const [],
  20. progressInPercentage: 0,
  21. cancelToken: CancelToken(),
  22. serverInfo: ServerInfo(
  23. diskAvailable: "0",
  24. diskAvailableRaw: 0,
  25. diskSize: "0",
  26. diskSizeRaw: 0,
  27. diskUsagePercentage: 0.0,
  28. diskUse: "0",
  29. diskUseRaw: 0,
  30. ),
  31. availableAlbums: const [],
  32. selectedBackupAlbums: const {},
  33. excludedBackupAlbums: const {},
  34. allUniqueAssets: const {},
  35. selectedAlbumsBackupAssetsIds: const {},
  36. ),
  37. );
  38. Ref? ref;
  39. final BackupService _backupService = BackupService();
  40. final ServerInfoService _serverInfoService = ServerInfoService();
  41. ///
  42. /// UI INTERACTION
  43. ///
  44. /// Album selection
  45. /// Due to the overlapping assets across multiple albums on the device
  46. /// We have method to include and exclude albums
  47. /// The total unique assets will be used for backing mechanism
  48. ///
  49. void addAlbumForBackup(AssetPathEntity album) {
  50. if (state.excludedBackupAlbums.contains(album)) {
  51. removeExcludedAlbumForBackup(album);
  52. }
  53. state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
  54. _updateBackupAssetCount();
  55. }
  56. void addExcludedAlbumForBackup(AssetPathEntity album) {
  57. if (state.selectedBackupAlbums.contains(album)) {
  58. removeAlbumForBackup(album);
  59. }
  60. state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
  61. _updateBackupAssetCount();
  62. }
  63. void removeAlbumForBackup(AssetPathEntity album) {
  64. Set<AssetPathEntity> currentSelectedAlbums = state.selectedBackupAlbums;
  65. currentSelectedAlbums.removeWhere((a) => a == album);
  66. state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
  67. _updateBackupAssetCount();
  68. }
  69. void removeExcludedAlbumForBackup(AssetPathEntity album) {
  70. Set<AssetPathEntity> currentExcludedAlbums = state.excludedBackupAlbums;
  71. currentExcludedAlbums.removeWhere((a) => a == album);
  72. state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
  73. _updateBackupAssetCount();
  74. }
  75. ///
  76. /// Get all album on the device
  77. /// Get all selected and excluded album from the user's persistent storage
  78. /// If this is the first time performing backup - set the default selected album to be
  79. /// the one that has all assets (Recent on Android, Recents on iOS)
  80. ///
  81. Future<void> getBackupAlbumsInfo() async {
  82. // Get all albums on the device
  83. List<AvailableAlbum> availableAlbums = [];
  84. List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(hasAll: true, type: RequestType.common);
  85. for (AssetPathEntity album in albums) {
  86. AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
  87. var assetList = await album.getAssetListRange(start: 0, end: album.assetCount);
  88. if (assetList.isNotEmpty) {
  89. var thumbnailAsset = assetList.first;
  90. var thumbnailData = await thumbnailAsset.thumbnailDataWithSize(const ThumbnailSize(512, 512));
  91. availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
  92. }
  93. availableAlbums.add(availableAlbum);
  94. }
  95. state = state.copyWith(availableAlbums: availableAlbums);
  96. // Put persistent storage info into local state of the app
  97. // Get local storage on selected backup album
  98. Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
  99. HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
  100. backupInfoKey,
  101. defaultValue: HiveBackupAlbums(
  102. selectedAlbumIds: [],
  103. excludedAlbumsIds: [],
  104. ),
  105. );
  106. if (backupAlbumInfo == null) {
  107. debugPrint("[ERROR] getting Hive backup album infomation");
  108. return;
  109. }
  110. // First time backup - set isAll album is the default one for backup.
  111. if (backupAlbumInfo.selectedAlbumIds.isEmpty) {
  112. debugPrint("First time backup setup recent album as default");
  113. // Get album that contains all assets
  114. var list = await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
  115. AssetPathEntity albumHasAllAssets = list.first;
  116. backupAlbumInfoBox.put(
  117. backupInfoKey,
  118. HiveBackupAlbums(
  119. selectedAlbumIds: [albumHasAllAssets.id],
  120. excludedAlbumsIds: [],
  121. ),
  122. );
  123. backupAlbumInfo = backupAlbumInfoBox.get(backupInfoKey);
  124. }
  125. // Generate AssetPathEntity from id to add to local state
  126. try {
  127. for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
  128. var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
  129. state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
  130. }
  131. for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
  132. var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
  133. state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
  134. }
  135. } catch (e) {
  136. debugPrint("[ERROR] Failed to generate album from id $e");
  137. }
  138. }
  139. ///
  140. /// From all the selected and albums assets
  141. /// Find the assets that are not overlapping between the two sets
  142. /// Those assets are unique and are used as the total assets
  143. ///
  144. void _updateBackupAssetCount() async {
  145. Set<AssetEntity> assetsFromSelectedAlbums = {};
  146. Set<AssetEntity> assetsFromExcludedAlbums = {};
  147. for (var album in state.selectedBackupAlbums) {
  148. var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
  149. assetsFromSelectedAlbums.addAll(assets);
  150. }
  151. for (var album in state.excludedBackupAlbums) {
  152. var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
  153. assetsFromExcludedAlbums.addAll(assets);
  154. }
  155. Set<AssetEntity> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
  156. List<String> allAssetOnDatabase = await _backupService.getDeviceBackupAsset();
  157. // Find asset that were backup from selected albums
  158. Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.id));
  159. selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
  160. if (allUniqueAssets.isEmpty) {
  161. debugPrint("No Asset On Device");
  162. state = state.copyWith(
  163. backupProgress: BackUpProgressEnum.idle,
  164. allAssetOnDatabase: allAssetOnDatabase,
  165. allUniqueAssets: {},
  166. selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
  167. );
  168. return;
  169. } else {
  170. state = state.copyWith(
  171. allAssetOnDatabase: allAssetOnDatabase,
  172. allUniqueAssets: allUniqueAssets,
  173. selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
  174. );
  175. }
  176. // Save to persistent storage
  177. _updatePersistentAlbumsSelection();
  178. }
  179. ///
  180. /// Get all necessary information for calculating the available albums,
  181. /// which albums are selected or excluded
  182. /// and then update the UI according to those information
  183. ///
  184. void getBackupInfo() async {
  185. await getBackupAlbumsInfo();
  186. _updateServerInfo();
  187. _updateBackupAssetCount();
  188. }
  189. ///
  190. /// Save user selection of selected albums and excluded albums to
  191. /// Hive database
  192. ///
  193. void _updatePersistentAlbumsSelection() {
  194. Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
  195. backupAlbumInfoBox.put(
  196. backupInfoKey,
  197. HiveBackupAlbums(
  198. selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
  199. excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
  200. ),
  201. );
  202. }
  203. ///
  204. /// Invoke backup process
  205. ///
  206. void startBackupProcess() async {
  207. _updateServerInfo();
  208. _updateBackupAssetCount();
  209. state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
  210. var authResult = await PhotoManager.requestPermissionExtend();
  211. if (authResult.isAuth) {
  212. await PhotoManager.clearFileCache();
  213. if (state.allUniqueAssets.isEmpty) {
  214. debugPrint("No Asset On Device - Abort Backup Process");
  215. state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
  216. return;
  217. }
  218. Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
  219. // Remove item that has already been backed up
  220. for (var assetId in state.allAssetOnDatabase) {
  221. assetsWillBeBackup.removeWhere((e) => e.id == assetId);
  222. }
  223. if (assetsWillBeBackup.isEmpty) {
  224. state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
  225. }
  226. // Perform Backup
  227. state = state.copyWith(cancelToken: CancelToken());
  228. _backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
  229. } else {
  230. PhotoManager.openSetting();
  231. }
  232. }
  233. void cancelBackup() {
  234. state.cancelToken.cancel('Cancel Backup');
  235. state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
  236. }
  237. void _onAssetUploaded(String deviceAssetId, String deviceId) {
  238. state = state.copyWith(
  239. selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, deviceAssetId},
  240. allAssetOnDatabase: [...state.allAssetOnDatabase, deviceAssetId]);
  241. if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
  242. state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
  243. }
  244. _updateServerInfo();
  245. }
  246. void _onUploadProgress(int sent, int total) {
  247. state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
  248. }
  249. void _updateServerInfo() async {
  250. var serverInfo = await _serverInfoService.getServerInfo();
  251. // Update server info
  252. state = state.copyWith(
  253. serverInfo: ServerInfo(
  254. diskSize: serverInfo.diskSize,
  255. diskUse: serverInfo.diskUse,
  256. diskAvailable: serverInfo.diskAvailable,
  257. diskSizeRaw: serverInfo.diskSizeRaw,
  258. diskUseRaw: serverInfo.diskUseRaw,
  259. diskAvailableRaw: serverInfo.diskAvailableRaw,
  260. diskUsagePercentage: serverInfo.diskUsagePercentage,
  261. ),
  262. );
  263. }
  264. void resumeBackup() {
  265. var authState = ref?.read(authenticationProvider);
  266. // Check if user is login
  267. var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
  268. // User has been logged out return
  269. if (authState != null) {
  270. if (accessKey == null || !authState.isAuthenticated) {
  271. debugPrint("[resumeBackup] not authenticated - abort");
  272. return;
  273. }
  274. // Check if this device is enable backup by the user
  275. if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
  276. // check if backup is alreayd in process - then return
  277. if (state.backupProgress == BackUpProgressEnum.inProgress) {
  278. debugPrint("[resumeBackup] Backup is already in progress - abort");
  279. return;
  280. }
  281. // Run backup
  282. debugPrint("[resumeBackup] Start back up");
  283. startBackupProcess();
  284. }
  285. return;
  286. }
  287. }
  288. }
  289. final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
  290. return BackupNotifier(ref: ref);
  291. });