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);
|
bitmap = getThumbnailBytesByMediaStore(p);
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
byte[] data = null;
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
|
@ -95,16 +95,16 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
|
|
||||||
ContentResolver resolver = activity.getContentResolver();
|
ContentResolver resolver = activity.getContentResolver();
|
||||||
try {
|
try {
|
||||||
return resolver.loadThumbnail(entry.getUri(), new Size(width, height), null);
|
return resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||||
} catch (IOException e) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap getThumbnailBytesByMediaStore(Params params) {
|
private Bitmap getThumbnailBytesByMediaStore(Params params) {
|
||||||
ImageEntry entry = params.entry;
|
ImageEntry entry = params.entry;
|
||||||
long contentId = ContentUris.parseId(entry.getUri());
|
long contentId = ContentUris.parseId(entry.uri);
|
||||||
|
|
||||||
ContentResolver resolver = activity.getContentResolver();
|
ContentResolver resolver = activity.getContentResolver();
|
||||||
try {
|
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);
|
return MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
int height = params.height;
|
int height = params.height;
|
||||||
|
|
||||||
// add signature to ignore cache for images which got modified but kept the same URI
|
// 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()
|
RequestOptions options = new RequestOptions()
|
||||||
.signature(signature)
|
.signature(signature)
|
||||||
.override(width, height);
|
.override(width, height);
|
||||||
|
@ -136,14 +136,14 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
target = Glide.with(activity)
|
target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(new VideoThumbnail(activity, entry.getUri()))
|
.load(new VideoThumbnail(activity, entry.uri))
|
||||||
.signature(signature)
|
.signature(signature)
|
||||||
.submit(width, height);
|
.submit(width, height);
|
||||||
} else {
|
} else {
|
||||||
target = Glide.with(activity)
|
target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(entry.getUri())
|
.load(entry.uri)
|
||||||
.signature(signature)
|
.signature(signature)
|
||||||
.submit(width, height);
|
.submit(width, height);
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
try {
|
try {
|
||||||
return target.get();
|
return target.get();
|
||||||
} catch (InterruptedException e) {
|
} 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) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -162,7 +162,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Result result) {
|
protected void onPostExecute(Result result) {
|
||||||
MethodChannel.Result r = result.params.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);
|
result.params.complete.accept(uri);
|
||||||
if (result.data != null) {
|
if (result.data != null) {
|
||||||
r.success(result.data);
|
r.success(result.data);
|
||||||
|
|
|
@ -35,7 +35,7 @@ public class ImageDecodeTaskManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancel(String uri) {
|
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);
|
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) {
|
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<>();
|
||||||
|
|
||||||
try (InputStream is = getInputStream(path, uri)) {
|
try (InputStream is = getInputStream(path, uri)) {
|
||||||
Map<String, Map<String, String>> metadataMap = new HashMap<>();
|
|
||||||
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) {
|
||||||
|
@ -165,8 +167,10 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
String mimeType = call.argument("mimeType");
|
String mimeType = call.argument("mimeType");
|
||||||
String path = call.argument("path");
|
String path = call.argument("path");
|
||||||
String uri = call.argument("uri");
|
String uri = call.argument("uri");
|
||||||
|
|
||||||
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
try (InputStream is = getInputStream(path, uri)) {
|
try (InputStream is = getInputStream(path, uri)) {
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
|
||||||
if (!Constants.MIME_MP2T.equalsIgnoreCase(mimeType)) {
|
if (!Constants.MIME_MP2T.equalsIgnoreCase(mimeType)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
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();
|
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||||
try {
|
try {
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
|
@ -266,15 +270,17 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
|
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<>();
|
Map<String, String> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
if (isVideo(call.argument("mimeType"))) {
|
if (isVideo(mimeType)) {
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = call.argument("path");
|
|
||||||
String uri = call.argument("uri");
|
|
||||||
try (InputStream is = getInputStream(path, uri)) {
|
try (InputStream is = getInputStream(path, uri)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||||
|
|
|
@ -1,27 +1,49 @@
|
||||||
package deckers.thibault.aves.model;
|
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.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
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.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.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import deckers.thibault.aves.utils.Constants;
|
import deckers.thibault.aves.utils.Constants;
|
||||||
|
import deckers.thibault.aves.utils.MetadataHelper;
|
||||||
|
|
||||||
|
import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
||||||
|
|
||||||
public class ImageEntry {
|
public class ImageEntry {
|
||||||
// from source
|
public Uri uri; // content or file URI
|
||||||
private String path; // best effort to get local path from content providers
|
public String path; // best effort to get local path
|
||||||
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;
|
|
||||||
|
|
||||||
// uri: content provider uri
|
public String mimeType, title, bucketDisplayName;
|
||||||
// path: FileUtils.getPathFromUri(activity, itemUri) is useful (for Download, File, etc.) but is slower than directly using `MediaStore.MediaColumns.DATA` from the MediaStore query
|
@Nullable
|
||||||
|
public Integer width, height, orientationDegrees;
|
||||||
|
@Nullable
|
||||||
|
public Long sizeBytes, dateModifiedSecs, sourceDateTakenMillis, durationMillis;
|
||||||
|
|
||||||
|
public ImageEntry() {
|
||||||
|
}
|
||||||
|
|
||||||
public ImageEntry(Map map) {
|
public ImageEntry(Map map) {
|
||||||
this.uri = Uri.parse((String) map.get("uri"));
|
this.uri = Uri.parse((String) map.get("uri"));
|
||||||
|
@ -38,49 +60,175 @@ public class ImageEntry {
|
||||||
this.durationMillis = toLong(map.get("durationMillis"));
|
this.durationMillis = toLong(map.get("durationMillis"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri getUri() {
|
public Map<String, Object> toMap() {
|
||||||
return uri;
|
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
|
private Long getContentId() {
|
||||||
public String getPath() {
|
if (uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
|
||||||
return path;
|
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() {
|
public String getFilename() {
|
||||||
return path == null ? null : new File(path).getName();
|
return path == null ? null : new File(path).getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isImage() {
|
||||||
|
return mimeType.startsWith(Constants.MIME_IMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isVideo() {
|
public boolean isVideo() {
|
||||||
return mimeType.startsWith(Constants.MIME_VIDEO);
|
return mimeType.startsWith(Constants.MIME_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEditable() {
|
// metadata retrieval
|
||||||
return path != null;
|
|
||||||
|
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() {
|
// expects entry with: uri/path, mimeType
|
||||||
return Constants.MIME_GIF.equals(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() {
|
// expects entry with: uri/path, mimeType
|
||||||
return 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
||||||
|
if (title != null) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
retriever.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getWidth() {
|
// expects entry with: uri/path, mimeType
|
||||||
return width;
|
// 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 int getHeight() {
|
// expects entry with: uri/path
|
||||||
return height;
|
// finds: width, height
|
||||||
}
|
private void fillByBitmapDecode(Context context) {
|
||||||
|
try (InputStream is = getInputStream(context)) {
|
||||||
public int getOrientationDegrees() {
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
return orientationDegrees;
|
options.inJustDecodeBounds = true;
|
||||||
}
|
BitmapFactory.decodeStream(is, null, options);
|
||||||
|
width = options.outWidth;
|
||||||
public long getDateModifiedSecs() {
|
height = options.outHeight;
|
||||||
return dateModifiedSecs;
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convenience method
|
// 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.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.drew.imaging.ImageMetadataReader;
|
import com.drew.imaging.ImageMetadataReader;
|
||||||
import com.drew.imaging.ImageProcessingException;
|
import com.drew.imaging.ImageProcessingException;
|
||||||
import com.drew.metadata.Metadata;
|
import com.drew.metadata.Metadata;
|
||||||
|
@ -40,7 +43,7 @@ import deckers.thibault.aves.utils.Utils;
|
||||||
public abstract class ImageProvider {
|
public abstract class ImageProvider {
|
||||||
private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class);
|
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();
|
callback.onFailure();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +123,8 @@ public abstract class ImageProvider {
|
||||||
// `context.getContentResolver().getType()` sometimes return incorrect value
|
// `context.getContentResolver().getType()` sometimes return incorrect value
|
||||||
// `MediaMetadataRetriever.setDataSource()` sometimes fail with `status = 0x80000000`
|
// `MediaMetadataRetriever.setDataSource()` sometimes fail with `status = 0x80000000`
|
||||||
// so we check with `metadata-extractor`
|
// 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)) {
|
try (InputStream is = context.getContentResolver().openInputStream(uri)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
FileTypeDirectory fileTypeDir = metadata.getFirstDirectoryOfType(FileTypeDirectory.class);
|
FileTypeDirectory fileTypeDir = metadata.getFirstDirectoryOfType(FileTypeDirectory.class);
|
||||||
|
|
|
@ -9,27 +9,20 @@ import androidx.annotation.NonNull;
|
||||||
public class ImageProviderFactory {
|
public class ImageProviderFactory {
|
||||||
public static ImageProvider getProvider(@NonNull Uri uri) {
|
public static ImageProvider getProvider(@NonNull Uri uri) {
|
||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
if (scheme != null) {
|
|
||||||
switch (scheme) {
|
if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme)) {
|
||||||
case ContentResolver.SCHEME_CONTENT: // content://
|
// a URI's authority is [userinfo@]host[:port]
|
||||||
// a URI's authority is [userinfo@]host[:port]
|
// but we only want the host when comparing to Media Store's "authority"
|
||||||
// but we only want the host when comparing to Media Store's "authority"
|
if (MediaStore.AUTHORITY.equalsIgnoreCase(uri.getHost())) {
|
||||||
String host = uri.getHost();
|
return new MediaStoreImageProvider();
|
||||||
if (host != null) {
|
|
||||||
switch (host) {
|
|
||||||
case MediaStore.AUTHORITY:
|
|
||||||
return new MediaStoreImageProvider();
|
|
||||||
// case Constants.DOWNLOADS_AUTHORITY:
|
|
||||||
// return new DownloadImageProvider();
|
|
||||||
default:
|
|
||||||
return new UnknownContentImageProvider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
// case ContentResolver.SCHEME_FILE: // file://
|
|
||||||
// return new FileImageProvider();
|
|
||||||
}
|
}
|
||||||
|
return new ContentImageProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme)) {
|
||||||
|
return new FileImageProvider();
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,26 +3,20 @@ package deckers.thibault.aves.model.provider;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.drew.imaging.ImageMetadataReader;
|
import androidx.annotation.NonNull;
|
||||||
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.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
import deckers.thibault.aves.utils.Constants;
|
import deckers.thibault.aves.utils.Constants;
|
||||||
import deckers.thibault.aves.utils.Env;
|
import deckers.thibault.aves.utils.Env;
|
||||||
import deckers.thibault.aves.utils.PermissionManager;
|
import deckers.thibault.aves.utils.PermissionManager;
|
||||||
|
@ -30,12 +24,9 @@ import deckers.thibault.aves.utils.StorageUtils;
|
||||||
import deckers.thibault.aves.utils.Utils;
|
import deckers.thibault.aves.utils.Utils;
|
||||||
import io.flutter.plugin.common.EventChannel;
|
import io.flutter.plugin.common.EventChannel;
|
||||||
|
|
||||||
import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
|
||||||
|
|
||||||
public class MediaStoreImageProvider extends ImageProvider {
|
public class MediaStoreImageProvider extends ImageProvider {
|
||||||
private static final String LOG_TAG = Utils.createLogTag(MediaStoreImageProvider.class);
|
private static final String LOG_TAG = Utils.createLogTag(MediaStoreImageProvider.class);
|
||||||
|
|
||||||
|
|
||||||
private static final String[] BASE_PROJECTION = {
|
private static final String[] BASE_PROJECTION = {
|
||||||
MediaStore.MediaColumns._ID,
|
MediaStore.MediaColumns._ID,
|
||||||
MediaStore.MediaColumns.DATA,
|
MediaStore.MediaColumns.DATA,
|
||||||
|
@ -73,7 +64,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
long id = ContentUris.parseId(uri);
|
||||||
int entryCount = 0;
|
int entryCount = 0;
|
||||||
NewEntryHandler onSuccess = (entry) -> {
|
NewEntryHandler onSuccess = (entry) -> {
|
||||||
|
@ -82,10 +73,10 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
};
|
};
|
||||||
if (mimeType.startsWith(Constants.MIME_IMAGE)) {
|
if (mimeType.startsWith(Constants.MIME_IMAGE)) {
|
||||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
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)) {
|
} else if (mimeType.startsWith(Constants.MIME_VIDEO)) {
|
||||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
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) {
|
if (entryCount == 0) {
|
||||||
callback.onFailure();
|
callback.onFailure();
|
||||||
|
@ -93,12 +84,12 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@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";
|
String orderBy = MediaStore.MediaColumns.DATE_TAKEN + " DESC";
|
||||||
int entryCount = 0;
|
int entryCount = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Cursor cursor = activity.getContentResolver().query(contentUri, projection, null, null, orderBy);
|
Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, orderBy);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
// image & video
|
// image & video
|
||||||
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
|
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
|
||||||
|
@ -109,7 +100,6 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
int widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH);
|
int widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH);
|
||||||
int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT);
|
int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT);
|
||||||
int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED);
|
int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED);
|
||||||
|
|
||||||
int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN);
|
int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN);
|
||||||
int bucketDisplayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
|
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);
|
int durationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION);
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
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
|
// this is fine if `contentUri` does not already contain the ID
|
||||||
Uri itemUri = ContentUris.withAppendedId(contentUri, contentId);
|
final Uri itemUri = ContentUris.withAppendedId(contentUri, contentId);
|
||||||
String path = cursor.getString(pathColumn);
|
final String path = cursor.getString(pathColumn);
|
||||||
int width = cursor.getInt(widthColumn);
|
int width = cursor.getInt(widthColumn);
|
||||||
int height = cursor.getInt(heightColumn);
|
int height = cursor.getInt(heightColumn);
|
||||||
int orientationDegrees = orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0;
|
|
||||||
|
Map<String, Object> entryMap = new HashMap<String, Object>() {{
|
||||||
|
put("uri", itemUri.toString());
|
||||||
|
put("path", path);
|
||||||
|
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);
|
||||||
|
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
// some images are incorrectly registered in the Media Store,
|
// some images are incorrectly registered in the Media Store,
|
||||||
// they are valid but miss some attributes, such as width, height, orientation
|
// they are valid but miss some attributes, such as width, height, orientation
|
||||||
try (InputStream is = activity.getContentResolver().openInputStream(itemUri)) {
|
ImageEntry entry = new ImageEntry(entryMap).fillPreCatalogMetadata(context);
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
entryMap = entry.toMap();
|
||||||
|
width = entry.width != null ? entry.width : 0;
|
||||||
// JPEG
|
height = entry.height != null ? entry.height : 0;
|
||||||
|
|
||||||
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) {
|
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);
|
Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path);
|
||||||
} else {
|
} 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("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);
|
|
||||||
}};
|
|
||||||
entryMap.put("width", width);
|
|
||||||
entryMap.put("height", height);
|
|
||||||
entryMap.put("orientationDegrees", orientationDegrees);
|
|
||||||
newEntryHandler.handleEntry(entryMap);
|
newEntryHandler.handleEntry(entryMap);
|
||||||
entryCount++;
|
entryCount++;
|
||||||
}
|
}
|
||||||
|
@ -213,7 +173,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
return;
|
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
|
// but it doesn't delete the file, even if the app has the permission
|
||||||
StorageUtils.deleteFromSdCard(activity, sdCardTreeUri, Env.getStorageVolumes(activity), path);
|
StorageUtils.deleteFromSdCard(activity, sdCardTreeUri, Env.getStorageVolumes(activity), path);
|
||||||
Log.d(LOG_TAG, "deleted from SD card at path=" + uri);
|
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
|
// mime types
|
||||||
|
|
||||||
|
public static final String MIME_IMAGE = "image";
|
||||||
public static final String MIME_GIF = "image/gif";
|
public static final String MIME_GIF = "image/gif";
|
||||||
public static final String MIME_JPEG = "image/jpeg";
|
public static final String MIME_JPEG = "image/jpeg";
|
||||||
public static final String MIME_PNG = "image/png";
|
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_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
|
// video metadata keys, from android.media.MediaMetadataRetriever
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
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.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
@ -40,8 +41,9 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class FileUtils {
|
public class FileUtils {
|
||||||
|
// useful (for Download, File, etc.) but slower
|
||||||
public String getPathFromUri(final Context context, final Uri uri) {
|
// 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);
|
String path = getPathFromLocalUri(context, uri);
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
path = getPathFromRemoteUri(context, uri);
|
path = getPathFromRemoteUri(context, uri);
|
||||||
|
@ -50,7 +52,7 @@ public class FileUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@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;
|
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
|
|
||||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
||||||
|
@ -95,7 +97,7 @@ public class FileUtils {
|
||||||
|
|
||||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||||
}
|
}
|
||||||
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
} else if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
|
||||||
// Return the remote address
|
// Return the remote address
|
||||||
if (isGooglePhotosUri(uri)) {
|
if (isGooglePhotosUri(uri)) {
|
||||||
|
@ -103,7 +105,7 @@ public class FileUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDataColumn(context, uri, null, null);
|
return getDataColumn(context, uri, null, null);
|
||||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
} else if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
|
||||||
return uri.getPath();
|
return uri.getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ package deckers.thibault.aves.utils;
|
||||||
|
|
||||||
import android.media.ExifInterface;
|
import android.media.ExifInterface;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
@ -28,7 +30,11 @@ public class MetadataHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
|
// 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
|
// optional sub-second
|
||||||
String subSecond = null;
|
String subSecond = null;
|
||||||
Matcher subSecondMatcher = Pattern.compile("(\\d{6})(\\.\\d+)").matcher(dateString);
|
Matcher subSecondMatcher = Pattern.compile("(\\d{6})(\\.\\d+)").matcher(dateString);
|
||||||
|
|
Loading…
Reference in a new issue