scoped storage: fixed opening files and reading metadata
This commit is contained in:
parent
836e7fe4d0
commit
1d6103c0c0
7 changed files with 79 additions and 56 deletions
|
@ -7,6 +7,9 @@
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<!-- to access media with unredacted metadata with scoped storage (Android 10+) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
|
|
||||||
<!-- TODO remove this permission once this issue is fixed:
|
<!-- TODO remove this permission once this issue is fixed:
|
||||||
https://github.com/flutter/flutter/issues/42349
|
https://github.com/flutter/flutter/issues/42349
|
||||||
https://github.com/flutter/flutter/issues/42451
|
https://github.com/flutter/flutter/issues/42451
|
||||||
|
|
|
@ -26,7 +26,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.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -38,6 +37,7 @@ import java.util.regex.Pattern;
|
||||||
import deckers.thibault.aves.utils.Constants;
|
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 io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
|
@ -97,18 +97,13 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
return mimeType != null && mimeType.startsWith(MimeTypes.VIDEO);
|
return mimeType != null && mimeType.startsWith(MimeTypes.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) {
|
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String path = call.argument("path");
|
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 = getInputStream(path, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri), path)) {
|
||||||
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) {
|
||||||
|
@ -158,13 +153,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
// unnamed fallback directory
|
// unnamed fallback directory
|
||||||
metadataMap.put("", dirMap);
|
metadataMap.put("", dirMap);
|
||||||
|
|
||||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
try (MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri), path)) {
|
||||||
try {
|
|
||||||
if (path != null) {
|
|
||||||
retriever.setDataSource(path);
|
|
||||||
} else {
|
|
||||||
retriever.setDataSource(context, Uri.parse(uri));
|
|
||||||
}
|
|
||||||
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();
|
||||||
String value = retriever.extractMetadata(key);
|
String value = retriever.extractMetadata(key);
|
||||||
|
@ -183,8 +172,6 @@ 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 + ", path=" + path, e.getMessage());
|
||||||
} finally {
|
|
||||||
retriever.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +182,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
try (InputStream is = getInputStream(path, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri), path)) {
|
||||||
if (!MimeTypes.MP2T.equals(mimeType)) {
|
if (!MimeTypes.MP2T.equals(mimeType)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
|
||||||
|
@ -253,13 +240,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
try (MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri), path)) {
|
||||||
try {
|
|
||||||
if (path != null) {
|
|
||||||
retriever.setDataSource(path);
|
|
||||||
} else {
|
|
||||||
retriever.setDataSource(context, Uri.parse(uri));
|
|
||||||
}
|
|
||||||
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);
|
||||||
String locationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION);
|
String locationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION);
|
||||||
|
@ -295,8 +276,6 @@ 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 + ", path=" + path, e.getMessage());
|
||||||
} finally {
|
|
||||||
retriever.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
|
@ -321,7 +300,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (InputStream is = getInputStream(path, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri), path)) {
|
||||||
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) {
|
||||||
|
|
|
@ -13,6 +13,8 @@ import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils;
|
||||||
|
|
||||||
class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
||||||
private final VideoThumbnail model;
|
private final VideoThumbnail model;
|
||||||
|
|
||||||
|
@ -22,9 +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 = new MediaMetadataRetriever();
|
try (MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri(), null)) {
|
||||||
try {
|
|
||||||
retriever.setDataSource(model.getContext(), model.getUri());
|
|
||||||
byte[] picture = retriever.getEmbeddedPicture();
|
byte[] picture = retriever.getEmbeddedPicture();
|
||||||
if (picture != null) {
|
if (picture != null) {
|
||||||
callback.onDataReady(new ByteArrayInputStream(picture));
|
callback.onDataReady(new ByteArrayInputStream(picture));
|
||||||
|
@ -40,8 +40,6 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
callback.onLoadFailed(ex);
|
callback.onLoadFailed(ex);
|
||||||
} finally {
|
|
||||||
retriever.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,6 @@ import com.drew.metadata.jpeg.JpegDirectory;
|
||||||
import com.drew.metadata.mp4.Mp4Directory;
|
import com.drew.metadata.mp4.Mp4Directory;
|
||||||
import com.drew.metadata.mp4.media.Mp4VideoDirectory;
|
import com.drew.metadata.mp4.media.Mp4VideoDirectory;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -31,6 +28,7 @@ import java.util.TimeZone;
|
||||||
|
|
||||||
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 static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
||||||
|
|
||||||
|
@ -38,11 +36,21 @@ public class ImageEntry {
|
||||||
public Uri uri; // content or file URI
|
public Uri uri; // content or file URI
|
||||||
public String path; // best effort to get local path
|
public String path; // best effort to get local path
|
||||||
|
|
||||||
public String mimeType, title, bucketDisplayName;
|
public String mimeType;
|
||||||
|
@Nullable
|
||||||
|
public String title;
|
||||||
|
@Nullable
|
||||||
|
private String bucketDisplayName;
|
||||||
@Nullable
|
@Nullable
|
||||||
public Integer width, height, orientationDegrees;
|
public Integer width, height, orientationDegrees;
|
||||||
@Nullable
|
@Nullable
|
||||||
public Long sizeBytes, dateModifiedSecs, sourceDateTakenMillis, durationMillis;
|
public Long sizeBytes;
|
||||||
|
@Nullable
|
||||||
|
public Long dateModifiedSecs;
|
||||||
|
@Nullable
|
||||||
|
private Long sourceDateTakenMillis;
|
||||||
|
@Nullable
|
||||||
|
private Long durationMillis;
|
||||||
|
|
||||||
public ImageEntry() {
|
public ImageEntry() {
|
||||||
}
|
}
|
||||||
|
@ -101,10 +109,6 @@ public class ImageEntry {
|
||||||
return durationMillis != null && durationMillis > 0;
|
return durationMillis != null && durationMillis > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFilename() {
|
|
||||||
return path == null ? null : new File(path).getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isImage() {
|
private boolean isImage() {
|
||||||
return mimeType.startsWith(MimeTypes.IMAGE);
|
return mimeType.startsWith(MimeTypes.IMAGE);
|
||||||
}
|
}
|
||||||
|
@ -119,11 +123,6 @@ public class ImageEntry {
|
||||||
|
|
||||||
// metadata retrieval
|
// metadata retrieval
|
||||||
|
|
||||||
private InputStream getInputStream(Context context) throws FileNotFoundException {
|
|
||||||
// FileInputStream is faster than input stream from ContentResolver
|
|
||||||
return path != null ? new FileInputStream(path) : context.getContentResolver().openInputStream(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
// expects entry with: uri/path, mimeType
|
// expects entry with: uri/path, 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) {
|
||||||
|
@ -138,10 +137,7 @@ public class ImageEntry {
|
||||||
// expects entry with: uri/path, mimeType
|
// expects entry with: uri/path, 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 = new MediaMetadataRetriever();
|
try (MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri, path)) {
|
||||||
try {
|
|
||||||
retriever.setDataSource(context, uri);
|
|
||||||
|
|
||||||
String width = null, height = null, rotation = null, durationMillis = null;
|
String width = null, height = null, rotation = null, durationMillis = null;
|
||||||
if (isImage()) {
|
if (isImage()) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
@ -181,8 +177,6 @@ public class ImageEntry {
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
|
||||||
retriever.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +185,7 @@ public class ImageEntry {
|
||||||
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 = getInputStream(context)) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri, path)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
|
||||||
if (MimeTypes.JPEG.equals(mimeType)) {
|
if (MimeTypes.JPEG.equals(mimeType)) {
|
||||||
|
@ -253,7 +247,7 @@ public class ImageEntry {
|
||||||
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 = getInputStream(context)) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri, path)) {
|
||||||
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);
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
* https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
|
* https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TLAD: copied from https://raw.githubusercontent.com/flutter/plugins/master/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java
|
// TLAD: formatted code copied from:
|
||||||
|
// https://raw.githubusercontent.com/flutter/plugins/master/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java
|
||||||
|
// do not add code to this file!
|
||||||
|
|
||||||
package deckers.thibault.aves.utils;
|
package deckers.thibault.aves.utils;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package deckers.thibault.aves.utils;
|
package deckers.thibault.aves.utils;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.MediaMetadataRetriever;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
@ -16,7 +19,10 @@ import com.google.common.base.Splitter;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -28,6 +34,47 @@ import java.util.Set;
|
||||||
public class StorageUtils {
|
public class StorageUtils {
|
||||||
private static final String LOG_TAG = Utils.createLogTag(StorageUtils.class);
|
private static final String LOG_TAG = Utils.createLogTag(StorageUtils.class);
|
||||||
|
|
||||||
|
private static boolean isMediaStoreContentUri(Uri uri) {
|
||||||
|
// a URI's authority is [userinfo@]host[:port]
|
||||||
|
// but we only want the host when comparing to Media Store's "authority"
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
if (isMediaStoreContentUri(uri)) {
|
||||||
|
uri = MediaStore.setRequireOriginal(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) {
|
||||||
|
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||||
|
|
||||||
|
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
|
||||||
|
if (isMediaStoreContentUri(uri)) {
|
||||||
|
uri = MediaStore.setRequireOriginal(uri);
|
||||||
|
}
|
||||||
|
retriever.setDataSource(context, uri);
|
||||||
|
return retriever;
|
||||||
|
}
|
||||||
|
|
||||||
|
// on Android <Q, we directly work with file paths if possible
|
||||||
|
if (path != null) {
|
||||||
|
retriever.setDataSource(path);
|
||||||
|
} else {
|
||||||
|
retriever.setDataSource(context, uri);
|
||||||
|
}
|
||||||
|
return retriever;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all available SD-Cards in the system (include emulated)
|
* Returns all available SD-Cards in the system (include emulated)
|
||||||
* <p/>
|
* <p/>
|
||||||
|
|
|
@ -68,7 +68,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
// TODO reduce permission check time
|
// TODO reduce permission check time
|
||||||
final permissions = await [
|
final permissions = await [
|
||||||
Permission.storage,
|
Permission.storage,
|
||||||
// unredacted EXIF with scoped storage (Android 10+)
|
// to access media with unredacted metadata with scoped storage (Android 10+)
|
||||||
Permission.accessMediaLocation,
|
Permission.accessMediaLocation,
|
||||||
].request(); // 350ms
|
].request(); // 350ms
|
||||||
if (permissions[Permission.storage] != PermissionStatus.granted) {
|
if (permissions[Permission.storage] != PermissionStatus.granted) {
|
||||||
|
|
Loading…
Reference in a new issue