ente/lib/services/local_sync_service.dart

296 lines
10 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:io';
import 'package:computer/computer.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/file_migration_db.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/models/file.dart';
2021-11-15 15:35:07 +00:00
import 'package:photos/services/app_lifecycle_service.dart';
import 'package:photos/utils/file_sync_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocalSyncService {
final _logger = Logger("LocalSyncService");
final _db = FilesDB.instance;
final Computer _computer = Computer();
SharedPreferences _prefs;
2022-01-03 18:24:43 +00:00
Completer<void> _existingSync;
static const kDbUpdationTimeKey = "db_updation_time";
static const kHasCompletedFirstImportKey = "has_completed_firstImport";
static const kHasGrantedPermissionsKey = "has_granted_permissions";
static const kPermissionStateKey = "permission_state";
static const kEditedFileIDsKey = "edited_file_ids";
static const kDownloadedFileIDsKey = "downloaded_file_ids";
2022-07-03 09:49:33 +00:00
// 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";
2022-07-29 06:06:03 +00:00
2021-07-22 18:41:58 +00:00
LocalSyncService._privateConstructor();
2022-07-03 09:49:33 +00:00
static final LocalSyncService instance =
LocalSyncService._privateConstructor();
2021-11-15 15:35:07 +00:00
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
2021-11-15 15:35:07 +00:00
if (!AppLifecycleService.instance.isForeground) {
await PhotoManager.setIgnorePermissionCheck(true);
}
await _computer.turnOn(workersCount: 1);
2022-01-03 18:24:43 +00:00
if (hasGrantedPermissions()) {
_registerChangeCallback();
}
}
2021-09-05 10:18:14 +00:00
Future<void> sync() async {
if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
_logger.info("Skipping local sync since permission has not been granted");
return;
}
2021-11-15 15:35:07 +00:00
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
2021-09-05 10:10:43 +00:00
final permissionState = await PhotoManager.requestPermissionExtend();
if (permissionState != PermissionState.authorized) {
2022-06-11 08:23:52 +00:00
_logger.severe(
"sync requested with invalid permission",
permissionState.toString(),
);
return;
}
}
2022-01-03 18:24:43 +00:00
if (_existingSync != null) {
_logger.warning("Sync already in progress, skipping.");
return _existingSync.future;
}
_existingSync = Completer<void>();
final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
2021-08-04 20:46:58 +00:00
_logger.info(
2022-06-11 08:23:52 +00:00
existingLocalFileIDs.length.toString() + " localIDs were discovered",
);
2021-06-18 06:46:56 +00:00
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
2022-07-03 07:47:15 +00:00
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);
_logger.fine("first gallery import finished");
2022-07-03 09:49:33 +00:00
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");
2022-01-03 18:24:43 +00:00
_existingSync.complete();
_existingSync = null;
2021-06-15 11:57:33 +00:00
}
2021-06-15 11:57:33 +00:00
Future<bool> syncAll() async {
final sTime = DateTime.now().microsecondsSinceEpoch;
final localAssets = await getAllLocalAssets();
final eTime = DateTime.now().microsecondsSinceEpoch;
final d = Duration(microseconds: eTime - sTime);
2022-06-11 08:23:52 +00:00
_logger.info(
"Loading from the beginning returned " +
localAssets.length.toString() +
" assets and took " +
d.inMilliseconds.toString() +
"ms",
);
2021-06-15 11:57:33 +00:00
final existingIDs = await _db.getExistingLocalFileIDs();
2021-06-18 06:46:56 +00:00
final invalidIDs = getInvalidFileIDs().toSet();
2022-07-03 09:49:33 +00:00
final unsyncedFiles =
await getUnsyncedFiles(localAssets, existingIDs, invalidIDs, _computer);
2021-06-18 06:46:56 +00:00
if (unsyncedFiles.isNotEmpty) {
await _db.insertMultiple(unsyncedFiles);
_logger.info(
2022-06-11 08:23:52 +00:00
"Inserted " + unsyncedFiles.length.toString() + " unsynced files.",
);
_updatePathsToBackup(unsyncedFiles);
2021-06-18 06:46:56 +00:00
Bus.instance.fire(LocalPhotosUpdatedEvent(unsyncedFiles));
return true;
}
return false;
}
Future<void> trackEditedFile(File file) async {
2021-06-18 06:46:56 +00:00
final editedIDs = getEditedFileIDs();
editedIDs.add(file.localID);
await _prefs.setStringList(kEditedFileIDsKey, editedIDs);
}
2021-06-18 06:46:56 +00:00
List<String> getEditedFileIDs() {
if (_prefs.containsKey(kEditedFileIDsKey)) {
return _prefs.getStringList(kEditedFileIDsKey);
} else {
List<String> editedIDs = [];
return editedIDs;
}
}
Future<void> trackDownloadedFile(String localID) async {
2021-06-18 06:46:56 +00:00
final downloadedIDs = getDownloadedFileIDs();
downloadedIDs.add(localID);
await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs);
}
2021-06-18 06:46:56 +00:00
List<String> getDownloadedFileIDs() {
if (_prefs.containsKey(kDownloadedFileIDsKey)) {
return _prefs.getStringList(kDownloadedFileIDsKey);
} else {
List<String> downloadedIDs = [];
return downloadedIDs;
}
}
2021-06-18 06:46:56 +00:00
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 {
List<String> invalidIDs = [];
return invalidIDs;
}
}
bool hasGrantedPermissions() {
return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
}
bool hasGrantedLimitedPermissions() {
2022-07-03 09:49:33 +00:00
return _prefs.getString(kPermissionStateKey) ==
PermissionState.limited.toString();
}
Future<void> onPermissionGranted(PermissionState state) async {
await _prefs.setBool(kHasGrantedPermissionsKey, true);
await _prefs.setString(kPermissionStateKey, state.toString());
2022-01-03 18:24:43 +00:00
_registerChangeCallback();
}
bool hasCompletedFirstImport() {
return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
}
Future<void> _loadAndStorePhotos(
int fromTime,
int toTime,
Set<String> existingLocalFileIDs,
Set<String> editedFileIDs,
Set<String> downloadedFileIDs,
) async {
2022-06-11 08:23:52 +00:00
_logger.info(
"Loading photos from " +
DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() +
" to " +
DateTime.fromMicrosecondsSinceEpoch(toTime).toString(),
);
final files = await getDeviceFiles(fromTime, toTime, _computer);
if (files.isNotEmpty) {
_logger.info("Fetched " + files.length.toString() + " files.");
2022-07-03 09:49:33 +00:00
final updatedFiles = files
.where((file) => existingLocalFileIDs.contains(file.localID))
.toList();
updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID));
2022-07-03 09:49:33 +00:00
updatedFiles
.removeWhere((file) => downloadedFileIDs.contains(file.localID));
2022-05-10 07:42:41 +00:00
if (updatedFiles.isNotEmpty) {
2022-05-12 03:27:32 +00:00
_logger.info(
2022-06-11 08:23:52 +00:00
updatedFiles.length.toString() + " local files were updated.",
);
2022-05-10 07:42:41 +00:00
}
List<String> updatedLocalIDs = [];
for (final file in updatedFiles) {
if (file.localID != null) {
updatedLocalIDs.add(file.localID);
}
}
await FilesMigrationDB.instance.insertMultiple(
updatedLocalIDs,
FilesMigrationDB.modificationTimeUpdated,
);
final List<File> allFiles = [];
allFiles.addAll(files);
files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
await _db.insertMultiple(files);
_logger.info("Inserted " + files.length.toString() + " files.");
_updatePathsToBackup(files);
Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles));
}
await _prefs.setInt(kDbUpdationTimeKey, toTime);
}
void _updatePathsToBackup(List<File> files) {
if (Configuration.instance.hasSelectedAllFoldersForBackup()) {
final pathsToBackup = Configuration.instance.getPathsToBackUp();
final newFilePaths = files.map((file) => file.deviceFolder).toList();
pathsToBackup.addAll(newFilePaths);
Configuration.instance.setPathsToBackUp(pathsToBackup);
}
}
2022-01-03 18:24:43 +00:00
void _registerChangeCallback() {
// In case of iOS limit permission, this call back is fired immediately
// after file selection dialog is dismissed.
2022-01-03 18:24:43 +00:00
PhotoManager.addChangeCallback((value) async {
_logger.info("Something changed on disk");
if (_existingSync != null) {
await _existingSync.future;
}
if (hasGrantedLimitedPermissions()) {
syncAll();
} else {
sync();
}
2022-01-03 18:24:43 +00:00
});
PhotoManager.startChangeNotify();
}
}