added file image provider
This commit is contained in:
parent
ce878614cf
commit
a5b47726ed
13 changed files with 348 additions and 299 deletions
|
@ -74,7 +74,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
bitmap = getThumbnailBytesByMediaStore(p);
|
||||
}
|
||||
} else {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + p.entry.getUri() + " cancelled");
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + p.entry.uri + " cancelled");
|
||||
}
|
||||
byte[] data = null;
|
||||
if (bitmap != null) {
|
||||
|
@ -95,16 +95,16 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
try {
|
||||
return resolver.loadThumbnail(entry.getUri(), new Size(width, height), null);
|
||||
return resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "failed to load thumbnail for uri=" + entry.getUri(), e);
|
||||
Log.e(LOG_TAG, "failed to load thumbnail for uri=" + entry.uri, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Bitmap getThumbnailBytesByMediaStore(Params params) {
|
||||
ImageEntry entry = params.entry;
|
||||
long contentId = ContentUris.parseId(entry.getUri());
|
||||
long contentId = ContentUris.parseId(entry.uri);
|
||||
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
try {
|
||||
|
@ -114,7 +114,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
return MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "failed to get thumbnail for uri=" + entry.getUri(), e);
|
||||
Log.e(LOG_TAG, "failed to get thumbnail for uri=" + entry.uri, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
int height = params.height;
|
||||
|
||||
// add signature to ignore cache for images which got modified but kept the same URI
|
||||
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
||||
Key signature = new ObjectKey("" + entry.dateModifiedSecs + entry.width + entry.orientationDegrees);
|
||||
RequestOptions options = new RequestOptions()
|
||||
.signature(signature)
|
||||
.override(width, height);
|
||||
|
@ -136,14 +136,14 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
target = Glide.with(activity)
|
||||
.asBitmap()
|
||||
.apply(options)
|
||||
.load(new VideoThumbnail(activity, entry.getUri()))
|
||||
.load(new VideoThumbnail(activity, entry.uri))
|
||||
.signature(signature)
|
||||
.submit(width, height);
|
||||
} else {
|
||||
target = Glide.with(activity)
|
||||
.asBitmap()
|
||||
.apply(options)
|
||||
.load(entry.getUri())
|
||||
.load(entry.uri)
|
||||
.signature(signature)
|
||||
.submit(width, height);
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
try {
|
||||
return target.get();
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.uri + " interrupted");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
MethodChannel.Result r = result.params.result;
|
||||
String uri = result.params.entry.getUri().toString();
|
||||
String uri = result.params.entry.uri.toString();
|
||||
result.params.complete.accept(uri);
|
||||
if (result.data != null) {
|
||||
r.success(result.data);
|
||||
|
|
|
@ -35,7 +35,7 @@ public class ImageDecodeTaskManager {
|
|||
}
|
||||
|
||||
void cancel(String uri) {
|
||||
boolean removed = taskParamsQueue.removeIf(p -> uri.equals(p.entry.getUri().toString()));
|
||||
boolean removed = taskParamsQueue.removeIf(p -> uri.equals(p.entry.uri.toString()));
|
||||
if (removed) Log.d(LOG_TAG, "cancelled uri=" + uri);
|
||||
}
|
||||
|
||||
|
|
|
@ -80,8 +80,10 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
|
||||
String path = call.argument("path");
|
||||
String uri = call.argument("uri");
|
||||
try (InputStream is = getInputStream(path, uri)) {
|
||||
|
||||
Map<String, Map<String, String>> metadataMap = new HashMap<>();
|
||||
|
||||
try (InputStream is = getInputStream(path, uri)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
for (Directory dir : metadata.getDirectories()) {
|
||||
if (dir.getTagCount() > 0) {
|
||||
|
@ -165,8 +167,10 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
String mimeType = call.argument("mimeType");
|
||||
String path = call.argument("path");
|
||||
String uri = call.argument("uri");
|
||||
try (InputStream is = getInputStream(path, uri)) {
|
||||
|
||||
Map<String, Object> metadataMap = new HashMap<>();
|
||||
|
||||
try (InputStream is = getInputStream(path, uri)) {
|
||||
if (!Constants.MIME_MP2T.equalsIgnoreCase(mimeType)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
|
||||
|
@ -208,7 +212,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
if (isVideo(call.argument("mimeType"))) {
|
||||
if (isVideo(mimeType)) {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
if (path != null) {
|
||||
|
@ -266,15 +270,17 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
|
||||
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
|
||||
String mimeType = call.argument("mimeType");
|
||||
String path = call.argument("path");
|
||||
String uri = call.argument("uri");
|
||||
|
||||
Map<String, String> metadataMap = new HashMap<>();
|
||||
|
||||
if (isVideo(call.argument("mimeType"))) {
|
||||
if (isVideo(mimeType)) {
|
||||
result.success(metadataMap);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = call.argument("path");
|
||||
String uri = call.argument("uri");
|
||||
try (InputStream is = getInputStream(path, uri)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||
|
|
|
@ -1,27 +1,49 @@
|
|||
package deckers.thibault.aves.model;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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 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.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
|
||||
import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
||||
|
||||
public class ImageEntry {
|
||||
// from source
|
||||
private String path; // best effort to get local path from content providers
|
||||
private Uri uri; // content URI
|
||||
private String mimeType;
|
||||
private int width, height, orientationDegrees;
|
||||
private long sizeBytes;
|
||||
private String title, bucketDisplayName;
|
||||
private long dateModifiedSecs, sourceDateTakenMillis;
|
||||
private long durationMillis;
|
||||
public Uri uri; // content or file URI
|
||||
public String path; // best effort to get local path
|
||||
|
||||
// uri: content provider uri
|
||||
// path: FileUtils.getPathFromUri(activity, itemUri) is useful (for Download, File, etc.) but is slower than directly using `MediaStore.MediaColumns.DATA` from the MediaStore query
|
||||
public String mimeType, title, bucketDisplayName;
|
||||
@Nullable
|
||||
public Integer width, height, orientationDegrees;
|
||||
@Nullable
|
||||
public Long sizeBytes, dateModifiedSecs, sourceDateTakenMillis, durationMillis;
|
||||
|
||||
public ImageEntry() {
|
||||
}
|
||||
|
||||
public ImageEntry(Map map) {
|
||||
this.uri = Uri.parse((String) map.get("uri"));
|
||||
|
@ -38,49 +60,175 @@ public class ImageEntry {
|
|||
this.durationMillis = toLong(map.get("durationMillis"));
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
public Map<String, Object> toMap() {
|
||||
return new HashMap<String, Object>() {{
|
||||
put("uri", uri.toString());
|
||||
put("path", path);
|
||||
put("mimeType", mimeType);
|
||||
put("width", width);
|
||||
put("height", height);
|
||||
put("orientationDegrees", orientationDegrees != null ? orientationDegrees : 0);
|
||||
put("sizeBytes", sizeBytes);
|
||||
put("title", title);
|
||||
put("dateModifiedSecs", dateModifiedSecs);
|
||||
put("sourceDateTakenMillis", sourceDateTakenMillis);
|
||||
put("bucketDisplayName", bucketDisplayName);
|
||||
put("durationMillis", durationMillis);
|
||||
// only for map export
|
||||
put("contentId", getContentId());
|
||||
}};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPath() {
|
||||
return path;
|
||||
private Long getContentId() {
|
||||
if (uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
|
||||
try {
|
||||
return ContentUris.parseId(uri);
|
||||
} catch (NumberFormatException | UnsupportedOperationException e) {
|
||||
// ignore when the ID is not a number
|
||||
// e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasSize() {
|
||||
return width != null && width > 0 && height != null && height > 0;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return path == null ? null : new File(path).getName();
|
||||
}
|
||||
|
||||
public boolean isImage() {
|
||||
return mimeType.startsWith(Constants.MIME_IMAGE);
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return mimeType.startsWith(Constants.MIME_VIDEO);
|
||||
}
|
||||
|
||||
public boolean isEditable() {
|
||||
return path != null;
|
||||
// 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);
|
||||
}
|
||||
|
||||
public boolean isGif() {
|
||||
return Constants.MIME_GIF.equals(mimeType);
|
||||
// expects entry with: uri/path, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
public ImageEntry fillPreCatalogMetadata(Context context) {
|
||||
fillByMediaMetadataRetriever(context);
|
||||
if (hasSize()) return this;
|
||||
fillByMetadataExtractor(context);
|
||||
if (hasSize()) return this;
|
||||
fillByBitmapDecode(context);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
// expects entry with: uri/path, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
private void fillByMediaMetadataRetriever(Context context) {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
retriever.setDataSource(context, uri);
|
||||
|
||||
String width = null, height = null, rotation = null, durationMillis = null;
|
||||
if (isImage()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
|
||||
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
|
||||
rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
|
||||
}
|
||||
} else if (isVideo()) {
|
||||
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
|
||||
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
|
||||
rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
||||
durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
||||
}
|
||||
if (width != null) {
|
||||
this.width = Integer.parseInt(width);
|
||||
}
|
||||
if (height != null) {
|
||||
this.height = Integer.parseInt(height);
|
||||
}
|
||||
if (rotation != null) {
|
||||
this.orientationDegrees = Integer.parseInt(rotation);
|
||||
}
|
||||
if (durationMillis != null) {
|
||||
this.durationMillis = Long.parseLong(durationMillis);
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
||||
long dateMillis = MetadataHelper.parseVideoMetadataDate(dateString);
|
||||
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
||||
if (dateMillis > 0) {
|
||||
this.sourceDateTakenMillis = dateMillis;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
||||
if (title != null) {
|
||||
this.title = title;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
} finally {
|
||||
retriever.release();
|
||||
}
|
||||
}
|
||||
|
||||
public int getOrientationDegrees() {
|
||||
return orientationDegrees;
|
||||
// expects entry with: uri/path, mimeType
|
||||
// finds: width, height, orientation, date
|
||||
private void fillByMetadataExtractor(Context context) {
|
||||
try (InputStream is = getInputStream(context)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
|
||||
if (Constants.MIME_JPEG.equals(mimeType)) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
} else if (Constants.MIME_MP4.equals(mimeType)) {
|
||||
Mp4VideoDirectory mp4VideoDir = metadata.getFirstDirectoryOfType(Mp4VideoDirectory.class);
|
||||
if (mp4VideoDir != null) {
|
||||
if (mp4VideoDir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
||||
width = mp4VideoDir.getInt(Mp4VideoDirectory.TAG_WIDTH);
|
||||
}
|
||||
if (mp4VideoDir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
||||
height = mp4VideoDir.getInt(Mp4VideoDirectory.TAG_HEIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | ImageProcessingException | MetadataException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public long getDateModifiedSecs() {
|
||||
return dateModifiedSecs;
|
||||
// expects entry with: uri/path
|
||||
// finds: width, height
|
||||
private void fillByBitmapDecode(Context context) {
|
||||
try (InputStream is = getInputStream(context)) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(is, null, options);
|
||||
width = options.outWidth;
|
||||
height = options.outHeight;
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// convenience method
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package deckers.thibault.aves.model.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
|
||||
class ContentImageProvider extends ImageProvider {
|
||||
@Override
|
||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||
ImageEntry entry = new ImageEntry();
|
||||
entry.uri = uri;
|
||||
entry.mimeType = mimeType;
|
||||
entry.fillPreCatalogMetadata(context);
|
||||
|
||||
if (entry.hasSize()) {
|
||||
callback.onSuccess(entry.toMap());
|
||||
} else {
|
||||
callback.onFailure();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package deckers.thibault.aves.model.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.utils.FileUtils;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
|
||||
class FileImageProvider extends ImageProvider {
|
||||
private static final String LOG_TAG = Utils.createLogTag(FileImageProvider.class);
|
||||
|
||||
@Override
|
||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||
ImageEntry entry = new ImageEntry();
|
||||
entry.uri = uri;
|
||||
entry.mimeType = mimeType;
|
||||
|
||||
String path = FileUtils.getPathFromUri(context, uri);
|
||||
if (path != null) {
|
||||
try {
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
entry.path = path;
|
||||
entry.title = file.getName();
|
||||
entry.sizeBytes = file.length();
|
||||
entry.dateModifiedSecs = file.lastModified() / 1000;
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
Log.w(LOG_TAG, "failed to get path from file at uri=" + uri);
|
||||
callback.onFailure();
|
||||
}
|
||||
}
|
||||
entry.fillPreCatalogMetadata(context);
|
||||
|
||||
if (entry.hasSize()) {
|
||||
callback.onSuccess(entry.toMap());
|
||||
} else {
|
||||
callback.onFailure();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,9 @@ import android.os.ParcelFileDescriptor;
|
|||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.drew.imaging.ImageMetadataReader;
|
||||
import com.drew.imaging.ImageProcessingException;
|
||||
import com.drew.metadata.Metadata;
|
||||
|
@ -40,7 +43,7 @@ import deckers.thibault.aves.utils.Utils;
|
|||
public abstract class ImageProvider {
|
||||
private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class);
|
||||
|
||||
public void fetchSingle(final Activity activity, final Uri uri, final String mimeType, final ImageOpCallback callback) {
|
||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||
callback.onFailure();
|
||||
}
|
||||
|
||||
|
@ -120,7 +123,8 @@ public abstract class ImageProvider {
|
|||
// `context.getContentResolver().getType()` sometimes return incorrect value
|
||||
// `MediaMetadataRetriever.setDataSource()` sometimes fail with `status = 0x80000000`
|
||||
// so we check with `metadata-extractor`
|
||||
private String getMimeType(final Context context, final Uri uri) {
|
||||
@Nullable
|
||||
private String getMimeType(@NonNull final Context context, @NonNull final Uri uri) {
|
||||
try (InputStream is = context.getContentResolver().openInputStream(uri)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
FileTypeDirectory fileTypeDir = metadata.getFirstDirectoryOfType(FileTypeDirectory.class);
|
||||
|
|
|
@ -9,27 +9,20 @@ import androidx.annotation.NonNull;
|
|||
public class ImageProviderFactory {
|
||||
public static ImageProvider getProvider(@NonNull Uri uri) {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme != null) {
|
||||
switch (scheme) {
|
||||
case ContentResolver.SCHEME_CONTENT: // content://
|
||||
|
||||
if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme)) {
|
||||
// a URI's authority is [userinfo@]host[:port]
|
||||
// but we only want the host when comparing to Media Store's "authority"
|
||||
String host = uri.getHost();
|
||||
if (host != null) {
|
||||
switch (host) {
|
||||
case MediaStore.AUTHORITY:
|
||||
if (MediaStore.AUTHORITY.equalsIgnoreCase(uri.getHost())) {
|
||||
return new MediaStoreImageProvider();
|
||||
// case Constants.DOWNLOADS_AUTHORITY:
|
||||
// return new DownloadImageProvider();
|
||||
default:
|
||||
return new UnknownContentImageProvider();
|
||||
}
|
||||
return new ContentImageProvider();
|
||||
}
|
||||
return null;
|
||||
// case ContentResolver.SCHEME_FILE: // file://
|
||||
// return new FileImageProvider();
|
||||
}
|
||||
|
||||
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme)) {
|
||||
return new FileImageProvider();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,26 +3,20 @@ package deckers.thibault.aves.model.provider;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
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 com.drew.metadata.mp4.media.Mp4VideoDirectory;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.Env;
|
||||
import deckers.thibault.aves.utils.PermissionManager;
|
||||
|
@ -30,12 +24,9 @@ import deckers.thibault.aves.utils.StorageUtils;
|
|||
import deckers.thibault.aves.utils.Utils;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
|
||||
import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
||||
|
||||
public class MediaStoreImageProvider extends ImageProvider {
|
||||
private static final String LOG_TAG = Utils.createLogTag(MediaStoreImageProvider.class);
|
||||
|
||||
|
||||
private static final String[] BASE_PROJECTION = {
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaStore.MediaColumns.DATA,
|
||||
|
@ -73,7 +64,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void fetchSingle(final Activity activity, final Uri uri, final String mimeType, final ImageOpCallback callback) {
|
||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||
long id = ContentUris.parseId(uri);
|
||||
int entryCount = 0;
|
||||
NewEntryHandler onSuccess = (entry) -> {
|
||||
|
@ -82,10 +73,10 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
};
|
||||
if (mimeType.startsWith(Constants.MIME_IMAGE)) {
|
||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
||||
entryCount = fetchFrom(activity, onSuccess, contentUri, IMAGE_PROJECTION);
|
||||
entryCount = fetchFrom(context, onSuccess, contentUri, IMAGE_PROJECTION);
|
||||
} else if (mimeType.startsWith(Constants.MIME_VIDEO)) {
|
||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
||||
entryCount = fetchFrom(activity, onSuccess, contentUri, VIDEO_PROJECTION);
|
||||
entryCount = fetchFrom(context, onSuccess, contentUri, VIDEO_PROJECTION);
|
||||
}
|
||||
if (entryCount == 0) {
|
||||
callback.onFailure();
|
||||
|
@ -93,12 +84,12 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private int fetchFrom(final Activity activity, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) {
|
||||
private int fetchFrom(final Context context, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) {
|
||||
String orderBy = MediaStore.MediaColumns.DATE_TAKEN + " DESC";
|
||||
int entryCount = 0;
|
||||
|
||||
try {
|
||||
Cursor cursor = activity.getContentResolver().query(contentUri, projection, null, null, orderBy);
|
||||
Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, orderBy);
|
||||
if (cursor != null) {
|
||||
// image & video
|
||||
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
|
||||
|
@ -109,7 +100,6 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
int widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH);
|
||||
int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT);
|
||||
int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED);
|
||||
|
||||
int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN);
|
||||
int bucketDisplayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
|
||||
|
||||
|
@ -120,73 +110,43 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
int durationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
long contentId = cursor.getLong(idColumn);
|
||||
final long contentId = cursor.getLong(idColumn);
|
||||
// this is fine if `contentUri` does not already contain the ID
|
||||
Uri itemUri = ContentUris.withAppendedId(contentUri, contentId);
|
||||
String path = cursor.getString(pathColumn);
|
||||
final Uri itemUri = ContentUris.withAppendedId(contentUri, contentId);
|
||||
final String path = cursor.getString(pathColumn);
|
||||
int width = cursor.getInt(widthColumn);
|
||||
int height = cursor.getInt(heightColumn);
|
||||
int orientationDegrees = orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0;
|
||||
if (width <= 0 || height <= 0) {
|
||||
// some images are incorrectly registered in the Media Store,
|
||||
// they are valid but miss some attributes, such as width, height, orientation
|
||||
try (InputStream is = activity.getContentResolver().openInputStream(itemUri)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
|
||||
// JPEG
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// EXIF
|
||||
|
||||
ExifIFD0Directory exifDir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
|
||||
if (exifDir != null) {
|
||||
if (exifDir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||
orientationDegrees = getOrientationDegreesForExifCode(exifDir.getInt(ExifIFD0Directory.TAG_ORIENTATION));
|
||||
}
|
||||
}
|
||||
|
||||
// MP4
|
||||
|
||||
Mp4VideoDirectory mp4VideoDir = metadata.getFirstDirectoryOfType(Mp4VideoDirectory.class);
|
||||
if (mp4VideoDir != null) {
|
||||
if (mp4VideoDir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
||||
width = mp4VideoDir.getInt(Mp4VideoDirectory.TAG_WIDTH);
|
||||
}
|
||||
if (mp4VideoDir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
||||
height = mp4VideoDir.getInt(Mp4VideoDirectory.TAG_HEIGHT);
|
||||
}
|
||||
}
|
||||
} catch (IOException | ImageProcessingException | MetadataException e) {
|
||||
// this is probably not a real image, like "/storage/emulated/0", so we skip it
|
||||
}
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path);
|
||||
} else {
|
||||
Map<String, Object> entryMap = new HashMap<String, Object>() {{
|
||||
put("uri", itemUri.toString());
|
||||
put("path", path);
|
||||
put("contentId", contentId);
|
||||
put("mimeType", cursor.getString(mimeTypeColumn));
|
||||
put("orientationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0);
|
||||
put("sizeBytes", cursor.getLong(sizeColumn));
|
||||
put("title", cursor.getString(titleColumn));
|
||||
put("dateModifiedSecs", cursor.getLong(dateModifiedColumn));
|
||||
put("sourceDateTakenMillis", cursor.getLong(dateTakenColumn));
|
||||
put("bucketDisplayName", cursor.getString(bucketDisplayNameColumn));
|
||||
put("durationMillis", durationColumn != -1 ? cursor.getLong(durationColumn) : 0);
|
||||
// only for map export
|
||||
put("contentId", contentId);
|
||||
}};
|
||||
entryMap.put("width", width);
|
||||
entryMap.put("height", height);
|
||||
entryMap.put("orientationDegrees", orientationDegrees);
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
// some images are incorrectly registered in the Media Store,
|
||||
// they are valid but miss some attributes, such as width, height, orientation
|
||||
ImageEntry entry = new ImageEntry(entryMap).fillPreCatalogMetadata(context);
|
||||
entryMap = entry.toMap();
|
||||
width = entry.width != null ? entry.width : 0;
|
||||
height = entry.height != null ? entry.height : 0;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
// this is probably not a real image, like "/storage/emulated/0", so we skip it
|
||||
Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path);
|
||||
} else {
|
||||
newEntryHandler.handleEntry(entryMap);
|
||||
entryCount++;
|
||||
}
|
||||
|
@ -213,7 +173,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
// if the file is on SD card, calling the content resolver delete() removes the entry from the MediaStore
|
||||
// if the file is on SD card, calling the content resolver delete() removes the entry from the Media Store
|
||||
// but it doesn't delete the file, even if the app has the permission
|
||||
StorageUtils.deleteFromSdCard(activity, sdCardTreeUri, Env.getStorageVolumes(activity), path);
|
||||
Log.d(LOG_TAG, "deleted from SD card at path=" + uri);
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
package deckers.thibault.aves.model.provider;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
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 deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
|
||||
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;
|
||||
Integer orientationDegrees = null;
|
||||
Long sourceDateTakenMillis = null, durationMillis = null;
|
||||
String title = null;
|
||||
|
||||
// check first metadata with MediaMetadataRetriever
|
||||
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
retriever.setDataSource(activity, uri);
|
||||
|
||||
title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
||||
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
||||
long dateMillis = MetadataHelper.parseVideoMetadataDate(dateString);
|
||||
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
||||
if (dateMillis > 0) {
|
||||
sourceDateTakenMillis = dateMillis;
|
||||
}
|
||||
|
||||
String widthString = null, heightString = null, rotationString = null, durationMillisString = null;
|
||||
if (mimeType.startsWith(Constants.MIME_IMAGE)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
widthString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
|
||||
heightString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
|
||||
rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
|
||||
}
|
||||
} else if (mimeType.startsWith(Constants.MIME_VIDEO)) {
|
||||
widthString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
|
||||
heightString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
|
||||
rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
||||
durationMillisString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
||||
}
|
||||
if (widthString != null) {
|
||||
width = Integer.parseInt(widthString);
|
||||
}
|
||||
if (heightString != null) {
|
||||
height = Integer.parseInt(heightString);
|
||||
}
|
||||
if (rotationString != null) {
|
||||
orientationDegrees = Integer.parseInt(rotationString);
|
||||
}
|
||||
if (durationMillisString != null) {
|
||||
durationMillis = Long.parseLong(durationMillisString);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
} finally {
|
||||
retriever.release();
|
||||
}
|
||||
|
||||
// fallback to metadata-extractor for known types
|
||||
if (width <= 0 || height <= 0 || orientationDegrees == null || sourceDateTakenMillis == null) {
|
||||
if (Constants.MIME_JPEG.equals(mimeType)) {
|
||||
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<String, Object> 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 != null ? orientationDegrees : 0);
|
||||
entry.put("sizeBytes", null);
|
||||
entry.put("title", title);
|
||||
entry.put("dateModifiedSecs", null);
|
||||
entry.put("sourceDateTakenMillis", sourceDateTakenMillis);
|
||||
entry.put("bucketDisplayName", null);
|
||||
entry.put("durationMillis", durationMillis);
|
||||
callback.onSuccess(entry);
|
||||
}
|
||||
}
|
|
@ -10,12 +10,14 @@ public class Constants {
|
|||
|
||||
// mime types
|
||||
|
||||
public static final String MIME_IMAGE = "image";
|
||||
public static final String MIME_GIF = "image/gif";
|
||||
public static final String MIME_JPEG = "image/jpeg";
|
||||
public static final String MIME_PNG = "image/png";
|
||||
public static final String MIME_MP2T = "video/mp2t"; // .m2ts
|
||||
public static final String MIME_IMAGE = "image";
|
||||
|
||||
public static final String MIME_VIDEO = "video";
|
||||
public static final String MIME_MP2T = "video/mp2t"; // .m2ts
|
||||
public static final String MIME_MP4 = "video/mp4";
|
||||
|
||||
// video metadata keys, from android.media.MediaMetadataRetriever
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
package deckers.thibault.aves.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
@ -40,8 +41,9 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public String getPathFromUri(final Context context, final Uri uri) {
|
||||
// useful (for Download, File, etc.) but slower
|
||||
// than directly using `MediaStore.MediaColumns.DATA` from the MediaStore query
|
||||
public static String getPathFromUri(final Context context, final Uri uri) {
|
||||
String path = getPathFromLocalUri(context, uri);
|
||||
if (path == null) {
|
||||
path = getPathFromRemoteUri(context, uri);
|
||||
|
@ -50,7 +52,7 @@ public class FileUtils {
|
|||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private String getPathFromLocalUri(final Context context, final Uri uri) {
|
||||
private static String getPathFromLocalUri(final Context context, final Uri uri) {
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
|
@ -95,7 +97,7 @@ public class FileUtils {
|
|||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
} else if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
|
||||
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri)) {
|
||||
|
@ -103,7 +105,7 @@ public class FileUtils {
|
|||
}
|
||||
|
||||
return getDataColumn(context, uri, null, null);
|
||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
} else if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ package deckers.thibault.aves.utils;
|
|||
|
||||
import android.media.ExifInterface;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
@ -28,7 +30,11 @@ public class MetadataHelper {
|
|||
}
|
||||
|
||||
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
|
||||
public static long parseVideoMetadataDate(String dateString) {
|
||||
public static long parseVideoMetadataDate(@Nullable String dateString) {
|
||||
if (dateString == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// optional sub-second
|
||||
String subSecond = null;
|
||||
Matcher subSecondMatcher = Pattern.compile("(\\d{6})(\\.\\d+)").matcher(dateString);
|
||||
|
|
Loading…
Reference in a new issue