local_sync_service.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:computer/computer.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:photo_manager/photo_manager.dart';
  6. import 'package:photos/core/configuration.dart';
  7. import 'package:photos/core/event_bus.dart';
  8. import 'package:photos/db/files_db.dart';
  9. import 'package:photos/events/local_photos_updated_event.dart';
  10. import 'package:photos/events/sync_status_update_event.dart';
  11. import 'package:photos/models/file.dart';
  12. import 'package:photos/services/app_lifecycle_service.dart';
  13. import 'package:photos/utils/file_sync_util.dart';
  14. import 'package:shared_preferences/shared_preferences.dart';
  15. class LocalSyncService {
  16. final _logger = Logger("LocalSyncService");
  17. final _db = FilesDB.instance;
  18. final Computer _computer = Computer();
  19. SharedPreferences _prefs;
  20. Completer<void> _existingSync;
  21. static const kDbUpdationTimeKey = "db_updation_time";
  22. static const kHasCompletedFirstImportKey = "has_completed_firstImport";
  23. static const kHasGrantedPermissionsKey = "has_granted_permissions";
  24. static const kPermissionStateKey = "permission_state";
  25. static const kEditedFileIDsKey = "edited_file_ids";
  26. static const kDownloadedFileIDsKey = "downloaded_file_ids";
  27. // Adding `_2` as a suffic to pull files that were earlier ignored due to permission errors
  28. // See https://github.com/CaiJingLong/flutter_photo_manager/issues/589
  29. static const kInvalidFileIDsKey = "invalid_file_ids_2";
  30. LocalSyncService._privateConstructor();
  31. static final LocalSyncService instance =
  32. LocalSyncService._privateConstructor();
  33. Future<void> init() async {
  34. _prefs = await SharedPreferences.getInstance();
  35. if (!AppLifecycleService.instance.isForeground) {
  36. await PhotoManager.setIgnorePermissionCheck(true);
  37. }
  38. await _computer.turnOn(workersCount: 1);
  39. if (hasGrantedPermissions()) {
  40. _registerChangeCallback();
  41. }
  42. }
  43. Future<void> sync() async {
  44. if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
  45. _logger.info("Skipping local sync since permission has not been granted");
  46. return;
  47. }
  48. if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
  49. final permissionState = await PhotoManager.requestPermissionExtend();
  50. if (permissionState != PermissionState.authorized) {
  51. _logger.severe(
  52. "sync requested with invalid permission",
  53. permissionState.toString(),
  54. );
  55. return;
  56. }
  57. }
  58. if (_existingSync != null) {
  59. _logger.warning("Sync already in progress, skipping.");
  60. return _existingSync.future;
  61. }
  62. _existingSync = Completer<void>();
  63. final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
  64. _logger.info(
  65. existingLocalFileIDs.length.toString() + " localIDs were discovered",
  66. );
  67. final editedFileIDs = getEditedFileIDs().toSet();
  68. final downloadedFileIDs = getDownloadedFileIDs().toSet();
  69. final syncStartTime = DateTime.now().microsecondsSinceEpoch;
  70. final lastDBUpdationTime = _prefs.getInt(kDbUpdationTimeKey) ?? 0;
  71. final startTime = DateTime.now().microsecondsSinceEpoch;
  72. if (lastDBUpdationTime != 0) {
  73. await _loadAndStorePhotos(
  74. lastDBUpdationTime,
  75. syncStartTime,
  76. existingLocalFileIDs,
  77. editedFileIDs,
  78. downloadedFileIDs,
  79. );
  80. } else {
  81. // Load from 0 - 01.01.2010
  82. Bus.instance.fire(SyncStatusUpdate(SyncStatus.startedFirstGalleryImport));
  83. var startTime = 0;
  84. var toYear = 2010;
  85. var toTime = DateTime(toYear).microsecondsSinceEpoch;
  86. while (toTime < syncStartTime) {
  87. await _loadAndStorePhotos(
  88. startTime,
  89. toTime,
  90. existingLocalFileIDs,
  91. editedFileIDs,
  92. downloadedFileIDs,
  93. );
  94. startTime = toTime;
  95. toYear++;
  96. toTime = DateTime(toYear).microsecondsSinceEpoch;
  97. }
  98. await _loadAndStorePhotos(
  99. startTime,
  100. syncStartTime,
  101. existingLocalFileIDs,
  102. editedFileIDs,
  103. downloadedFileIDs,
  104. );
  105. }
  106. if (!_prefs.containsKey(kHasCompletedFirstImportKey) ||
  107. !_prefs.getBool(kHasCompletedFirstImportKey)) {
  108. await _prefs.setBool(kHasCompletedFirstImportKey, true);
  109. _logger.fine("first gallery import finished");
  110. Bus.instance
  111. .fire(SyncStatusUpdate(SyncStatus.completedFirstGalleryImport));
  112. }
  113. final endTime = DateTime.now().microsecondsSinceEpoch;
  114. final duration = Duration(microseconds: endTime - startTime);
  115. _logger.info("Load took " + duration.inMilliseconds.toString() + "ms");
  116. _existingSync.complete();
  117. _existingSync = null;
  118. }
  119. Future<bool> syncAll() async {
  120. final sTime = DateTime.now().microsecondsSinceEpoch;
  121. final localAssets = await getAllLocalAssets();
  122. final eTime = DateTime.now().microsecondsSinceEpoch;
  123. final d = Duration(microseconds: eTime - sTime);
  124. _logger.info(
  125. "Loading from the beginning returned " +
  126. localAssets.length.toString() +
  127. " assets and took " +
  128. d.inMilliseconds.toString() +
  129. "ms",
  130. );
  131. final existingIDs = await _db.getExistingLocalFileIDs();
  132. final invalidIDs = getInvalidFileIDs().toSet();
  133. final unsyncedFiles =
  134. await getUnsyncedFiles(localAssets, existingIDs, invalidIDs, _computer);
  135. if (unsyncedFiles.isNotEmpty) {
  136. await _db.insertMultiple(unsyncedFiles);
  137. _logger.info(
  138. "Inserted " + unsyncedFiles.length.toString() + " unsynced files.",
  139. );
  140. _updatePathsToBackup(unsyncedFiles);
  141. Bus.instance.fire(LocalPhotosUpdatedEvent(unsyncedFiles));
  142. return true;
  143. }
  144. return false;
  145. }
  146. Future<void> trackEditedFile(File file) async {
  147. final editedIDs = getEditedFileIDs();
  148. editedIDs.add(file.localID);
  149. await _prefs.setStringList(kEditedFileIDsKey, editedIDs);
  150. }
  151. List<String> getEditedFileIDs() {
  152. if (_prefs.containsKey(kEditedFileIDsKey)) {
  153. return _prefs.getStringList(kEditedFileIDsKey);
  154. } else {
  155. final List<String> editedIDs = [];
  156. return editedIDs;
  157. }
  158. }
  159. Future<void> trackDownloadedFile(String localID) async {
  160. final downloadedIDs = getDownloadedFileIDs();
  161. downloadedIDs.add(localID);
  162. await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs);
  163. }
  164. List<String> getDownloadedFileIDs() {
  165. if (_prefs.containsKey(kDownloadedFileIDsKey)) {
  166. return _prefs.getStringList(kDownloadedFileIDsKey);
  167. } else {
  168. final List<String> downloadedIDs = [];
  169. return downloadedIDs;
  170. }
  171. }
  172. Future<void> trackInvalidFile(File file) async {
  173. final invalidIDs = getInvalidFileIDs();
  174. invalidIDs.add(file.localID);
  175. await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs);
  176. }
  177. List<String> getInvalidFileIDs() {
  178. if (_prefs.containsKey(kInvalidFileIDsKey)) {
  179. return _prefs.getStringList(kInvalidFileIDsKey);
  180. } else {
  181. final List<String> invalidIDs = [];
  182. return invalidIDs;
  183. }
  184. }
  185. bool hasGrantedPermissions() {
  186. return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
  187. }
  188. bool hasGrantedLimitedPermissions() {
  189. return _prefs.getString(kPermissionStateKey) ==
  190. PermissionState.limited.toString();
  191. }
  192. Future<void> onPermissionGranted(PermissionState state) async {
  193. await _prefs.setBool(kHasGrantedPermissionsKey, true);
  194. await _prefs.setString(kPermissionStateKey, state.toString());
  195. _registerChangeCallback();
  196. }
  197. bool hasCompletedFirstImport() {
  198. return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
  199. }
  200. Future<void> _loadAndStorePhotos(
  201. int fromTime,
  202. int toTime,
  203. Set<String> existingLocalFileIDs,
  204. Set<String> editedFileIDs,
  205. Set<String> downloadedFileIDs,
  206. ) async {
  207. _logger.info(
  208. "Loading photos from " +
  209. DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() +
  210. " to " +
  211. DateTime.fromMicrosecondsSinceEpoch(toTime).toString(),
  212. );
  213. final files = await getDeviceFiles(fromTime, toTime, _computer);
  214. if (files.isNotEmpty) {
  215. _logger.info("Fetched " + files.length.toString() + " files.");
  216. final updatedFiles = files
  217. .where((file) => existingLocalFileIDs.contains(file.localID))
  218. .toList();
  219. updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID));
  220. updatedFiles
  221. .removeWhere((file) => downloadedFileIDs.contains(file.localID));
  222. if (updatedFiles.isNotEmpty) {
  223. _logger.info(
  224. updatedFiles.length.toString() + " local files were updated.",
  225. );
  226. }
  227. for (final file in updatedFiles) {
  228. await captureUpdateLogs(file);
  229. await _db.updateUploadedFile(
  230. file.localID,
  231. file.title,
  232. file.location,
  233. file.creationTime,
  234. file.modificationTime,
  235. null,
  236. );
  237. }
  238. final List<File> allFiles = [];
  239. allFiles.addAll(files);
  240. files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
  241. await _db.insertMultiple(files);
  242. _logger.info("Inserted " + files.length.toString() + " files.");
  243. _updatePathsToBackup(files);
  244. Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles));
  245. }
  246. await _prefs.setInt(kDbUpdationTimeKey, toTime);
  247. }
  248. // _captureUpdateLogs is a helper method to log details
  249. // about the file which is being marked for re-upload
  250. Future<void> captureUpdateLogs(File file) async {
  251. _logger.info(
  252. 're-upload locally updated file ${file.toString()}',
  253. );
  254. try {
  255. if (Platform.isIOS) {
  256. final assetEntity = await AssetEntity.fromId(file.localID);
  257. if (assetEntity != null) {
  258. final isLocallyAvailable =
  259. await assetEntity.isLocallyAvailable(isOrigin: true);
  260. _logger.info(
  261. 're-upload asset ${file.toString()} with localAvailableFlag '
  262. '$isLocallyAvailable and fav ${assetEntity.isFavorite}',
  263. );
  264. } else {
  265. _logger
  266. .info('re-upload failed to fetch assetInfo ${file.toString()}');
  267. }
  268. }
  269. } catch (ignore) {
  270. //ignore
  271. }
  272. }
  273. void _updatePathsToBackup(List<File> files) {
  274. if (Configuration.instance.hasSelectedAllFoldersForBackup()) {
  275. final pathsToBackup = Configuration.instance.getPathsToBackUp();
  276. final newFilePaths = files.map((file) => file.deviceFolder).toList();
  277. pathsToBackup.addAll(newFilePaths);
  278. Configuration.instance.setPathsToBackUp(pathsToBackup);
  279. }
  280. }
  281. void _registerChangeCallback() {
  282. // In case of iOS limit permission, this call back is fired immediately
  283. // after file selection dialog is dismissed.
  284. PhotoManager.addChangeCallback((value) async {
  285. _logger.info("Something changed on disk");
  286. if (_existingSync != null) {
  287. await _existingSync.future;
  288. }
  289. if (hasGrantedLimitedPermissions()) {
  290. syncAll();
  291. } else {
  292. sync();
  293. }
  294. });
  295. PhotoManager.startChangeNotify();
  296. }
  297. }