local_sync_service.dart 10.0 KB

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