various minor fixes
This commit is contained in:
parent
79e2c857b9
commit
c86534d600
9 changed files with 32 additions and 28 deletions
|
@ -27,9 +27,7 @@
|
||||||
<!-- to access media with unredacted metadata with scoped storage (Android Q+) -->
|
<!-- to access media with unredacted metadata with scoped storage (Android Q+) -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
|
|
||||||
<!-- TODO TLAD remove this permission once this issue is fixed:
|
<!-- TODO TLAD remove this permission when this is fixed: https://github.com/flutter/flutter/issues/42451 -->
|
||||||
https://github.com/flutter/flutter/issues/42451
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<!-- from Android R, we should define <queries> to make other apps visible to this app -->
|
<!-- from Android R, we should define <queries> to make other apps visible to this app -->
|
||||||
|
|
|
@ -55,7 +55,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchAll() {
|
private fun fetchAll() {
|
||||||
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap()) { success(it) }
|
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap()) { success(it) }
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory
|
||||||
import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory
|
import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory
|
||||||
import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory
|
import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
@ -16,6 +18,7 @@ import kotlin.math.roundToLong
|
||||||
|
|
||||||
object ExifInterfaceHelper {
|
object ExifInterfaceHelper {
|
||||||
private val LOG_TAG = LogUtils.createTag(ExifInterfaceHelper::class.java)
|
private val LOG_TAG = LogUtils.createTag(ExifInterfaceHelper::class.java)
|
||||||
|
private val DATETIME_FORMAT = SimpleDateFormat("yyyy:MM:dd hh:mm:ss", Locale.ROOT)
|
||||||
|
|
||||||
private const val precisionErrorTolerance = 1e-10
|
private const val precisionErrorTolerance = 1e-10
|
||||||
|
|
||||||
|
@ -358,11 +361,15 @@ object ExifInterfaceHelper {
|
||||||
|
|
||||||
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) {
|
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) {
|
||||||
if (this.hasAttribute(tag)) {
|
if (this.hasAttribute(tag)) {
|
||||||
// TODO TLAD parse date with "yyyy:MM:dd HH:mm:ss" or find the original long
|
val dateString = this.getAttribute(tag)
|
||||||
val formattedDate = this.getAttribute(tag)
|
if (dateString != null) {
|
||||||
val value = formattedDate?.toLongOrNull()
|
try {
|
||||||
if (value != null && value > 0) {
|
DATETIME_FORMAT.parse(dateString)?.let { date ->
|
||||||
save(value)
|
save(date.time)
|
||||||
|
}
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
Log.w(LOG_TAG, "failed to parse date=$dateString", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import android.provider.MediaStore
|
||||||
import deckers.thibault.aves.model.SourceEntry
|
import deckers.thibault.aves.model.SourceEntry
|
||||||
|
|
||||||
internal class ContentImageProvider : ImageProvider() {
|
internal class ContentImageProvider : ImageProvider() {
|
||||||
override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
callback.onFailure(Exception("MIME type is null for uri=$uri"))
|
callback.onFailure(Exception("MIME type is null for uri=$uri"))
|
||||||
return
|
return
|
||||||
|
|
|
@ -6,7 +6,7 @@ import deckers.thibault.aves.model.SourceEntry
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
internal class FileImageProvider : ImageProvider() {
|
internal class FileImageProvider : ImageProvider() {
|
||||||
override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
callback.onFailure(Exception("MIME type is null for uri=$uri"))
|
callback.onFailure(Exception("MIME type is null for uri=$uri"))
|
||||||
return
|
return
|
||||||
|
|
|
@ -35,7 +35,7 @@ import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
abstract class ImageProvider {
|
abstract class ImageProvider {
|
||||||
open suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
open fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
||||||
callback.onFailure(UnsupportedOperationException())
|
callback.onFailure(UnsupportedOperationException())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,12 @@ import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
|
||||||
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
|
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
|
||||||
import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission
|
import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission
|
||||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class MediaStoreImageProvider : ImageProvider() {
|
class MediaStoreImageProvider : ImageProvider() {
|
||||||
suspend fun fetchAll(context: Context, knownEntries: Map<Int, Int?>, handleNewEntry: NewEntryHandler) {
|
fun fetchAll(context: Context, knownEntries: Map<Int, Int?>, handleNewEntry: NewEntryHandler) {
|
||||||
val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean {
|
val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean {
|
||||||
val knownDate = knownEntries[contentId]
|
val knownDate = knownEntries[contentId]
|
||||||
return knownDate == null || knownDate < dateModifiedSecs
|
return knownDate == null || knownDate < dateModifiedSecs
|
||||||
|
@ -35,7 +34,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION)
|
fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) {
|
||||||
val id = uri.tryParseId()
|
val id = uri.tryParseId()
|
||||||
val onSuccess = fun(entry: FieldMap) {
|
val onSuccess = fun(entry: FieldMap) {
|
||||||
entry["uri"] = uri.toString()
|
entry["uri"] = uri.toString()
|
||||||
|
@ -45,17 +44,17 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
if (mimeType == null || isImage(mimeType)) {
|
if (mimeType == null || isImage(mimeType)) {
|
||||||
val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id)
|
val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id)
|
||||||
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) > 0) return
|
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION)) return
|
||||||
}
|
}
|
||||||
if (mimeType == null || isVideo(mimeType)) {
|
if (mimeType == null || isVideo(mimeType)) {
|
||||||
val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id)
|
val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id)
|
||||||
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return
|
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION)) return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the uri can be a file media URI (e.g. "content://0@media/external/file/30050")
|
// the uri can be a file media URI (e.g. "content://0@media/external/file/30050")
|
||||||
// without an equivalent image/video if it is shared from a file browser
|
// without an equivalent image/video if it is shared from a file browser
|
||||||
// but the file is not publicly visible
|
// but the file is not publicly visible
|
||||||
if (fetchFrom(context, alwaysValid, onSuccess, uri, BASE_PROJECTION, fileMimeType = mimeType) > 0) return
|
if (fetchFrom(context, alwaysValid, onSuccess, uri, BASE_PROJECTION, fileMimeType = mimeType)) return
|
||||||
|
|
||||||
callback.onFailure(Exception("failed to fetch entry at uri=$uri"))
|
callback.onFailure(Exception("failed to fetch entry at uri=$uri"))
|
||||||
}
|
}
|
||||||
|
@ -109,15 +108,15 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return obsoleteIds
|
return obsoleteIds
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchFrom(
|
private fun fetchFrom(
|
||||||
context: Context,
|
context: Context,
|
||||||
isValidEntry: NewEntryChecker,
|
isValidEntry: NewEntryChecker,
|
||||||
handleNewEntry: NewEntryHandler,
|
handleNewEntry: NewEntryHandler,
|
||||||
contentUri: Uri,
|
contentUri: Uri,
|
||||||
projection: Array<String>,
|
projection: Array<String>,
|
||||||
fileMimeType: String? = null,
|
fileMimeType: String? = null,
|
||||||
): Int {
|
): Boolean {
|
||||||
var newEntryCount = 0
|
var found = false
|
||||||
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
|
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
|
||||||
try {
|
try {
|
||||||
val cursor = context.contentResolver.query(contentUri, projection, null, null, orderBy)
|
val cursor = context.contentResolver.query(contentUri, projection, null, null, orderBy)
|
||||||
|
@ -191,11 +190,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewEntry(entryMap)
|
handleNewEntry(entryMap)
|
||||||
// TODO TLAD is this necessary?
|
found = true
|
||||||
if (newEntryCount % 30 == 0) {
|
|
||||||
delay(10)
|
|
||||||
}
|
|
||||||
newEntryCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,7 +199,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(LOG_TAG, "failed to get entries", e)
|
Log.e(LOG_TAG, "failed to get entries", e)
|
||||||
}
|
}
|
||||||
return newEntryCount
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
||||||
|
|
|
@ -7,6 +7,7 @@ object MimeTypes {
|
||||||
|
|
||||||
// generic raster
|
// generic raster
|
||||||
private const val BMP = "image/bmp"
|
private const val BMP = "image/bmp"
|
||||||
|
private const val DJVU = "image/vnd.djvu"
|
||||||
const val GIF = "image/gif"
|
const val GIF = "image/gif"
|
||||||
const val HEIC = "image/heic"
|
const val HEIC = "image/heic"
|
||||||
private const val HEIF = "image/heif"
|
private const val HEIF = "image/heif"
|
||||||
|
@ -35,6 +36,7 @@ object MimeTypes {
|
||||||
private const val VIDEO = "video"
|
private const val VIDEO = "video"
|
||||||
|
|
||||||
private const val MP2T = "video/mp2t"
|
private const val MP2T = "video/mp2t"
|
||||||
|
private const val MP2TS = "video/mp2ts"
|
||||||
private const val WEBM = "video/webm"
|
private const val WEBM = "video/webm"
|
||||||
|
|
||||||
fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE)
|
fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE)
|
||||||
|
@ -68,7 +70,7 @@ object MimeTypes {
|
||||||
|
|
||||||
// as of `metadata-extractor` v2.14.0
|
// as of `metadata-extractor` v2.14.0
|
||||||
fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) {
|
fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) {
|
||||||
WBMP, MP2T, WEBM -> false
|
DJVU, WBMP, MP2T, MP2TS, WEBM -> false
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ class MimeUtils {
|
||||||
switch (mime) {
|
switch (mime) {
|
||||||
case 'image/x-icon':
|
case 'image/x-icon':
|
||||||
return 'ICO';
|
return 'ICO';
|
||||||
|
case 'image/x-jg':
|
||||||
|
return 'ART';
|
||||||
case 'image/vnd.adobe.photoshop':
|
case 'image/vnd.adobe.photoshop':
|
||||||
case 'image/x-photoshop':
|
case 'image/x-photoshop':
|
||||||
return 'PSD';
|
return 'PSD';
|
||||||
|
|
Loading…
Reference in a new issue