123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- // @dart=2.9
- import 'dart:async';
- import 'dart:io';
- import 'package:computer/computer.dart';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/widgets.dart';
- import 'package:logging/logging.dart';
- import 'package:photo_manager/photo_manager.dart';
- import 'package:photos/core/configuration.dart';
- import 'package:photos/core/event_bus.dart';
- import 'package:photos/db/device_files_db.dart';
- import 'package:photos/db/file_updation_db.dart';
- import 'package:photos/db/files_db.dart';
- import 'package:photos/events/backup_folders_updated_event.dart';
- import 'package:photos/events/local_photos_updated_event.dart';
- import 'package:photos/events/sync_status_update_event.dart';
- import 'package:photos/extensions/stop_watch.dart';
- import 'package:photos/models/file.dart';
- import 'package:photos/services/app_lifecycle_service.dart';
- import 'package:photos/services/local/local_sync_util.dart';
- import 'package:shared_preferences/shared_preferences.dart';
- import 'package:sqflite/sqflite.dart';
- import 'package:tuple/tuple.dart';
- class LocalSyncService {
- final _logger = Logger("LocalSyncService");
- final _db = FilesDB.instance;
- final Computer _computer = Computer();
- SharedPreferences _prefs;
- Completer<void> _existingSync;
- static const kDbUpdationTimeKey = "db_updation_time";
- static const kHasCompletedFirstImportKey = "has_completed_firstImport";
- static const hasImportedDeviceCollections = "has_imported_device_collections";
- static const kHasGrantedPermissionsKey = "has_granted_permissions";
- static const kPermissionStateKey = "permission_state";
- static const kEditedFileIDsKey = "edited_file_ids";
- static const kDownloadedFileIDsKey = "downloaded_file_ids";
- // Adding `_2` as a suffic to pull files that were earlier ignored due to permission errors
- // See https://github.com/CaiJingLong/flutter_photo_manager/issues/589
- static const kInvalidFileIDsKey = "invalid_file_ids_2";
- LocalSyncService._privateConstructor();
- static final LocalSyncService instance =
- LocalSyncService._privateConstructor();
- Future<void> init(SharedPreferences preferences) async {
- _prefs = preferences;
- if (!AppLifecycleService.instance.isForeground) {
- await PhotoManager.setIgnorePermissionCheck(true);
- }
- await _computer.turnOn(workersCount: 1);
- if (hasGrantedPermissions()) {
- _registerChangeCallback();
- }
- }
- Future<void> sync() async {
- if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
- _logger.info("Skipping local sync since permission has not been granted");
- return;
- }
- if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
- final permissionState = await PhotoManager.requestPermissionExtend();
- if (permissionState != PermissionState.authorized) {
- _logger.severe(
- "sync requested with invalid permission",
- permissionState.toString(),
- );
- return;
- }
- }
- if (_existingSync != null) {
- _logger.warning("Sync already in progress, skipping.");
- return _existingSync.future;
- }
- _existingSync = Completer<void>();
- final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
- _logger.info(
- existingLocalFileIDs.length.toString() + " localIDs were discovered",
- );
- final editedFileIDs = _getEditedFileIDs().toSet();
- final downloadedFileIDs = _getDownloadedFileIDs().toSet();
- final syncStartTime = DateTime.now().microsecondsSinceEpoch;
- final lastDBUpdationTime = _prefs.getInt(kDbUpdationTimeKey) ?? 0;
- final startTime = DateTime.now().microsecondsSinceEpoch;
- if (lastDBUpdationTime != 0) {
- await _loadAndStorePhotos(
- lastDBUpdationTime,
- syncStartTime,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- } else {
- // Load from 0 - 01.01.2010
- Bus.instance.fire(SyncStatusUpdate(SyncStatus.startedFirstGalleryImport));
- var startTime = 0;
- var toYear = 2010;
- var toTime = DateTime(toYear).microsecondsSinceEpoch;
- while (toTime < syncStartTime) {
- await _loadAndStorePhotos(
- startTime,
- toTime,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- startTime = toTime;
- toYear++;
- toTime = DateTime(toYear).microsecondsSinceEpoch;
- }
- await _loadAndStorePhotos(
- startTime,
- syncStartTime,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- }
- if (!_prefs.containsKey(kHasCompletedFirstImportKey) ||
- !_prefs.getBool(kHasCompletedFirstImportKey)) {
- await _prefs.setBool(kHasCompletedFirstImportKey, true);
- // mark device collection has imported on first import
- await _refreshDeviceFolderCountAndCover(isFirstSync: true);
- await _prefs.setBool(hasImportedDeviceCollections, true);
- _logger.fine("first gallery import finished");
- Bus.instance
- .fire(SyncStatusUpdate(SyncStatus.completedFirstGalleryImport));
- }
- final endTime = DateTime.now().microsecondsSinceEpoch;
- final duration = Duration(microseconds: endTime - startTime);
- _logger.info("Load took " + duration.inMilliseconds.toString() + "ms");
- _existingSync.complete();
- _existingSync = null;
- }
- Future<bool> _refreshDeviceFolderCountAndCover({
- bool isFirstSync = false,
- }) async {
- final List<Tuple2<AssetPathEntity, String>> result =
- await getDeviceFolderWithCountAndCoverID();
- final bool hasUpdated = await _db.updateDeviceCoverWithCount(
- result,
- shouldBackup: Configuration.instance.hasSelectedAllFoldersForBackup(),
- );
- // do not fire UI update event during first sync. Otherwise the next screen
- // to shop the backup folder is skipped
- if (hasUpdated && !isFirstSync) {
- Bus.instance.fire(BackupFoldersUpdatedEvent());
- }
- // migrate the backed up folder settings after first import is done remove
- // after 6 months?
- if (!_prefs.containsKey(hasImportedDeviceCollections) &&
- _prefs.containsKey(kHasCompletedFirstImportKey)) {
- await _migrateOldSettings(result);
- }
- return hasUpdated;
- }
- Future<void> _migrateOldSettings(
- List<Tuple2<AssetPathEntity, String>> result,
- ) async {
- final pathsToBackUp = Configuration.instance.getPathsToBackUp();
- final entriesToBackUp = Map.fromEntries(
- result
- .where((element) => pathsToBackUp.contains(element.item1.name))
- .map((e) => MapEntry(e.item1.id, true)),
- );
- if (entriesToBackUp.isNotEmpty) {
- await _db.updateDevicePathSyncStatus(entriesToBackUp);
- Bus.instance.fire(BackupFoldersUpdatedEvent());
- }
- await Configuration.instance
- .setHasSelectedAnyBackupFolder(pathsToBackUp.isNotEmpty);
- await _prefs.setBool(hasImportedDeviceCollections, true);
- }
- bool isDeviceFileMigrationDone() {
- return _prefs.containsKey(hasImportedDeviceCollections);
- }
- Future<bool> syncAll() async {
- final stopwatch = EnteWatch("localSyncAll")..start();
- final localAssets = await getAllLocalAssets();
- _logger.info(
- "Loading allLocalAssets ${localAssets.length} took ${stopwatch.elapsedMilliseconds}ms ",
- );
- await _refreshDeviceFolderCountAndCover();
- _logger.info(
- "refreshDeviceFolderCountAndCover + allLocalAssets took ${stopwatch.elapsedMilliseconds}ms ",
- );
- final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
- final Map<String, Set<String>> pathToLocalIDs =
- await _db.getDevicePathIDToLocalIDMap();
- final invalidIDs = _getInvalidFileIDs().toSet();
- final localDiffResult = await getDiffWithLocal(
- localAssets,
- existingLocalFileIDs,
- pathToLocalIDs,
- invalidIDs,
- _computer,
- );
- bool hasAnyMappingChanged = false;
- if (localDiffResult.newPathToLocalIDs?.isNotEmpty ?? false) {
- await _db.insertPathIDToLocalIDMapping(localDiffResult.newPathToLocalIDs);
- hasAnyMappingChanged = true;
- }
- if (localDiffResult.deletePathToLocalIDs?.isNotEmpty ?? false) {
- await _db
- .deletePathIDToLocalIDMapping(localDiffResult.deletePathToLocalIDs);
- hasAnyMappingChanged = true;
- }
- final bool hasUnsyncedFiles =
- localDiffResult.uniqueLocalFiles?.isNotEmpty ?? false;
- if (hasUnsyncedFiles) {
- await _db.insertMultiple(
- localDiffResult.uniqueLocalFiles,
- conflictAlgorithm: ConflictAlgorithm.ignore,
- );
- _logger.info(
- "Inserted ${localDiffResult.uniqueLocalFiles.length} "
- "un-synced files",
- );
- }
- debugPrint(
- "syncAll: mappingChange : $hasAnyMappingChanged, "
- "unSyncedFiles: $hasUnsyncedFiles",
- );
- if (hasAnyMappingChanged || hasUnsyncedFiles) {
- Bus.instance.fire(
- LocalPhotosUpdatedEvent(
- localDiffResult.uniqueLocalFiles,
- source: "syncAllChange",
- ),
- );
- }
- _logger.info("syncAll took ${stopwatch.elapsed.inMilliseconds}ms ");
- return hasUnsyncedFiles;
- }
- Future<void> trackEditedFile(File file) async {
- final editedIDs = _getEditedFileIDs();
- editedIDs.add(file.localID);
- await _prefs.setStringList(kEditedFileIDsKey, editedIDs);
- }
- List<String> _getEditedFileIDs() {
- if (_prefs.containsKey(kEditedFileIDsKey)) {
- return _prefs.getStringList(kEditedFileIDsKey);
- } else {
- final List<String> editedIDs = [];
- return editedIDs;
- }
- }
- List<String> _getDownloadedFileIDs() {
- if (_prefs.containsKey(kDownloadedFileIDsKey)) {
- return _prefs.getStringList(kDownloadedFileIDsKey);
- } else {
- return <String>[];
- }
- }
- Future<void> trackInvalidFile(File file) async {
- final invalidIDs = _getInvalidFileIDs();
- invalidIDs.add(file.localID);
- await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs);
- }
- List<String> _getInvalidFileIDs() {
- if (_prefs.containsKey(kInvalidFileIDsKey)) {
- return _prefs.getStringList(kInvalidFileIDsKey);
- } else {
- return <String>[];
- }
- }
- bool hasGrantedPermissions() {
- return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
- }
- bool hasGrantedLimitedPermissions() {
- return _prefs.getString(kPermissionStateKey) ==
- PermissionState.limited.toString();
- }
- Future<void> onPermissionGranted(PermissionState state) async {
- await _prefs.setBool(kHasGrantedPermissionsKey, true);
- await _prefs.setString(kPermissionStateKey, state.toString());
- if (state == PermissionState.limited) {
- // when limited permission is granted, by default mark all folders for
- // backup
- await Configuration.instance.setSelectAllFoldersForBackup(true);
- }
- _registerChangeCallback();
- }
- bool hasCompletedFirstImport() {
- return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
- }
- // Warning: resetLocalSync should only be used for testing imported related
- // changes
- Future<void> resetLocalSync() async {
- assert(kDebugMode, "only available in debug mode");
- await FilesDB.instance.deleteDB();
- for (var element in [
- kHasCompletedFirstImportKey,
- hasImportedDeviceCollections,
- kDbUpdationTimeKey,
- kDownloadedFileIDsKey,
- kEditedFileIDsKey,
- "has_synced_edit_time",
- "has_selected_all_folders_for_backup",
- ]) {
- await _prefs.remove(element);
- }
- }
- Future<void> _loadAndStorePhotos(
- int fromTime,
- int toTime,
- Set<String> existingLocalFileIDs,
- Set<String> editedFileIDs,
- Set<String> downloadedFileIDs,
- ) async {
- final Tuple2<List<LocalPathAsset>, List<File>> result =
- await getLocalPathAssetsAndFiles(fromTime, toTime, _computer);
- await FilesDB.instance.insertLocalAssets(
- result.item1,
- shouldAutoBackup: Configuration.instance.hasSelectedAllFoldersForBackup(),
- );
- final List<File> files = result.item2;
- _logger.info(
- "Loaded ${files.length} photos from " +
- DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() +
- " to " +
- DateTime.fromMicrosecondsSinceEpoch(toTime).toString(),
- );
- if (files.isNotEmpty) {
- await _trackUpdatedFiles(
- files,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- final List<File> allFiles = [];
- allFiles.addAll(files);
- files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
- await _db.insertMultiple(
- files,
- conflictAlgorithm: ConflictAlgorithm.ignore,
- );
- _logger.info("Inserted " + files.length.toString() + " files.");
- Bus.instance.fire(
- LocalPhotosUpdatedEvent(allFiles, source: "loadedPhoto"),
- );
- }
- await _prefs.setInt(kDbUpdationTimeKey, toTime);
- }
- Future<void> _trackUpdatedFiles(
- List<File> files,
- Set<String> existingLocalFileIDs,
- Set<String> editedFileIDs,
- Set<String> downloadedFileIDs,
- ) async {
- final updatedFiles = files
- .where((file) => existingLocalFileIDs.contains(file.localID))
- .toList();
- updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID));
- updatedFiles
- .removeWhere((file) => downloadedFileIDs.contains(file.localID));
- if (updatedFiles.isNotEmpty) {
- _logger.info(
- updatedFiles.length.toString() + " local files were updated.",
- );
- final List<String> updatedLocalIDs = [];
- for (final file in updatedFiles) {
- updatedLocalIDs.add(file.localID);
- }
- await FileUpdationDB.instance.insertMultiple(
- updatedLocalIDs,
- FileUpdationDB.modificationTimeUpdated,
- );
- }
- }
- void _registerChangeCallback() {
- // In case of iOS limit permission, this call back is fired immediately
- // after file selection dialog is dismissed.
- PhotoManager.addChangeCallback((value) async {
- _logger.info("Something changed on disk");
- if (_existingSync != null) {
- await _existingSync.future;
- }
- if (hasGrantedLimitedPermissions()) {
- syncAll();
- } else {
- sync().then((value) => _refreshDeviceFolderCountAndCover());
- }
- });
- PhotoManager.startChangeNotify();
- }
- }
|