ente/lib/services/local_sync_service.dart

359 lines
12 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:io';
import 'package:computer/computer.dart';
import 'package:flutter/foundation.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';
2022-07-24 17:22:12 +00:00
import 'package:photos/db/device_files_db.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';
2022-07-20 09:49:10 +00:00
import 'package:photos/services/local/local_sync_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
2022-08-24 10:32:41 +00:00
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;
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",
);
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);
await refreshDeviceFolderCountAndCover();
_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
}
Future<bool> refreshDeviceFolderCountAndCover() async {
List<Tuple2<AssetPathEntity, String>> result =
await getDeviceFolderWithCountAndCoverID();
return await _db.updateDeviceCoverWithCount(
result,
autoSync: Configuration.instance.hasSelectedAllFoldersForBackup(),
);
}
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",
);
await refreshDeviceFolderCountAndCover();
final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
final Map<String, Set<String>> pathToLocalIDs =
await _db.getDevicePathIDToLocalIDMap();
final invalidIDs = _getInvalidFileIDs().toSet();
final localUnSyncResult = await getLocalUnSyncedFiles(
localAssets,
existingLocalFileIDs,
pathToLocalIDs,
invalidIDs,
_computer,
);
2022-08-24 10:32:41 +00:00
if (localUnSyncResult.newPathToLocalIDs.isNotEmpty) {
await _db
.insertPathIDToLocalIDMapping(localUnSyncResult.newPathToLocalIDs);
}
2022-08-24 10:32:41 +00:00
if (localUnSyncResult.deletePathToLocalIDs.isNotEmpty) {
await _db
2022-08-24 10:32:41 +00:00
.deletePathIDToLocalIDMapping(localUnSyncResult.deletePathToLocalIDs);
}
2022-08-24 10:32:41 +00:00
if (localUnSyncResult.uniqueLocalFiles.isNotEmpty) {
await _db.insertMultiple(
localUnSyncResult.uniqueLocalFiles,
conflictAlgorithm: ConflictAlgorithm.ignore,
);
2021-06-18 06:46:56 +00:00
_logger.info(
2022-08-24 10:32:41 +00:00
"Inserted ${localUnSyncResult.uniqueLocalFiles.length} "
"un-synced files",
2022-06-11 08:23:52 +00:00
);
2022-08-24 10:32:41 +00:00
Bus.instance.fire(LocalPhotosUpdatedEvent(localUnSyncResult));
2021-06-18 06:46:56 +00:00
return true;
}
return false;
}
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 {
List<String> editedIDs = [];
return editedIDs;
}
}
Future<void> trackDownloadedFile(String localID) async {
final downloadedIDs = _getDownloadedFileIDs();
downloadedIDs.add(localID);
await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs);
}
List<String> _getDownloadedFileIDs() {
if (_prefs.containsKey(kDownloadedFileIDsKey)) {
return _prefs.getStringList(kDownloadedFileIDsKey);
} else {
return <String>[];
}
}
2021-06-18 06:46:56 +00:00
Future<void> trackInvalidFile(File file) async {
final invalidIDs = _getInvalidFileIDs();
2021-06-18 06:46:56 +00:00
invalidIDs.add(file.localID);
await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs);
}
List<String> _getInvalidFileIDs() {
2021-06-18 06:46:56 +00:00
if (_prefs.containsKey(kInvalidFileIDsKey)) {
return _prefs.getStringList(kInvalidFileIDsKey);
} else {
return <String>[];
2021-06-18 06:46:56 +00:00
}
}
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;
}
// 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,
kDbUpdationTimeKey,
kDownloadedFileIDsKey,
kEditedFileIDsKey
]) {
await _prefs.remove(element);
}
}
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(),
);
2022-07-24 17:22:12 +00:00
final deviceFiles = await getDeviceFiles(fromTime, toTime, _computer);
final List<File> files = deviceFiles.item2;
unawaited(FilesDB.instance.insertDeviceFiles(files));
unawaited(
FilesDB.instance.insertOrUpdatePathName(
deviceFiles.item1,
autoSync: Configuration.instance.hasSelectedAllFoldersForBackup(),
),
);
if (files.isNotEmpty) {
_logger.info("Fetched " + files.length.toString() + " files.");
await _trackUpdatedFiles(
2022-08-24 10:32:41 +00:00
files,
existingLocalFileIDs,
editedFileIDs,
downloadedFileIDs,
);
final List<File> allFiles = [];
allFiles.addAll(files);
files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
2022-08-24 10:32:41 +00:00
await _db.insertMultiple(
files,
conflictAlgorithm: ConflictAlgorithm.ignore,
);
_logger.info("Inserted " + files.length.toString() + " files.");
// _updatePathsToBackup(files);
Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles));
}
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.",
);
List<String> updatedLocalIDs = [];
for (final file in updatedFiles) {
if (file.localID != null) {
updatedLocalIDs.add(file.localID);
}
}
await FilesMigrationDB.instance.insertMultiple(
updatedLocalIDs,
FilesMigrationDB.modificationTimeUpdated,
);
}
}
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();
}
}