view mode: fixed opening SVG, fixed deleting items scanned as file content
This commit is contained in:
parent
ba1175e344
commit
2ab0eaeae1
9 changed files with 79 additions and 52 deletions
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -436,52 +436,58 @@ 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")
|
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
|
||||||
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
val projection = arrayOf(
|
||||||
contentId = newUri.tryParseId()
|
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||||
if (contentId != null) {
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
if (isImage(mimeType)) {
|
MediaStore.MediaColumns.TITLE,
|
||||||
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
|
)
|
||||||
} else if (isVideo(mimeType)) {
|
try {
|
||||||
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
|
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
val newFields = HashMap<String, Any?>()
|
||||||
|
newFields["uri"] = uri.toString()
|
||||||
|
newFields["contentId"] = uri.tryParseId()
|
||||||
|
newFields["path"] = path
|
||||||
|
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.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
|
||||||
|
cursor.close()
|
||||||
|
return newFields
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to scan uri=$uri", e)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
if (contentUri == null) {
|
|
||||||
cont.resumeWithException(Exception("failed to get content URI of item at path=$path"))
|
if (newUri == null) {
|
||||||
|
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
|
||||||
return@scanFile
|
return@scanFile
|
||||||
}
|
}
|
||||||
|
|
||||||
val newFields = HashMap<String, Any?>()
|
var contentUri: Uri? = null
|
||||||
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
|
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
||||||
val projection = arrayOf(
|
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
val contentId = newUri.tryParseId()
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
if (contentId != null) {
|
||||||
MediaStore.MediaColumns.TITLE,
|
if (isImage(mimeType)) {
|
||||||
)
|
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
|
||||||
try {
|
} else if (isVideo(mimeType)) {
|
||||||
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
|
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
newFields["uri"] = contentUri.toString()
|
|
||||||
newFields["contentId"] = contentId
|
|
||||||
newFields["path"] = path
|
|
||||||
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.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
|
|
||||||
cursor.close()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
cont.resumeWithException(e)
|
|
||||||
return@scanFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newFields.isEmpty()) {
|
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
|
||||||
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
|
val newFields = scanUri(contentUri) ?: scanUri(newUri)
|
||||||
} else {
|
|
||||||
|
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)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
await metadataDb.saveEntries({this});
|
if (persist) {
|
||||||
if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!});
|
await metadataDb.saveEntries({this});
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue