diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java index f3f99007f..377435503 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java @@ -2,6 +2,7 @@ package deckers.thibault.aves.channelhandlers; import android.content.Context; import android.media.MediaMetadataRetriever; +import android.net.Uri; import android.text.format.Formatter; import androidx.annotation.NonNull; @@ -71,9 +72,15 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { return mimeType != null && mimeType.startsWith(Constants.MIME_VIDEO); } + private InputStream getInputStream(String path, String uri) throws FileNotFoundException { + // FileInputStream is faster than input stream from ContentResolver + return path != null ? new FileInputStream(path) : context.getContentResolver().openInputStream(Uri.parse(uri)); + } + private void getAllMetadata(MethodCall call, MethodChannel.Result result) { String path = call.argument("path"); - try (InputStream is = new FileInputStream(path)) { + String uri = call.argument("uri"); + try (InputStream is = getInputStream(path, uri)) { Map> metadataMap = new HashMap<>(); Metadata metadata = ImageMetadataReader.readMetadata(is); for (Directory dir : metadata.getDirectories()) { @@ -109,14 +116,15 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { } catch (ImageProcessingException e) { getAllVideoMetadataFallback(call, result); } catch (FileNotFoundException e) { - result.error("getAllMetadata-filenotfound", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getAllMetadata-filenotfound", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } catch (Exception e) { - result.error("getAllMetadata-exception", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getAllMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } } private void getAllVideoMetadataFallback(MethodCall call, MethodChannel.Result result) { String path = call.argument("path"); + String uri = call.argument("uri"); try { Map> metadataMap = new HashMap<>(); Map dirMap = new HashMap<>(); @@ -124,7 +132,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { metadataMap.put("", dirMap); MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(path); + if (path != null) { + retriever.setDataSource(path); + } else { + retriever.setDataSource(context, Uri.parse(uri)); + } for (Map.Entry kv : Constants.MEDIA_METADATA_KEYS.entrySet()) { Integer key = kv.getKey(); String value = retriever.extractMetadata(key); @@ -144,14 +156,15 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { result.success(metadataMap); } catch (Exception e) { - result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } } private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) { - String path = call.argument("path"); String mimeType = call.argument("mimeType"); - try (InputStream is = new FileInputStream(path)) { + String path = call.argument("path"); + String uri = call.argument("uri"); + try (InputStream is = getInputStream(path, uri)) { Map metadataMap = new HashMap<>(); if (!Constants.MIME_MP2T.equalsIgnoreCase(mimeType)) { Metadata metadata = ImageMetadataReader.readMetadata(is); @@ -197,7 +210,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { if (isVideo(call.argument("mimeType"))) { try { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(path); + if (path != null) { + retriever.setDataSource(path); + } else { + retriever.setDataSource(context, Uri.parse(uri)); + } String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); String rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); String locationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION); @@ -233,16 +250,16 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { } } } catch (Exception e) { - result.error("getCatalogMetadata-exception", "failed to get video metadata for path=" + path, e.getMessage()); + result.error("getCatalogMetadata-exception", "failed to get video metadata for uri=" + uri + ", path=" + path, e.getMessage()); } } result.success(metadataMap); } catch (ImageProcessingException e) { - result.error("getCatalogMetadata-imageprocessing", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getCatalogMetadata-imageprocessing", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } catch (FileNotFoundException e) { - result.error("getCatalogMetadata-filenotfound", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getCatalogMetadata-filenotfound", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } catch (Exception e) { - result.error("getCatalogMetadata-exception", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getCatalogMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } } @@ -255,7 +272,8 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { } String path = call.argument("path"); - try (InputStream is = new FileInputStream(path)) { + String uri = call.argument("uri"); + try (InputStream is = getInputStream(path, uri)) { Metadata metadata = ImageMetadataReader.readMetadata(is); ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); if (directory != null) { @@ -274,11 +292,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler { } result.success(metadataMap); } catch (ImageProcessingException e) { - result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } catch (FileNotFoundException e) { - result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getOverlayMetadata-filenotfound", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } catch (Exception e) { - result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e.getMessage()); + result.error("getOverlayMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); } } } \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java index f21406f0a..fd1843c66 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java @@ -21,6 +21,8 @@ public class ImageProviderFactory { return new MediaStoreImageProvider(); // case Constants.DOWNLOADS_AUTHORITY: // return new DownloadImageProvider(); + default: + return new UnknownContentImageProvider(); } } return null; diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/UnknownContentImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/UnknownContentImageProvider.java new file mode 100644 index 000000000..4c1a46c38 --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/UnknownContentImageProvider.java @@ -0,0 +1,87 @@ +package deckers.thibault.aves.model.provider; + +import android.app.Activity; +import android.graphics.BitmapFactory; +import android.net.Uri; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import com.drew.metadata.jpeg.JpegDirectory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode; + +class UnknownContentImageProvider extends ImageProvider { + @Override + public void fetchSingle(final Activity activity, final Uri uri, final String mimeType, final ImageOpCallback callback) { + int width = 0, height = 0, orientationDegrees = 0; + Long sourceDateTakenMillis = null; + + // check metadata first + try (InputStream is = activity.getContentResolver().openInputStream(uri)) { + Metadata metadata = ImageMetadataReader.readMetadata(is); + JpegDirectory jpegDir = metadata.getFirstDirectoryOfType(JpegDirectory.class); + if (jpegDir != null) { + if (jpegDir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) { + width = jpegDir.getInt(JpegDirectory.TAG_IMAGE_WIDTH); + } + if (jpegDir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) { + height = jpegDir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT); + } + } + ExifIFD0Directory exifDir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (exifDir != null) { + if (exifDir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { + orientationDegrees = getOrientationDegreesForExifCode(exifDir.getInt(ExifIFD0Directory.TAG_ORIENTATION)); + } + if (exifDir.containsTag(ExifIFD0Directory.TAG_DATETIME)) { + sourceDateTakenMillis = exifDir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime(); + } + } + } catch (IOException | ImageProcessingException | MetadataException e) { + // ignore + } + + // fallback to decoding the image bounds + if (width <= 0 || height <= 0) { + try (InputStream is = activity.getContentResolver().openInputStream(uri)) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + width = options.outWidth; + height = options.outHeight; + } catch (IOException e) { + // ignore + } + } + + if (width <= 0 || height <= 0) { + callback.onFailure(); + return; + } + + Map entry = new HashMap<>(); + entry.put("uri", uri.toString()); + entry.put("path", null); + entry.put("contentId", null); + entry.put("mimeType", mimeType); + entry.put("width", width); + entry.put("height", height); + entry.put("orientationDegrees", orientationDegrees); + entry.put("sizeBytes", null); + entry.put("title", null); + entry.put("dateModifiedSecs", null); + entry.put("sourceDateTakenMillis", sourceDateTakenMillis); + entry.put("bucketDisplayName", null); + entry.put("durationMillis", null); + callback.onSuccess(entry); + } +} diff --git a/lib/model/metadata_service.dart b/lib/model/metadata_service.dart index 0c5ffe779..e1321b98b 100644 --- a/lib/model/metadata_service.dart +++ b/lib/model/metadata_service.dart @@ -12,6 +12,7 @@ class MetadataService { final result = await platform.invokeMethod('getAllMetadata', { 'mimeType': entry.mimeType, 'path': entry.path, + 'uri': entry.uri, }); return result as Map; } on PlatformException catch (e) { @@ -30,6 +31,7 @@ class MetadataService { final result = await platform.invokeMethod('getCatalogMetadata', { 'mimeType': entry.mimeType, 'path': entry.path, + 'uri': entry.uri, }) as Map; result['contentId'] = entry.contentId; return CatalogMetadata.fromMap(result); @@ -45,6 +47,7 @@ class MetadataService { final result = await platform.invokeMethod('getOverlayMetadata', { 'mimeType': entry.mimeType, 'path': entry.path, + 'uri': entry.uri, }) as Map; return OverlayMetadata.fromMap(result); } on PlatformException catch (e) { diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index 78e8dd6fe..e4de60487 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -144,6 +144,10 @@ class _FullscreenBottomOverlayContent extends StatelessWidget { builder: (c, orientation, child) { final twoColumns = orientation == Orientation.landscape && maxWidth / 2 > _subRowMinWidth; final subRowWidth = twoColumns ? min(_subRowMinWidth, maxWidth / 2) : maxWidth; + final positionTitle = [ + if (position != null) position, + if (entry.title != null) entry.title, + ].join(' – '); final hasShootingDetails = details != null && !details.isEmpty; return Column( mainAxisSize: MainAxisSize.min, @@ -151,7 +155,7 @@ class _FullscreenBottomOverlayContent extends StatelessWidget { children: [ SizedBox( width: maxWidth, - child: Text('${position != null ? '$position – ' : ''}${entry.title ?? '?'}', strutStyle: Constants.overflowStrutStyle), + child: Text(positionTitle, strutStyle: Constants.overflowStrutStyle), ), if (entry.hasGps) Container(