diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee05054a6..c67c31539 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- untracked binned items recovery
+- untracked vault items recovery
## [v1.10.4] - 2024-02-07
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
index 43a5d46c8..62754b9be 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
@@ -30,6 +30,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
"getUntrackedTrashPaths" -> ioScope.launch { safe(call, result, ::getUntrackedTrashPaths) }
+ "getUntrackedVaultPaths" -> ioScope.launch { safe(call, result, ::getUntrackedVaultPaths) }
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
@@ -129,13 +130,28 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun getUntrackedTrashPaths(call: MethodCall, result: MethodChannel.Result) {
val knownPaths = call.argument>("knownPaths")
if (knownPaths == null) {
- result.error("getUntrackedBinPaths-args", "missing arguments", null)
+ result.error("getUntrackedTrashPaths-args", "missing arguments", null)
return
}
val trashDirs = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }
- val trashPaths = trashDirs.flatMap { dir -> dir.listFiles()?.map { file -> file.path } ?: listOf() }
- val untrackedPaths = trashPaths.filter { !knownPaths.contains(it) }.toList()
+ val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.map { file -> file.path } ?: listOf() }
+ val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
+
+ result.success(untrackedPaths)
+ }
+
+ private fun getUntrackedVaultPaths(call: MethodCall, result: MethodChannel.Result) {
+ val vault = call.argument("vault")
+ val knownPaths = call.argument>("knownPaths")
+ if (vault == null || knownPaths == null) {
+ result.error("getUntrackedVaultPaths-args", "missing arguments", null)
+ return
+ }
+
+ val vaultDir = File(StorageUtils.getVaultRoot(context), vault)
+ val vaultItemPaths = vaultDir.listFiles()?.map { file -> file.path } ?: listOf()
+ val untrackedPaths = vaultItemPaths.filterNot(knownPaths::contains).toList()
result.success(untrackedPaths)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt
index 05822f03a..97498c0e4 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt
@@ -159,7 +159,7 @@ object SafePngMetadataReader {
// Only compression method allowed by the spec is zero: deflate
if (compressionMethod.toInt() == 0) {
// bytes left for compressed text is:
- // total bytes length - (profilenamebytes length + null byte + compression method byte)
+ // total bytes length - (profileNameBytes length + null byte + compression method byte)
val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1)
val compressedProfile = reader.getBytes(bytesLeft)
try {
diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart
index 74d854a43..d118dd101 100644
--- a/lib/model/source/media_store_source.dart
+++ b/lib/model/source/media_store_source.dart
@@ -154,7 +154,7 @@ class MediaStoreSource extends CollectionSource {
// recover untracked trash items
debugPrint('$runtimeType refresh ${stopwatch.elapsed} recover untracked entries');
if (directory == null) {
- pendingNewEntries.addAll(await recoverLostTrashItems());
+ pendingNewEntries.addAll(await recoverUntrackedTrashItems());
}
// fetch new & modified entries
diff --git a/lib/model/source/trash.dart b/lib/model/source/trash.dart
index fac1387ff..0249da2d4 100644
--- a/lib/model/source/trash.dart
+++ b/lib/model/source/trash.dart
@@ -38,10 +38,11 @@ mixin TrashMixin on SourceBase {
return await completer.future;
}
- Future> recoverLostTrashItems() async {
+ Future> recoverUntrackedTrashItems() async {
+ final newEntries = {};
+
final knownPaths = allEntries.map((v) => v.trashDetails?.path).whereNotNull().toSet();
final untrackedPaths = await storageService.getUntrackedTrashPaths(knownPaths);
- final newEntries = {};
if (untrackedPaths.isNotEmpty) {
debugPrint('Recovering ${untrackedPaths.length} untracked bin items');
final recoveryPath = pContext.join(androidFileUtils.picturesPath, AndroidFileUtils.recoveryDir);
@@ -75,7 +76,7 @@ mixin TrashMixin on SourceBase {
sourceEntry.trashDetails = _buildTrashDetails(id);
newEntries.add(sourceEntry);
} else {
- debugPrint('Failed to recover untracked bin item at uri=$uri');
+ await reportService.recordError('Failed to recover untracked bin item at uri=$uri', null);
}
}
});
diff --git a/lib/model/vaults/vaults.dart b/lib/model/vaults/vaults.dart
index da80ec112..90a442dab 100644
--- a/lib/model/vaults/vaults.dart
+++ b/lib/model/vaults/vaults.dart
@@ -1,11 +1,15 @@
import 'dart:async';
import 'dart:io';
+import 'package:aves/model/entry/entry.dart';
+import 'package:aves/model/entry/origins.dart';
+import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/details.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves_screen_state/aves_screen_state.dart';
import 'package:collection/collection.dart';
-import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
final Vaults vaults = Vaults._private();
@@ -14,6 +18,8 @@ class Vaults extends ChangeNotifier {
Set _rows = {};
final Set _unlockedDirPaths = {};
+ static const _fileScheme = 'file';
+
Vaults._private();
Future init() async {
@@ -118,7 +124,7 @@ class Vaults extends ChangeNotifier {
bool isVaultEntryUri(String uriString) {
final uri = Uri.parse(uriString);
- if (uri.scheme != 'file') return false;
+ if (uri.scheme != _fileScheme) return false;
final path = uri.pathSegments.fold('', (prev, v) => '$prev${pContext.separator}$v');
return vaultDirectories.any(path.startsWith);
@@ -132,13 +138,47 @@ class Vaults extends ChangeNotifier {
_onLockStateChanged();
}
- void unlock(String dirPath) {
+ Future unlock(BuildContext context, String dirPath) async {
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return;
+ // recover untracked vault items
+ final source = context.read();
+ final newEntries = await recoverUntrackedItems(source, dirPath);
+ if (newEntries.isNotEmpty) {
+ source.addEntries(newEntries);
+ await metadataDb.saveEntries(newEntries);
+ unawaited(source.analyze(null, entries: newEntries));
+ }
+
_unlockedDirPaths.add(dirPath);
_onLockStateChanged();
}
+ Future> recoverUntrackedItems(CollectionSource source, String dirPath) async {
+ final newEntries = {};
+
+ final vaultName = detailsForPath(dirPath)?.name;
+ if (vaultName == null) return newEntries;
+
+ final knownPaths = source.allEntries.where((v) => v.origin == EntryOrigins.vault && v.directory == dirPath).map((v) => v.path).whereNotNull().toSet();
+ final untrackedPaths = await storageService.getUntrackedVaultPaths(vaultName, knownPaths);
+ if (untrackedPaths.isNotEmpty) {
+ debugPrint('Recovering ${untrackedPaths.length} untracked vault items');
+ await Future.forEach(untrackedPaths, (untrackedPath) async {
+ final uri = Uri.file(untrackedPath).toString();
+ final sourceEntry = await mediaFetchService.getEntry(uri, null);
+ if (sourceEntry != null) {
+ sourceEntry.id = metadataDb.nextId;
+ sourceEntry.origin = EntryOrigins.vault;
+ newEntries.add(sourceEntry);
+ } else {
+ await reportService.recordError('Failed to recover untracked vault item at uri=$uri', null);
+ }
+ });
+ }
+ return newEntries;
+ }
+
void _onScreenOff() => lock(all.where((v) => v.autoLockScreenOff).map((v) => v.path).toSet());
void _onLockStateChanged() {
diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart
index 648a59bd3..6bf1b681d 100644
--- a/lib/services/storage_service.dart
+++ b/lib/services/storage_service.dart
@@ -14,6 +14,8 @@ abstract class StorageService {
Future> getUntrackedTrashPaths(Iterable knownPaths);
+ Future> getUntrackedVaultPaths(String vaultName, Iterable knownPaths);
+
Future getVaultRoot();
Future getFreeSpace(StorageVolume volume);
@@ -86,6 +88,20 @@ class PlatformStorageService implements StorageService {
return {};
}
+ @override
+ Future> getUntrackedVaultPaths(String vaultName, Iterable knownPaths) async {
+ try {
+ final result = await _platform.invokeMethod('getUntrackedVaultPaths', {
+ 'vault': vaultName,
+ 'knownPaths': knownPaths.toList(),
+ });
+ return (result as List).cast().toSet();
+ } on PlatformException catch (e, stack) {
+ await reportService.recordError(e, stack);
+ }
+ return {};
+ }
+
@override
Future getVaultRoot() async {
try {
diff --git a/lib/widgets/common/action_mixins/vault_aware.dart b/lib/widgets/common/action_mixins/vault_aware.dart
index 2da5a7662..22f6f9f74 100644
--- a/lib/widgets/common/action_mixins/vault_aware.dart
+++ b/lib/widgets/common/action_mixins/vault_aware.dart
@@ -16,7 +16,7 @@ import 'package:local_auth/error_codes.dart' as auth_error;
import 'package:local_auth/local_auth.dart';
mixin VaultAwareMixin on FeedbackMixin {
- Future _tryUnlock(String dirPath, BuildContext context) async {
+ Future _tryUnlock(BuildContext context, String dirPath) async {
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return true;
final details = vaults.detailsForPath(dirPath);
@@ -67,12 +67,12 @@ mixin VaultAwareMixin on FeedbackMixin {
if (confirmed == null || !confirmed) return false;
- vaults.unlock(dirPath);
+ await vaults.unlock(context, dirPath);
return true;
}
Future unlockAlbum(BuildContext context, String dirPath) async {
- final success = await _tryUnlock(dirPath, context);
+ final success = await _tryUnlock(context, dirPath);
if (!success) {
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
}
diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart
index 105c13be9..7d156a89e 100644
--- a/lib/widgets/viewer/debug/db.dart
+++ b/lib/widgets/viewer/debug/db.dart
@@ -85,7 +85,14 @@ class _DbTabState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DB entry:${data == null ? ' no row' : ''}'),
- if (data != null)
+ if (data != null) ...[
+ ElevatedButton(
+ onPressed: () async {
+ final source = context.read();
+ await source.removeEntries({entry.uri}, includeTrash: true);
+ },
+ child: const Text('Untrack entry'),
+ ),
InfoRowGroup(
info: {
'uri': data.uri,
@@ -103,6 +110,7 @@ class _DbTabState extends State {
'trashed': '${data.trashed}',
},
),
+ ],
],
);
},
@@ -181,13 +189,6 @@ class _DbTabState extends State {
},
child: const Text('Remove details'),
),
- ElevatedButton(
- onPressed: () async {
- final source = context.read();
- await source.removeEntries({entry.uri}, includeTrash: true);
- },
- child: const Text('Untrack entry'),
- ),
InfoRowGroup(
info: {
'dateMillis': '${data.dateMillis}',