view mode: fixed opening SVG, fixed deleting items scanned as file content

This commit is contained in:
Thibault Deckers 2021-06-17 16:09:59 +09:00
parent ba1175e344
commit 2ab0eaeae1
9 changed files with 79 additions and 52 deletions

View file

@ -252,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
private fun getShareableUri(uri: Uri): Uri? { private fun getShareableUri(uri: Uri): Uri? {
return when (uri.scheme?.toLowerCase(Locale.ROOT)) { return when (uri.scheme?.lowercase(Locale.ROOT)) {
ContentResolver.SCHEME_FILE -> { ContentResolver.SCHEME_FILE -> {
uri.path?.let { path -> uri.path?.let { path ->
val authority = "${context.applicationContext.packageName}.fileprovider" val authority = "${context.applicationContext.packageName}.fileprovider"

View file

@ -116,6 +116,16 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
} }
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val metadataMap = getContentResolverMetadataForUri(contentUri) ?: getContentResolverMetadataForUri(uri)
if (metadataMap != null) {
result.success(metadataMap)
} else {
result.error("getContentResolverMetadata-null", "failed to get cursor for contentUri=$contentUri", null)
}
}
private fun getContentResolverMetadataForUri(contentUri: Uri): FieldMap? {
val cursor = context.contentResolver.query(contentUri, null, null, null, null) val cursor = context.contentResolver.query(contentUri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
val metadataMap = HashMap<String, Any?>() val metadataMap = HashMap<String, Any?>()
@ -137,10 +147,9 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
} }
cursor.close() cursor.close()
result.success(metadataMap) return metadataMap
} else {
result.error("getContentResolverMetadata-null", "failed to get cursor for contentUri=$contentUri", null)
} }
return null
} }
private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) {

View file

@ -73,7 +73,7 @@ object Metadata {
var timeZone: TimeZone? = null var timeZone: TimeZone? = null
val timeZoneMatcher = VIDEO_TIMEZONE_PATTERN.matcher(dateString) val timeZoneMatcher = VIDEO_TIMEZONE_PATTERN.matcher(dateString)
if (timeZoneMatcher.find()) { if (timeZoneMatcher.find()) {
timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z".toRegex(), "")}") timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z", "")}")
dateString = timeZoneMatcher.replaceAll("") dateString = timeZoneMatcher.replaceAll("")
} }

View file

@ -436,26 +436,9 @@ abstract class ImageProvider {
protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap = protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
suspendCoroutine { cont -> suspendCoroutine { cont ->
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? -> MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
var contentId: Long? = null fun scanUri(uri: Uri?): FieldMap? {
var contentUri: Uri? = null uri ?: return null
if (newUri != null) {
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
}
}
}
if (contentUri == null) {
cont.resumeWithException(Exception("failed to get content URI of item at path=$path"))
return@scanFile
}
val newFields = HashMap<String, Any?>()
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
val projection = arrayOf( val projection = arrayOf(
MediaStore.MediaColumns.DATE_MODIFIED, MediaStore.MediaColumns.DATE_MODIFIED,
@ -463,25 +446,48 @@ abstract class ImageProvider {
MediaStore.MediaColumns.TITLE, MediaStore.MediaColumns.TITLE,
) )
try { try {
val cursor = context.contentResolver.query(contentUri, projection, null, null, null) val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
newFields["uri"] = contentUri.toString() val newFields = HashMap<String, Any?>()
newFields["contentId"] = contentId newFields["uri"] = uri.toString()
newFields["contentId"] = uri.tryParseId()
newFields["path"] = path newFields["path"] = path
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) } cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
cursor.close() cursor.close()
return newFields
} }
} catch (e: Exception) { } catch (e: Exception) {
cont.resumeWithException(e) Log.w(LOG_TAG, "failed to scan uri=$uri", e)
}
return null
}
if (newUri == null) {
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
return@scanFile return@scanFile
} }
if (newFields.isEmpty()) { var contentUri: Uri? = null
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)")) // `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
} else { // but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
}
}
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
if (newFields != null) {
cont.resume(newFields) cont.resume(newFields)
} else {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
} }
} }
} }

View file

@ -7,11 +7,11 @@ import java.util.*
object ImageProviderFactory { object ImageProviderFactory {
fun getProvider(uri: Uri): ImageProvider? { fun getProvider(uri: Uri): ImageProvider? {
return when (uri.scheme?.toLowerCase(Locale.ROOT)) { return when (uri.scheme?.lowercase(Locale.ROOT)) {
ContentResolver.SCHEME_CONTENT -> { ContentResolver.SCHEME_CONTENT -> {
// a URI's authority is [userinfo@]host[:port] // a URI's authority is [userinfo@]host[:port]
// but we only want the host when comparing to Media Store's "authority" // but we only want the host when comparing to Media Store's "authority"
return when (uri.host?.toLowerCase(Locale.ROOT)) { return when (uri.host?.lowercase(Locale.ROOT)) {
MediaStore.AUTHORITY -> MediaStoreImageProvider() MediaStore.AUTHORITY -> MediaStoreImageProvider()
else -> ContentImageProvider() else -> ContentImageProvider()
} }

View file

@ -300,8 +300,17 @@ object StorageUtils {
Log.w(LOG_TAG, "failed to get document URI for mediaUri=$mediaUri", e) Log.w(LOG_TAG, "failed to get document URI for mediaUri=$mediaUri", e)
} }
} }
// fallback for older APIs // fallback for older APIs
return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) } val df = getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) }
if (df != null) return df
// try to strip user info, if any
if (mediaUri.userInfo != null) {
val genericMediaUri = Uri.parse(mediaUri.toString().replaceFirst("${mediaUri.userInfo}@", ""))
Log.d(LOG_TAG, "retry getDocumentFile for mediaUri=$mediaUri without userInfo: $genericMediaUri")
return getDocumentFile(context, anyPath, genericMediaUri)
}
} }
// good old `File` // good old `File`
return DocumentFileCompat.fromFile(File(anyPath)) return DocumentFileCompat.fromFile(File(anyPath))

View file

@ -418,7 +418,7 @@ class AvesEntry {
addressDetails = null; addressDetails = null;
} }
Future<void> catalog({bool background = false}) async { Future<void> catalog({bool background = false, bool persist = true}) async {
if (isCatalogued) return; if (isCatalogued) return;
if (isSvg) { if (isSvg) {
// vector image sizing is not essential, so we should not spend time for it during loading // vector image sizing is not essential, so we should not spend time for it during loading
@ -428,7 +428,7 @@ class AvesEntry {
await _applyNewFields({ await _applyNewFields({
'width': size.width.round(), 'width': size.width.round(),
'height': size.height.round(), 'height': size.height.round(),
}); }, persist: persist);
} }
catalogMetadata = CatalogMetadata(contentId: contentId); catalogMetadata = CatalogMetadata(contentId: contentId);
} else { } else {
@ -538,7 +538,7 @@ class AvesEntry {
_addressDetails?.locality, _addressDetails?.locality,
}.any((s) => s != null && s.toUpperCase().contains(query)); }.any((s) => s != null && s.toUpperCase().contains(query));
Future<void> _applyNewFields(Map newFields) async { Future<void> _applyNewFields(Map newFields, {required bool persist}) async {
final uri = newFields['uri']; final uri = newFields['uri'];
if (uri is String) this.uri = uri; if (uri is String) this.uri = uri;
final path = newFields['path']; final path = newFields['path'];
@ -560,8 +560,10 @@ class AvesEntry {
final isFlipped = newFields['isFlipped']; final isFlipped = newFields['isFlipped'];
if (isFlipped is bool) this.isFlipped = isFlipped; if (isFlipped is bool) this.isFlipped = isFlipped;
if (persist) {
await metadataDb.saveEntries({this}); await metadataDb.saveEntries({this});
if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!}); if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!});
}
metadataChangeNotifier.notifyListeners(); metadataChangeNotifier.notifyListeners();
} }
@ -573,7 +575,7 @@ class AvesEntry {
final oldDateModifiedSecs = dateModifiedSecs; final oldDateModifiedSecs = dateModifiedSecs;
final oldRotationDegrees = rotationDegrees; final oldRotationDegrees = rotationDegrees;
final oldIsFlipped = isFlipped; final oldIsFlipped = isFlipped;
await _applyNewFields(newFields); await _applyNewFields(newFields, persist: true);
await _onImageChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); await _onImageChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
return true; return true;
} }
@ -585,7 +587,7 @@ class AvesEntry {
final oldDateModifiedSecs = dateModifiedSecs; final oldDateModifiedSecs = dateModifiedSecs;
final oldRotationDegrees = rotationDegrees; final oldRotationDegrees = rotationDegrees;
final oldIsFlipped = isFlipped; final oldIsFlipped = isFlipped;
await _applyNewFields(newFields); await _applyNewFields(newFields, persist: true);
await _onImageChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); await _onImageChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
return true; return true;
} }

View file

@ -119,7 +119,7 @@ class _HomePageState extends State<HomePage> {
final entry = await imageFileService.getEntry(uri, mimeType); final entry = await imageFileService.getEntry(uri, mimeType);
if (entry != null) { if (entry != null) {
// cataloguing is essential for coordinates and video rotation // cataloguing is essential for coordinates and video rotation
await entry.catalog(); await entry.catalog(background: false, persist: false);
} }
return entry; return entry;
} }

View file

@ -187,6 +187,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final selectionCount = selection.length; final selectionCount = selection.length;
showOpReport<ExportOpEvent>( showOpReport<ExportOpEvent>(
context: context, context: context,
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
opStream: imageFileService.export( opStream: imageFileService.export(
selection, selection,
mimeType: MimeTypes.jpeg, mimeType: MimeTypes.jpeg,