#170: improved directory limited source init

This commit is contained in:
Thibault Deckers 2022-02-22 15:06:09 +09:00
parent 2165a4e058
commit 8048dfa7d6
5 changed files with 67 additions and 25 deletions

View file

@ -27,6 +27,8 @@ import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.StorageUtils.PathSegments
import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
import deckers.thibault.aves.utils.StorageUtils.removeTrailingSeparator
import deckers.thibault.aves.utils.UriUtils.tryParseId
import java.io.File
import java.io.OutputStream
@ -47,14 +49,33 @@ class MediaStoreImageProvider : ImageProvider() {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
}
val handleNew: NewEntryHandler
var selection: String? = null
var selectionArgs: Array<String>? = null
if (directory != null) {
selection = "${MediaColumns.PATH} LIKE ?"
selectionArgs = arrayOf("${StorageUtils.ensureTrailingSeparator(directory)}%")
val relativePathDirectory = ensureTrailingSeparator(directory)
val relativePath = PathSegments(context, relativePathDirectory).relativeDir
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) {
selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaColumns.PATH} LIKE ?"
selectionArgs = arrayOf(relativePath, "relativePathDirectory%")
} else {
selection = "${MediaColumns.PATH} LIKE ?"
selectionArgs = arrayOf("$relativePathDirectory%")
}
val parentCheckDirectory = removeTrailingSeparator(directory)
handleNew = { entry ->
// skip entries in subfolders
val path = entry["path"] as String?
if (path != null && File(path).parent == parentCheckDirectory) {
handleNewEntry(entry)
}
}
} else {
handleNew = handleNewEntry
}
fetchFrom(context, isModified, handleNewEntry, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection = selection, selectionArgs = selectionArgs)
fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection = selection, selectionArgs = selectionArgs)
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs)
}
// the provided URI can point to the wrong media collection,
@ -407,7 +428,7 @@ class MediaStoreImageProvider : ImageProvider() {
if (toBin) {
val trashDir = StorageUtils.trashDirFor(activity, sourcePath)
if (trashDir != null) {
effectiveTargetDir = StorageUtils.ensureTrailingSeparator(trashDir.path)
effectiveTargetDir = ensureTrailingSeparator(trashDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(trashDir)
}
}
@ -452,7 +473,7 @@ class MediaStoreImageProvider : ImageProvider() {
toBin: Boolean,
): FieldMap {
val sourcePath = sourceFile.path
val sourceDir = sourceFile.parent?.let { StorageUtils.ensureTrailingSeparator(it) }
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy
return skippedFieldMap
@ -550,10 +571,7 @@ class MediaStoreImageProvider : ImageProvider() {
}
private fun isDownloadDir(context: Context, dirPath: String): Boolean {
var relativeDir = PathSegments(context, dirPath).relativeDir ?: ""
if (relativeDir.endsWith(File.separator)) {
relativeDir = relativeDir.substring(0, relativeDir.length - 1)
}
val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "")
return relativeDir == Environment.DIRECTORY_DOWNLOADS
}

View file

@ -294,10 +294,7 @@ object StorageUtils {
fun convertDirPathToTreeUri(context: Context, dirPath: String): Uri? {
val uuid = getVolumeUuidForTreeUri(context, dirPath)
if (uuid != null) {
var relativeDir = PathSegments(context, dirPath).relativeDir ?: ""
if (relativeDir.endsWith(File.separator)) {
relativeDir = relativeDir.substring(0, relativeDir.length - 1)
}
val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "")
return DocumentsContract.buildTreeDocumentUri("com.android.externalstorage.documents", "$uuid:$relativeDir")
}
Log.e(LOG_TAG, "failed to convert dirPath=$dirPath to tree URI")
@ -579,6 +576,10 @@ object StorageUtils {
return if (dirPath.endsWith(File.separator)) dirPath else dirPath + File.separator
}
fun removeTrailingSeparator(dirPath: String): String {
return if (dirPath.endsWith(File.separator)) dirPath.substring(0, dirPath.length - 1) else dirPath
}
// `fullPath` should match "volumePath + relativeDir + fileName"
class PathSegments(context: Context, fullPath: String) {
var volumePath: String? = null // `volumePath` with trailing "/"

View file

@ -160,13 +160,28 @@ class SqfliteMetadataDb implements MetadataDb {
@override
Future<Set<AvesEntry>> loadEntries({String? directory}) async {
String? where;
List<Object?>? whereArgs;
if (directory != null) {
where = 'path LIKE ?';
whereArgs = ['$directory%'];
final separator = pContext.separator;
if (!directory.endsWith(separator)) {
directory = '$directory$separator';
}
const where = 'path LIKE ?';
final whereArgs = ['$directory%'];
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
final dirLength = directory.length;
return rows
.whereNot((row) {
// skip entries in subfolders
final path = row['path'] as String?;
return path == null || path.substring(dirLength).contains(separator);
})
.map(AvesEntry.fromMap)
.toSet();
}
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
final rows = await _db.query(entryTable);
return rows.map(AvesEntry.fromMap).toSet();
}

View file

@ -88,10 +88,11 @@ class MediaStoreSource extends CollectionSource {
final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
if (topEntries.isNotEmpty) {
final obsoleteTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
await removeEntries(obsoleteTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false);
final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
await removeEntries(removedTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false);
}
knownEntries.removeWhere((entry) => removedContentIds.contains(entry.contentId));
final removedEntries = knownEntries.where((entry) => removedContentIds.contains(entry.contentId)).toSet();
knownEntries.removeAll(removedEntries);
// show known entries
debugPrint('$runtimeType refresh ${stopwatch.elapsed} add known entries');
@ -109,8 +110,10 @@ class MediaStoreSource extends CollectionSource {
}
// clean up obsolete entries
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
await metadataDb.removeIds(removedContentIds);
if (removedEntries.isNotEmpty) {
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
await metadataDb.removeIds(removedEntries.map((entry) => entry.id));
}
if (directory != null) {
// trash
@ -173,6 +176,7 @@ class MediaStoreSource extends CollectionSource {
updateDirectories();
}
debugPrint('$runtimeType refresh ${stopwatch.elapsed} analyze');
Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds;
if (analysisIds != null) {
@ -180,7 +184,7 @@ class MediaStoreSource extends CollectionSource {
}
await analyze(analysisController, entries: analysisEntries);
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedContentIds.length} obsolete');
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedEntries.length} removed');
},
onError: (error) => debugPrint('$runtimeType stream error=$error'),
);

View file

@ -143,6 +143,10 @@ class _AppDebugPageState extends State<AppDebugPage> {
onPressed: () => source.init(directory: '${androidFileUtils.dcimPath}/Camera'),
child: const Text('Source refresh (camera)'),
),
ElevatedButton(
onPressed: () => source.init(directory: androidFileUtils.picturesPath),
child: const Text('Source refresh (pictures)'),
),
ElevatedButton(
onPressed: () => AnalysisService.startService(force: false),
child: const Text('Start analysis service'),