#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.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.StorageUtils.PathSegments 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 deckers.thibault.aves.utils.UriUtils.tryParseId
import java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
@ -47,14 +49,33 @@ class MediaStoreImageProvider : ImageProvider() {
val knownDate = knownEntries[contentId] val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs return knownDate == null || knownDate < dateModifiedSecs
} }
val handleNew: NewEntryHandler
var selection: String? = null var selection: String? = null
var selectionArgs: Array<String>? = null var selectionArgs: Array<String>? = null
if (directory != null) { if (directory != null) {
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 ?" selection = "${MediaColumns.PATH} LIKE ?"
selectionArgs = arrayOf("${StorageUtils.ensureTrailingSeparator(directory)}%") selectionArgs = arrayOf("$relativePathDirectory%")
} }
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) 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, 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, // the provided URI can point to the wrong media collection,
@ -407,7 +428,7 @@ class MediaStoreImageProvider : ImageProvider() {
if (toBin) { if (toBin) {
val trashDir = StorageUtils.trashDirFor(activity, sourcePath) val trashDir = StorageUtils.trashDirFor(activity, sourcePath)
if (trashDir != null) { if (trashDir != null) {
effectiveTargetDir = StorageUtils.ensureTrailingSeparator(trashDir.path) effectiveTargetDir = ensureTrailingSeparator(trashDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(trashDir) targetDirDocFile = DocumentFileCompat.fromFile(trashDir)
} }
} }
@ -452,7 +473,7 @@ class MediaStoreImageProvider : ImageProvider() {
toBin: Boolean, toBin: Boolean,
): FieldMap { ): FieldMap {
val sourcePath = sourceFile.path 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)) { if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy // nothing to do unless it's a renamed copy
return skippedFieldMap return skippedFieldMap
@ -550,10 +571,7 @@ class MediaStoreImageProvider : ImageProvider() {
} }
private fun isDownloadDir(context: Context, dirPath: String): Boolean { private fun isDownloadDir(context: Context, dirPath: String): Boolean {
var relativeDir = PathSegments(context, dirPath).relativeDir ?: "" val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "")
if (relativeDir.endsWith(File.separator)) {
relativeDir = relativeDir.substring(0, relativeDir.length - 1)
}
return relativeDir == Environment.DIRECTORY_DOWNLOADS return relativeDir == Environment.DIRECTORY_DOWNLOADS
} }

View file

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

View file

@ -160,13 +160,28 @@ class SqfliteMetadataDb implements MetadataDb {
@override @override
Future<Set<AvesEntry>> loadEntries({String? directory}) async { Future<Set<AvesEntry>> loadEntries({String? directory}) async {
String? where;
List<Object?>? whereArgs;
if (directory != null) { if (directory != null) {
where = 'path LIKE ?'; final separator = pContext.separator;
whereArgs = ['$directory%']; 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 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);
return rows.map(AvesEntry.fromMap).toSet(); return rows.map(AvesEntry.fromMap).toSet();
} }

View file

@ -88,10 +88,11 @@ class MediaStoreSource extends CollectionSource {
final knownContentIds = knownDateByContentId.keys.toList(); final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet(); final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
if (topEntries.isNotEmpty) { if (topEntries.isNotEmpty) {
final obsoleteTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId)); final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
await removeEntries(obsoleteTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false); 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 // show known entries
debugPrint('$runtimeType refresh ${stopwatch.elapsed} add known entries'); debugPrint('$runtimeType refresh ${stopwatch.elapsed} add known entries');
@ -109,8 +110,10 @@ class MediaStoreSource extends CollectionSource {
} }
// clean up obsolete entries // clean up obsolete entries
if (removedEntries.isNotEmpty) {
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries'); debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
await metadataDb.removeIds(removedContentIds); await metadataDb.removeIds(removedEntries.map((entry) => entry.id));
}
if (directory != null) { if (directory != null) {
// trash // trash
@ -173,6 +176,7 @@ class MediaStoreSource extends CollectionSource {
updateDirectories(); updateDirectories();
} }
debugPrint('$runtimeType refresh ${stopwatch.elapsed} analyze');
Set<AvesEntry>? analysisEntries; Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds; final analysisIds = analysisController?.entryIds;
if (analysisIds != null) { if (analysisIds != null) {
@ -180,7 +184,7 @@ class MediaStoreSource extends CollectionSource {
} }
await analyze(analysisController, entries: analysisEntries); 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'), 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'), onPressed: () => source.init(directory: '${androidFileUtils.dcimPath}/Camera'),
child: const Text('Source refresh (camera)'), child: const Text('Source refresh (camera)'),
), ),
ElevatedButton(
onPressed: () => source.init(directory: androidFileUtils.picturesPath),
child: const Text('Source refresh (pictures)'),
),
ElevatedButton( ElevatedButton(
onPressed: () => AnalysisService.startService(force: false), onPressed: () => AnalysisService.startService(force: false),
child: const Text('Start analysis service'), child: const Text('Start analysis service'),