read files with uri only, and fix to handle unknown MediaMetadataRetriever issues

This commit is contained in:
Thibault Deckers 2020-06-11 14:58:27 +09:00
parent cbacb923e7
commit a6eeba7744
6 changed files with 36 additions and 59 deletions

View file

@ -8,6 +8,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -29,12 +30,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import deckers.thibault.aves.utils.Utils;
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import static com.bumptech.glide.request.RequestOptions.centerCropTransform; import static com.bumptech.glide.request.RequestOptions.centerCropTransform;
public class AppAdapterHandler implements MethodChannel.MethodCallHandler { public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
private static final String LOG_TAG = Utils.createLogTag(AppAdapterHandler.class);
public static final String CHANNEL = "deckers.thibault/aves/app"; public static final String CHANNEL = "deckers.thibault/aves/app";
private Context context; private Context context;
@ -162,11 +166,11 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
data = stream.toByteArray(); data = stream.toByteArray();
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); Log.w(LOG_TAG, "failed to decode app icon for packageName=" + packageName, e);
} }
Glide.with(context).clear(target); Glide.with(context).clear(target);
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
e.printStackTrace(); Log.w(LOG_TAG, "failed to get app info for packageName=" + packageName, e);
return; return;
} }
if (data != null) { if (data != null) {

View file

@ -8,6 +8,7 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.format.Formatter; import android.text.format.Formatter;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -30,7 +31,6 @@ import com.drew.metadata.gif.GifAnimationDirectory;
import com.drew.metadata.webp.WebpDirectory; import com.drew.metadata.webp.WebpDirectory;
import com.drew.metadata.xmp.XmpDirectory; import com.drew.metadata.xmp.XmpDirectory;
import java.io.FileNotFoundException;
import java.io.InputStream; import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -42,10 +42,13 @@ import deckers.thibault.aves.utils.Constants;
import deckers.thibault.aves.utils.MetadataHelper; import deckers.thibault.aves.utils.MetadataHelper;
import deckers.thibault.aves.utils.MimeTypes; import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.StorageUtils; import deckers.thibault.aves.utils.StorageUtils;
import deckers.thibault.aves.utils.Utils;
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
public class MetadataHandler implements MethodChannel.MethodCallHandler { public class MetadataHandler implements MethodChannel.MethodCallHandler {
private static final String LOG_TAG = Utils.createLogTag(MetadataHandler.class);
public static final String CHANNEL = "deckers.thibault/aves/metadata"; public static final String CHANNEL = "deckers.thibault/aves/metadata";
// catalog metadata // catalog metadata
@ -105,12 +108,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} }
private void getAllMetadata(MethodCall call, MethodChannel.Result result) { private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
String uri = call.argument("uri"); String uri = call.argument("uri");
Map<String, Map<String, String>> metadataMap = new HashMap<>(); Map<String, Map<String, String>> metadataMap = new HashMap<>();
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri), path)) { try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
Metadata metadata = ImageMetadataReader.readMetadata(is); Metadata metadata = ImageMetadataReader.readMetadata(is);
for (Directory dir : metadata.getDirectories()) { for (Directory dir : metadata.getDirectories()) {
if (dir.getTagCount() > 0) { if (dir.getTagCount() > 0) {
@ -136,7 +138,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} }
} }
} catch (XMPException e) { } catch (XMPException e) {
e.printStackTrace(); Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uri, e);
} }
} }
} }
@ -144,15 +146,12 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
result.success(metadataMap); result.success(metadataMap);
} catch (ImageProcessingException e) { } catch (ImageProcessingException e) {
getAllVideoMetadataFallback(call, result); getAllVideoMetadataFallback(call, result);
} catch (FileNotFoundException e) {
result.error("getAllMetadata-filenotfound", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage());
} catch (Exception e) { } catch (Exception e) {
result.error("getAllMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); result.error("getAllMetadata-exception", "failed to get metadata for uri=" + uri, e.getMessage());
} }
} }
private void getAllVideoMetadataFallback(MethodCall call, MethodChannel.Result result) { private void getAllVideoMetadataFallback(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
String uri = call.argument("uri"); String uri = call.argument("uri");
Map<String, Map<String, String>> metadataMap = new HashMap<>(); Map<String, Map<String, String>> metadataMap = new HashMap<>();
@ -160,7 +159,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
// unnamed fallback directory // unnamed fallback directory
metadataMap.put("", dirMap); metadataMap.put("", dirMap);
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri), path); MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
try { try {
for (Map.Entry<Integer, String> kv : Constants.MEDIA_METADATA_KEYS.entrySet()) { for (Map.Entry<Integer, String> kv : Constants.MEDIA_METADATA_KEYS.entrySet()) {
Integer key = kv.getKey(); Integer key = kv.getKey();
@ -179,7 +178,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} }
result.success(metadataMap); result.success(metadataMap);
} catch (Exception e) { } catch (Exception e) {
result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for uri=" + uri, e.getMessage());
} finally { } finally {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release(); retriever.release();
@ -188,12 +187,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) { private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) {
String mimeType = call.argument("mimeType"); String mimeType = call.argument("mimeType");
String path = call.argument("path");
String uri = call.argument("uri"); String uri = call.argument("uri");
Map<String, Object> metadataMap = new HashMap<>(); Map<String, Object> metadataMap = new HashMap<>();
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri), path)) { try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
if (!MimeTypes.MP2T.equals(mimeType)) { if (!MimeTypes.MP2T.equals(mimeType)) {
Metadata metadata = ImageMetadataReader.readMetadata(is); Metadata metadata = ImageMetadataReader.readMetadata(is);
@ -233,7 +231,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
putLocalizedTextFromXmp(metadataMap, KEY_XMP_TITLE_DESCRIPTION, xmpMeta, XMP_DESCRIPTION_PROP_NAME); putLocalizedTextFromXmp(metadataMap, KEY_XMP_TITLE_DESCRIPTION, xmpMeta, XMP_DESCRIPTION_PROP_NAME);
} }
} catch (XMPException e) { } catch (XMPException e) {
e.printStackTrace(); Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uri, e);
} }
} }
@ -251,7 +249,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} }
if (isVideo(mimeType)) { if (isVideo(mimeType)) {
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri), path); MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
try { try {
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
String rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); String rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
@ -287,25 +285,20 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} }
} }
} catch (Exception e) { } catch (Exception e) {
result.error("getCatalogMetadata-exception", "failed to get video metadata for uri=" + uri + ", path=" + path, e.getMessage()); result.error("getCatalogMetadata-exception", "failed to get video metadata for uri=" + uri, e.getMessage());
} finally { } finally {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release(); retriever.release();
} }
} }
result.success(metadataMap); result.success(metadataMap);
} catch (ImageProcessingException e) {
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 uri=" + uri + ", path=" + path, e.getMessage());
} catch (Exception e) { } catch (Exception e) {
result.error("getCatalogMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); result.error("getCatalogMetadata-exception", "failed to get metadata for uri=" + uri, e.getMessage());
} }
} }
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) { private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
String mimeType = call.argument("mimeType"); String mimeType = call.argument("mimeType");
String path = call.argument("path");
String uri = call.argument("uri"); String uri = call.argument("uri");
Map<String, Object> metadataMap = new HashMap<>(); Map<String, Object> metadataMap = new HashMap<>();
@ -315,7 +308,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
return; return;
} }
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri), path)) { try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
Metadata metadata = ImageMetadataReader.readMetadata(is); Metadata metadata = ImageMetadataReader.readMetadata(is);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory != null) { if (directory != null) {
@ -327,12 +320,8 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} }
} }
result.success(metadataMap); result.success(metadataMap);
} catch (ImageProcessingException e) {
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 uri=" + uri + ", path=" + path, e.getMessage());
} catch (Exception e) { } catch (Exception e) {
result.error("getOverlayMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); result.error("getOverlayMetadata-exception", "failed to get metadata for uri=" + uri, e.getMessage());
} }
} }

View file

@ -24,7 +24,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
@Override @Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) { public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri(), null); MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri());
try { try {
byte[] picture = retriever.getEmbeddedPicture(); byte[] picture = retriever.getEmbeddedPicture();
if (picture != null) { if (picture != null) {

View file

@ -119,7 +119,7 @@ public class ImageEntry {
// metadata retrieval // metadata retrieval
// expects entry with: uri/path, mimeType // expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration // finds: width, height, orientation/rotation, date, title, duration
public ImageEntry fillPreCatalogMetadata(Context context) { public ImageEntry fillPreCatalogMetadata(Context context) {
fillByMediaMetadataRetriever(context); fillByMediaMetadataRetriever(context);
@ -130,10 +130,10 @@ public class ImageEntry {
return this; return this;
} }
// expects entry with: uri/path, mimeType // expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration // finds: width, height, orientation/rotation, date, title, duration
private void fillByMediaMetadataRetriever(Context context) { private void fillByMediaMetadataRetriever(Context context) {
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri, path); MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
try { try {
String width = null, height = null, rotation = null, durationMillis = null; String width = null, height = null, rotation = null, durationMillis = null;
if (isImage()) { if (isImage()) {
@ -180,12 +180,12 @@ public class ImageEntry {
} }
} }
// expects entry with: uri/path, mimeType // expects entry with: uri, mimeType
// finds: width, height, orientation, date // finds: width, height, orientation, date
private void fillByMetadataExtractor(Context context) { private void fillByMetadataExtractor(Context context) {
if (MimeTypes.SVG.equals(mimeType)) return; if (MimeTypes.SVG.equals(mimeType)) return;
try (InputStream is = StorageUtils.openInputStream(context, uri, path)) { try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is); Metadata metadata = ImageMetadataReader.readMetadata(is);
if (MimeTypes.JPEG.equals(mimeType)) { if (MimeTypes.JPEG.equals(mimeType)) {
@ -242,12 +242,12 @@ public class ImageEntry {
} }
} }
// expects entry with: uri/path // expects entry with: uri
// finds: width, height // finds: width, height
private void fillByBitmapDecode(Context context) { private void fillByBitmapDecode(Context context) {
if (MimeTypes.SVG.equals(mimeType)) return; if (MimeTypes.SVG.equals(mimeType)) return;
try (InputStream is = StorageUtils.openInputStream(context, uri, path)) { try (InputStream is = StorageUtils.openInputStream(context, uri)) {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options); BitmapFactory.decodeStream(is, null, options);

View file

@ -42,41 +42,28 @@ public class StorageUtils {
return uri != null && ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme()) && MediaStore.AUTHORITY.equalsIgnoreCase(uri.getHost()); return uri != null && ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme()) && MediaStore.AUTHORITY.equalsIgnoreCase(uri.getHost());
} }
public static InputStream openInputStream(Context context, Uri uri, String path) throws FileNotFoundException { public static InputStream openInputStream(Context context, Uri uri) throws FileNotFoundException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// we get a permission denial if we require original from a provider other than the media store // we get a permission denial if we require original from a provider other than the media store
if (isMediaStoreContentUri(uri)) { if (isMediaStoreContentUri(uri)) {
uri = MediaStore.setRequireOriginal(uri); uri = MediaStore.setRequireOriginal(uri);
} }
return context.getContentResolver().openInputStream(uri);
} }
return context.getContentResolver().openInputStream(uri);
// on Android <Q, we directly work with file paths if possible,
// as `FileInputStream` is faster than input stream from `ContentResolver`
return path != null ? new FileInputStream(path) : context.getContentResolver().openInputStream(uri);
} }
public static MediaMetadataRetriever openMetadataRetriever(Context context, Uri uri, String path) { public static MediaMetadataRetriever openMetadataRetriever(Context context, Uri uri) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever(); MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// we get a permission denial if we require original from a provider other than the media store // we get a permission denial if we require original from a provider other than the media store
if (isMediaStoreContentUri(uri)) { if (isMediaStoreContentUri(uri)) {
uri = MediaStore.setRequireOriginal(uri); uri = MediaStore.setRequireOriginal(uri);
} }
retriever.setDataSource(context, uri);
return retriever;
} }
retriever.setDataSource(context, uri);
// on Android <Q, we directly work with file paths if possible } catch (Exception e) {
if (path != null) { Log.w(LOG_TAG, "failed to open MediaMetadataRetriever for uri=" + uri, e);
retriever.setDataSource(path);
} else {
retriever.setDataSource(context, uri);
}
} catch (IllegalArgumentException e) {
Log.w(LOG_TAG, "failed to open MediaMetadataRetriever for uri=" + uri + ", path=" + path);
} }
return retriever; return retriever;
} }

View file

@ -14,7 +14,6 @@ class MetadataService {
try { try {
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{ final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'path': entry.path,
'uri': entry.uri, 'uri': entry.uri,
}); });
return result as Map; return result as Map;
@ -40,7 +39,6 @@ class MetadataService {
// 'xmpTitleDescription': XMP title or XMP description (string) // 'xmpTitleDescription': XMP title or XMP description (string)
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{ final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'path': entry.path,
'uri': entry.uri, 'uri': entry.uri,
}) as Map; }) as Map;
result['contentId'] = entry.contentId; result['contentId'] = entry.contentId;
@ -61,7 +59,6 @@ class MetadataService {
// return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso' // return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso'
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{ final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'path': entry.path,
'uri': entry.uri, 'uri': entry.uri,
}) as Map; }) as Map;
return OverlayMetadata.fromMap(result); return OverlayMetadata.fromMap(result);