Kotlin migration (WIP)
This commit is contained in:
parent
60d16a3e17
commit
46df3e98de
6 changed files with 327 additions and 346 deletions
|
@ -1,41 +0,0 @@
|
|||
package deckers.thibault.aves.model;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import deckers.thibault.aves.utils.MimeTypes;
|
||||
|
||||
public class AvesImageEntry {
|
||||
public Uri uri; // content or file URI
|
||||
public String path; // best effort to get local path
|
||||
public String mimeType;
|
||||
@Nullable
|
||||
public Integer width, height, rotationDegrees;
|
||||
@Nullable
|
||||
public Long dateModifiedSecs;
|
||||
|
||||
public AvesImageEntry(Map<String, Object> map) {
|
||||
this.uri = Uri.parse((String) map.get("uri"));
|
||||
this.path = (String) map.get("path");
|
||||
this.mimeType = (String) map.get("mimeType");
|
||||
this.width = (Integer) map.get("width");
|
||||
this.height = (Integer) map.get("height");
|
||||
this.rotationDegrees = (Integer) map.get("rotationDegrees");
|
||||
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return mimeType.startsWith(MimeTypes.VIDEO);
|
||||
}
|
||||
|
||||
// convenience method
|
||||
|
||||
private static Long toLong(Object o) {
|
||||
if (o == null) return null;
|
||||
if (o instanceof Integer) return Long.valueOf((Integer) o);
|
||||
return (long) o;
|
||||
}
|
||||
}
|
|
@ -1,292 +0,0 @@
|
|||
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.NonNull;
|
||||
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.avi.AviDirectory;
|
||||
import com.drew.metadata.exif.ExifIFD0Directory;
|
||||
import com.drew.metadata.jpeg.JpegDirectory;
|
||||
import com.drew.metadata.mp4.Mp4Directory;
|
||||
import com.drew.metadata.mp4.media.Mp4VideoDirectory;
|
||||
import com.drew.metadata.photoshop.PsdHeaderDirectory;
|
||||
|
||||
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.MetadataHelper;
|
||||
import deckers.thibault.aves.utils.MimeTypes;
|
||||
import deckers.thibault.aves.utils.StorageUtils;
|
||||
|
||||
public class SourceImageEntry {
|
||||
public Uri uri; // content or file URI
|
||||
public String path; // best effort to get local path
|
||||
|
||||
public String sourceMimeType;
|
||||
@Nullable
|
||||
public String title;
|
||||
@Nullable
|
||||
public Integer width, height, rotationDegrees;
|
||||
@Nullable
|
||||
public Boolean isFlipped;
|
||||
@Nullable
|
||||
public Long sizeBytes;
|
||||
@Nullable
|
||||
public Long dateModifiedSecs;
|
||||
@Nullable
|
||||
private Long sourceDateTakenMillis;
|
||||
@Nullable
|
||||
private Long durationMillis;
|
||||
|
||||
public SourceImageEntry() {
|
||||
}
|
||||
|
||||
public SourceImageEntry(@NonNull Map<String, Object> map) {
|
||||
this.uri = Uri.parse((String) map.get("uri"));
|
||||
this.path = (String) map.get("path");
|
||||
this.sourceMimeType = (String) map.get("sourceMimeType");
|
||||
this.width = (int) map.get("width");
|
||||
this.height = (int) map.get("height");
|
||||
this.rotationDegrees = (int) map.get("rotationDegrees");
|
||||
this.sizeBytes = toLong(map.get("sizeBytes"));
|
||||
this.title = (String) map.get("title");
|
||||
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
|
||||
this.sourceDateTakenMillis = toLong(map.get("sourceDateTakenMillis"));
|
||||
this.durationMillis = toLong(map.get("durationMillis"));
|
||||
}
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
return new HashMap<String, Object>() {{
|
||||
put("uri", uri.toString());
|
||||
put("path", path);
|
||||
put("sourceMimeType", sourceMimeType);
|
||||
put("width", width);
|
||||
put("height", height);
|
||||
put("rotationDegrees", rotationDegrees != null ? rotationDegrees : 0);
|
||||
put("isFlipped", isFlipped != null ? isFlipped : false);
|
||||
put("sizeBytes", sizeBytes);
|
||||
put("title", title);
|
||||
put("dateModifiedSecs", dateModifiedSecs);
|
||||
put("sourceDateTakenMillis", sourceDateTakenMillis);
|
||||
put("durationMillis", durationMillis);
|
||||
// only for map export
|
||||
put("contentId", getContentId());
|
||||
}};
|
||||
}
|
||||
|
||||
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 boolean hasOrientation() {
|
||||
return rotationDegrees != null;
|
||||
}
|
||||
|
||||
private boolean hasDuration() {
|
||||
return durationMillis != null && durationMillis > 0;
|
||||
}
|
||||
|
||||
private boolean isImage() {
|
||||
return sourceMimeType.startsWith(MimeTypes.IMAGE);
|
||||
}
|
||||
|
||||
public boolean isSvg() {
|
||||
return sourceMimeType.equals(MimeTypes.SVG);
|
||||
}
|
||||
|
||||
private boolean isVideo() {
|
||||
return sourceMimeType.startsWith(MimeTypes.VIDEO);
|
||||
}
|
||||
|
||||
// metadata retrieval
|
||||
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
public SourceImageEntry fillPreCatalogMetadata(@NonNull Context context) {
|
||||
if (isSvg()) return this;
|
||||
fillByMediaMetadataRetriever(context);
|
||||
if (hasSize() && hasOrientation() && (!isVideo() || hasDuration())) return this;
|
||||
fillByMetadataExtractor(context);
|
||||
if (hasSize()) return this;
|
||||
fillByBitmapDecode(context);
|
||||
return this;
|
||||
}
|
||||
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
private void fillByMediaMetadataRetriever(@NonNull Context context) {
|
||||
if (isImage()) return;
|
||||
|
||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||
if (retriever != null) {
|
||||
try {
|
||||
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.rotationDegrees = 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 {
|
||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||
retriever.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation, date
|
||||
private void fillByMetadataExtractor(@NonNull Context context) {
|
||||
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) return;
|
||||
|
||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
|
||||
// do not switch on specific mime types, as the reported mime type could be wrong
|
||||
// (e.g. PNG registered as JPG)
|
||||
if (isVideo()) {
|
||||
for (AviDirectory dir : metadata.getDirectoriesOfType(AviDirectory.class)) {
|
||||
if (dir.containsTag(AviDirectory.TAG_WIDTH)) {
|
||||
width = dir.getInt(AviDirectory.TAG_WIDTH);
|
||||
}
|
||||
if (dir.containsTag(AviDirectory.TAG_HEIGHT)) {
|
||||
height = dir.getInt(AviDirectory.TAG_HEIGHT);
|
||||
}
|
||||
if (dir.containsTag(AviDirectory.TAG_DURATION)) {
|
||||
durationMillis = dir.getLong(AviDirectory.TAG_DURATION);
|
||||
}
|
||||
}
|
||||
for (Mp4VideoDirectory dir : metadata.getDirectoriesOfType(Mp4VideoDirectory.class)) {
|
||||
if (dir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
||||
width = dir.getInt(Mp4VideoDirectory.TAG_WIDTH);
|
||||
}
|
||||
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
||||
height = dir.getInt(Mp4VideoDirectory.TAG_HEIGHT);
|
||||
}
|
||||
}
|
||||
for (Mp4Directory dir : metadata.getDirectoriesOfType(Mp4Directory.class)) {
|
||||
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
|
||||
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (JpegDirectory dir : metadata.getDirectoriesOfType(JpegDirectory.class)) {
|
||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) {
|
||||
width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH);
|
||||
}
|
||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
|
||||
height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
|
||||
}
|
||||
}
|
||||
for (PsdHeaderDirectory dir : metadata.getDirectoriesOfType(PsdHeaderDirectory.class)) {
|
||||
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_WIDTH)) {
|
||||
width = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH);
|
||||
}
|
||||
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)) {
|
||||
height = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// EXIF, if defined, should override metadata found in other directories
|
||||
for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) {
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_WIDTH)) {
|
||||
width = dir.getInt(ExifIFD0Directory.TAG_IMAGE_WIDTH);
|
||||
}
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_HEIGHT)) {
|
||||
height = dir.getInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT);
|
||||
}
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||
int exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
||||
rotationDegrees = MetadataHelper.getRotationDegreesForExifCode(exifOrientation);
|
||||
isFlipped = MetadataHelper.isFlippedForExifCode(exifOrientation);
|
||||
}
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
|
||||
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | ImageProcessingException | MetadataException | NoClassDefFoundError e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// expects entry with: uri
|
||||
// finds: width, height
|
||||
private void fillByBitmapDecode(@NonNull Context context) {
|
||||
try (InputStream is = StorageUtils.openInputStream(context, 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
|
||||
}
|
||||
}
|
||||
|
||||
// convenience method
|
||||
|
||||
private static Long toLong(@Nullable Object o) {
|
||||
if (o == null) return null;
|
||||
if (o instanceof Integer) return Long.valueOf((Integer) o);
|
||||
return (long) o;
|
||||
}
|
||||
}
|
|
@ -10,12 +10,9 @@ import deckers.thibault.aves.model.SourceImageEntry;
|
|||
class ContentImageProvider extends ImageProvider {
|
||||
@Override
|
||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||
SourceImageEntry entry = new SourceImageEntry();
|
||||
entry.uri = uri;
|
||||
entry.sourceMimeType = mimeType;
|
||||
entry.fillPreCatalogMetadata(context);
|
||||
SourceImageEntry entry = new SourceImageEntry(uri, mimeType).fillPreCatalogMetadata(context);
|
||||
|
||||
if (entry.hasSize() || entry.isSvg()) {
|
||||
if (entry.getHasSize() || entry.isSvg()) {
|
||||
callback.onSuccess(entry.toMap());
|
||||
} else {
|
||||
callback.onFailure(new Exception("entry has no size"));
|
||||
|
|
|
@ -13,19 +13,14 @@ import deckers.thibault.aves.utils.FileUtils;
|
|||
class FileImageProvider extends ImageProvider {
|
||||
@Override
|
||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||
SourceImageEntry entry = new SourceImageEntry();
|
||||
entry.uri = uri;
|
||||
entry.sourceMimeType = mimeType;
|
||||
SourceImageEntry entry = new SourceImageEntry(uri, 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;
|
||||
entry.initFromFile(path, file.getName(), file.length(), file.lastModified() / 1000);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
callback.onFailure(e);
|
||||
|
@ -33,7 +28,7 @@ class FileImageProvider extends ImageProvider {
|
|||
}
|
||||
entry.fillPreCatalogMetadata(context);
|
||||
|
||||
if (entry.hasSize() || entry.isSvg()) {
|
||||
if (entry.getHasSize() || entry.isSvg()) {
|
||||
callback.onSuccess(entry.toMap());
|
||||
} else {
|
||||
callback.onFailure(new Exception("entry has no size"));
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package deckers.thibault.aves.model
|
||||
|
||||
import android.net.Uri
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
|
||||
class AvesImageEntry(map: Map<String?, Any?>) {
|
||||
@JvmField
|
||||
val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI
|
||||
|
||||
@JvmField
|
||||
val path = map["path"] as String? // best effort to get local path
|
||||
|
||||
@JvmField
|
||||
val mimeType = map["mimeType"] as String
|
||||
|
||||
@JvmField
|
||||
val width = map["width"] as Int
|
||||
|
||||
@JvmField
|
||||
val height = map["height"] as Int
|
||||
|
||||
@JvmField
|
||||
val rotationDegrees = map["rotationDegrees"] as Int
|
||||
|
||||
@JvmField
|
||||
val dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||
|
||||
val isVideo: Boolean
|
||||
get() = mimeType.startsWith(MimeTypes.VIDEO)
|
||||
|
||||
companion object {
|
||||
// convenience method
|
||||
private fun toLong(o: Any?): Long? = when (o) {
|
||||
is Int -> o.toLong()
|
||||
else -> o as? Long
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
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 com.drew.imaging.ImageMetadataReader
|
||||
import com.drew.metadata.avi.AviDirectory
|
||||
import com.drew.metadata.exif.ExifIFD0Directory
|
||||
import com.drew.metadata.jpeg.JpegDirectory
|
||||
import com.drew.metadata.mp4.Mp4Directory
|
||||
import com.drew.metadata.mp4.media.Mp4VideoDirectory
|
||||
import com.drew.metadata.photoshop.PsdHeaderDirectory
|
||||
import deckers.thibault.aves.utils.MetadataHelper.getRotationDegreesForExifCode
|
||||
import deckers.thibault.aves.utils.MetadataHelper.isFlippedForExifCode
|
||||
import deckers.thibault.aves.utils.MetadataHelper.parseVideoMetadataDate
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class SourceImageEntry {
|
||||
val uri: Uri // content or file URI
|
||||
var path: String? = null // best effort to get local path
|
||||
private val sourceMimeType: String
|
||||
var title: String? = null
|
||||
var width: Int? = null
|
||||
var height: Int? = null
|
||||
private var rotationDegrees: Int? = null
|
||||
private var isFlipped: Boolean? = null
|
||||
var sizeBytes: Long? = null
|
||||
var dateModifiedSecs: Long? = null
|
||||
private var sourceDateTakenMillis: Long? = null
|
||||
private var durationMillis: Long? = null
|
||||
|
||||
constructor(uri: Uri, sourceMimeType: String) {
|
||||
this.uri = uri
|
||||
this.sourceMimeType = sourceMimeType
|
||||
}
|
||||
|
||||
constructor(map: Map<String, Any?>) {
|
||||
uri = Uri.parse(map["uri"] as String)
|
||||
path = map["path"] as String?
|
||||
sourceMimeType = map["sourceMimeType"] as String
|
||||
width = map["width"] as Int
|
||||
height = map["height"] as Int
|
||||
rotationDegrees = map["rotationDegrees"] as Int
|
||||
sizeBytes = toLong(map["sizeBytes"])
|
||||
title = map["title"] as String?
|
||||
dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||
sourceDateTakenMillis = toLong(map["sourceDateTakenMillis"])
|
||||
durationMillis = toLong(map["durationMillis"])
|
||||
}
|
||||
|
||||
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedSecs: Long) {
|
||||
this.path = path
|
||||
this.title = title
|
||||
this.sizeBytes = sizeBytes
|
||||
this.dateModifiedSecs = dateModifiedSecs
|
||||
}
|
||||
|
||||
fun toMap(): Map<String, Any?> {
|
||||
return hashMapOf(
|
||||
"uri" to uri.toString(),
|
||||
"path" to path,
|
||||
"sourceMimeType" to sourceMimeType,
|
||||
"width" to width,
|
||||
"height" to height,
|
||||
"rotationDegrees" to (rotationDegrees ?: 0),
|
||||
"isFlipped" to (isFlipped ?: false),
|
||||
"sizeBytes" to sizeBytes,
|
||||
"title" to title,
|
||||
"dateModifiedSecs" to dateModifiedSecs,
|
||||
"sourceDateTakenMillis" to sourceDateTakenMillis,
|
||||
"durationMillis" to durationMillis,
|
||||
// only for map export
|
||||
"contentId" to contentId,
|
||||
)
|
||||
}
|
||||
|
||||
// ignore when the ID is not a number
|
||||
// e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg
|
||||
private val contentId: Long?
|
||||
get() {
|
||||
if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
|
||||
try {
|
||||
return ContentUris.parseId(uri)
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val hasSize: Boolean
|
||||
get() = width ?: 0 > 0 && height ?: 0 > 0
|
||||
|
||||
private val hasOrientation: Boolean
|
||||
get() = rotationDegrees != null
|
||||
|
||||
private val hasDuration: Boolean
|
||||
get() = durationMillis ?: 0 > 0
|
||||
|
||||
private val isImage: Boolean
|
||||
get() = sourceMimeType.startsWith(MimeTypes.IMAGE)
|
||||
|
||||
private val isVideo: Boolean
|
||||
get() = sourceMimeType.startsWith(MimeTypes.VIDEO)
|
||||
|
||||
val isSvg: Boolean
|
||||
get() = sourceMimeType == MimeTypes.SVG
|
||||
|
||||
// metadata retrieval
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
fun fillPreCatalogMetadata(context: Context): SourceImageEntry {
|
||||
if (isSvg) return this
|
||||
fillByMediaMetadataRetriever(context)
|
||||
if (hasSize && hasOrientation && (!isVideo || hasDuration)) return this
|
||||
fillByMetadataExtractor(context)
|
||||
if (hasSize) return this
|
||||
fillByBitmapDecode(context)
|
||||
return this
|
||||
}
|
||||
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
private fun fillByMediaMetadataRetriever(context: Context) {
|
||||
if (isImage) return
|
||||
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return
|
||||
try {
|
||||
var width: String? = null
|
||||
var height: String? = null
|
||||
var rotationDegrees: String? = null
|
||||
var durationMillis: String? = 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)
|
||||
rotationDegrees = 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)
|
||||
rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
||||
durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
}
|
||||
if (width != null) {
|
||||
this.width = width.toInt()
|
||||
}
|
||||
if (height != null) {
|
||||
this.height = height.toInt()
|
||||
}
|
||||
if (rotationDegrees != null) {
|
||||
this.rotationDegrees = rotationDegrees.toInt()
|
||||
}
|
||||
if (durationMillis != null) {
|
||||
this.durationMillis = durationMillis.toLong()
|
||||
}
|
||||
val dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
|
||||
val dateMillis = parseVideoMetadataDate(dateString)
|
||||
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
||||
if (dateMillis > 0) {
|
||||
sourceDateTakenMillis = dateMillis
|
||||
}
|
||||
val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||
if (title != null) {
|
||||
this.title = title
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
} finally {
|
||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||
retriever.release()
|
||||
}
|
||||
}
|
||||
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation, date
|
||||
private fun fillByMetadataExtractor(context: Context) {
|
||||
if (!isSupportedByMetadataExtractor(sourceMimeType)) return
|
||||
try {
|
||||
StorageUtils.openInputStream(context, uri).use { input ->
|
||||
val metadata = ImageMetadataReader.readMetadata(input)
|
||||
|
||||
// do not switch on specific mime types, as the reported mime type could be wrong
|
||||
// (e.g. PNG registered as JPG)
|
||||
if (isVideo) {
|
||||
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {
|
||||
if (dir.containsTag(AviDirectory.TAG_WIDTH)) {
|
||||
width = dir.getInt(AviDirectory.TAG_WIDTH)
|
||||
}
|
||||
if (dir.containsTag(AviDirectory.TAG_HEIGHT)) {
|
||||
height = dir.getInt(AviDirectory.TAG_HEIGHT)
|
||||
}
|
||||
if (dir.containsTag(AviDirectory.TAG_DURATION)) {
|
||||
durationMillis = dir.getLong(AviDirectory.TAG_DURATION)
|
||||
}
|
||||
}
|
||||
for (dir in metadata.getDirectoriesOfType(Mp4VideoDirectory::class.java)) {
|
||||
if (dir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
||||
width = dir.getInt(Mp4VideoDirectory.TAG_WIDTH)
|
||||
}
|
||||
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
||||
height = dir.getInt(Mp4VideoDirectory.TAG_HEIGHT)
|
||||
}
|
||||
}
|
||||
for (dir in metadata.getDirectoriesOfType(Mp4Directory::class.java)) {
|
||||
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
|
||||
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (dir in metadata.getDirectoriesOfType(JpegDirectory::class.java)) {
|
||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) {
|
||||
width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH)
|
||||
}
|
||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
|
||||
height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT)
|
||||
}
|
||||
}
|
||||
for (dir in metadata.getDirectoriesOfType(PsdHeaderDirectory::class.java)) {
|
||||
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_WIDTH)) {
|
||||
width = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH)
|
||||
}
|
||||
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)) {
|
||||
height = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)
|
||||
}
|
||||
}
|
||||
|
||||
// EXIF, if defined, should override metadata found in other directories
|
||||
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_WIDTH)) {
|
||||
width = dir.getInt(ExifIFD0Directory.TAG_IMAGE_WIDTH)
|
||||
}
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_HEIGHT)) {
|
||||
height = dir.getInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT)
|
||||
}
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||
val exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION)
|
||||
rotationDegrees = getRotationDegreesForExifCode(exifOrientation)
|
||||
isFlipped = isFlippedForExifCode(exifOrientation)
|
||||
}
|
||||
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
|
||||
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// expects entry with: uri
|
||||
// finds: width, height
|
||||
private fun fillByBitmapDecode(context: Context) {
|
||||
try {
|
||||
StorageUtils.openInputStream(context, uri).use { input ->
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(input, null, options)
|
||||
width = options.outWidth
|
||||
height = options.outHeight
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// convenience method
|
||||
private fun toLong(o: Any?): Long? = when (o) {
|
||||
is Int -> o.toLong()
|
||||
else -> o as? Long
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue