#170: improved directory limited source init
This commit is contained in:
parent
2165a4e058
commit
8048dfa7d6
5 changed files with 67 additions and 25 deletions
|
@ -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) {
|
||||||
selection = "${MediaColumns.PATH} LIKE ?"
|
val relativePathDirectory = ensureTrailingSeparator(directory)
|
||||||
selectionArgs = arrayOf("${StorageUtils.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, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs)
|
||||||
fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection = selection, selectionArgs = 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 "/"
|
||||||
|
|
|
@ -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 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();
|
return rows.map(AvesEntry.fromMap).toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
|
if (removedEntries.isNotEmpty) {
|
||||||
await metadataDb.removeIds(removedContentIds);
|
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
|
||||||
|
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'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
Loading…
Reference in a new issue