info: PNG IPTC display
This commit is contained in:
parent
f72b3e775f
commit
90f6c5d841
10 changed files with 105 additions and 23 deletions
|
@ -34,16 +34,20 @@ import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription
|
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription
|
||||||
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt
|
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt
|
||||||
|
import deckers.thibault.aves.metadata.Metadata.DIR_PNG_TEXTUAL_DATA
|
||||||
import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode
|
import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode
|
||||||
import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode
|
import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode
|
||||||
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_ITXT_DIR_NAME
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME
|
||||||
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractPngProfile
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
|
||||||
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isPngTextDir
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
|
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
||||||
|
@ -53,12 +57,12 @@ import deckers.thibault.aves.metadata.XMP.isPanorama
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
|
||||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
||||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
|
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isHeic
|
import deckers.thibault.aves.utils.MimeTypes.isHeic
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
import deckers.thibault.aves.utils.MimeTypes.isImage
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
@ -143,7 +147,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
// optional parent to distinguish child directories of the same type
|
// optional parent to distinguish child directories of the same type
|
||||||
dir.parent?.name?.let { thisDirName = "$it/$thisDirName" }
|
dir.parent?.name?.let { thisDirName = "$it/$thisDirName" }
|
||||||
|
|
||||||
val dirMap = metadataMap[thisDirName] ?: HashMap()
|
var dirMap = metadataMap[thisDirName] ?: HashMap()
|
||||||
metadataMap[thisDirName] = dirMap
|
metadataMap[thisDirName] = dirMap
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
|
@ -168,18 +172,35 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
} else {
|
} else {
|
||||||
dirMap.putAll(tags.map { tagMapper(it) })
|
dirMap.putAll(tags.map { tagMapper(it) })
|
||||||
}
|
}
|
||||||
} else if (dir is PngDirectory) {
|
} else if (dir.isPngTextDir()) {
|
||||||
|
metadataMap.remove(thisDirName)
|
||||||
|
dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap()
|
||||||
|
metadataMap[DIR_PNG_TEXTUAL_DATA] = dirMap
|
||||||
|
|
||||||
for (tag in tags) {
|
for (tag in tags) {
|
||||||
val tagType = tag.tagType
|
val tagType = tag.tagType
|
||||||
if (tagType == PngDirectory.TAG_TEXTUAL_DATA) {
|
if (tagType == PngDirectory.TAG_TEXTUAL_DATA) {
|
||||||
val pairs = dir.getObject(tagType) as List<*>
|
val pairs = dir.getObject(tagType) as List<*>
|
||||||
dirMap.putAll(pairs.map {
|
val textPairs = pairs.map { pair ->
|
||||||
val kv = it as KeyValuePair
|
val kv = pair as KeyValuePair
|
||||||
// PNG spec says encoding charset is always Latin-1 / ISO-8859-1
|
val key = kv.key
|
||||||
// but in practice UTF-8 is sometimes used in PNG-iTXt chunks
|
// `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1
|
||||||
val charset = if (baseDirName == "PNG-iTXt") StandardCharsets.UTF_8 else kv.value.charset
|
val charset = if (baseDirName == PNG_ITXT_DIR_NAME) StandardCharsets.UTF_8 else kv.value.charset
|
||||||
Pair(kv.key, String(kv.value.bytes, charset))
|
val valueString = String(kv.value.bytes, charset)
|
||||||
})
|
val dirs = extractPngProfile(key, valueString)
|
||||||
|
if (dirs?.any() == true) {
|
||||||
|
dirs.forEach { profileDir ->
|
||||||
|
val profileDirName = profileDir.name
|
||||||
|
val profileDirMap = metadataMap[profileDirName] ?: HashMap()
|
||||||
|
metadataMap[profileDirName] = profileDirMap
|
||||||
|
profileDirMap.putAll(profileDir.tags.map { Pair(it.tagName, it.description) })
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Pair(key, valueString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dirMap.putAll(textPairs.filterNotNull())
|
||||||
} else {
|
} else {
|
||||||
dirMap[tag.tagName] = tag.description
|
dirMap[tag.tagName] = tag.description
|
||||||
}
|
}
|
||||||
|
@ -383,7 +404,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
// In the end, `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives),
|
// In the end, `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives),
|
||||||
// in which case we trust the file extension
|
// in which case we trust the file extension
|
||||||
// cf https://github.com/drewnoakes/metadata-extractor/issues/296
|
// cf https://github.com/drewnoakes/metadata-extractor/issues/296
|
||||||
if (path?.matches(tiffExtensionPattern) == true) {
|
if (path?.matches(TIFF_EXTENSION_PATTERN) == true) {
|
||||||
metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF
|
metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF
|
||||||
} else {
|
} else {
|
||||||
dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) {
|
dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) {
|
||||||
|
|
|
@ -79,7 +79,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
} else {
|
} else {
|
||||||
var errorDetails: String? = exception?.message
|
var errorDetails: String? = exception?.message
|
||||||
if (errorDetails?.isNotEmpty() == true) {
|
if (errorDetails?.isNotEmpty() == true) {
|
||||||
errorDetails = errorDetails.split("\n".toRegex(), 2).first()
|
errorDetails = errorDetails.split(Regex("\n"), 2).first()
|
||||||
}
|
}
|
||||||
result.error("getThumbnail-null", "failed to get thumbnail for mimeType=$mimeType uri=$uri", errorDetails)
|
result.error("getThumbnail-null", "failed to get thumbnail for mimeType=$mimeType uri=$uri", errorDetails)
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
private fun toErrorDetails(e: Exception): String? {
|
private fun toErrorDetails(e: Exception): String? {
|
||||||
val errorDetails = e.message
|
val errorDetails = e.message
|
||||||
return if (errorDetails?.isNotEmpty() == true) {
|
return if (errorDetails?.isNotEmpty() == true) {
|
||||||
errorDetails.split("\n".toRegex(), 2).first()
|
errorDetails.split(Regex("\n"), 2).first()
|
||||||
} else {
|
} else {
|
||||||
errorDetails
|
errorDetails
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import java.util.regex.Pattern
|
||||||
object Metadata {
|
object Metadata {
|
||||||
private val LOG_TAG = LogUtils.createTag<Metadata>()
|
private val LOG_TAG = LogUtils.createTag<Metadata>()
|
||||||
|
|
||||||
|
const val IPTC_MARKER_BYTE: Byte = 0x1c
|
||||||
|
|
||||||
// Pattern to extract latitude & longitude from a video location tag (cf ISO 6709)
|
// Pattern to extract latitude & longitude from a video location tag (cf ISO 6709)
|
||||||
// Examples:
|
// Examples:
|
||||||
// "+37.5090+127.0243/" (Samsung)
|
// "+37.5090+127.0243/" (Samsung)
|
||||||
|
@ -31,6 +33,7 @@ object Metadata {
|
||||||
const val DIR_XMP = "XMP" // from metadata-extractor
|
const val DIR_XMP = "XMP" // from metadata-extractor
|
||||||
const val DIR_MEDIA = "Media" // custom
|
const val DIR_MEDIA = "Media" // custom
|
||||||
const val DIR_COVER_ART = "Cover" // custom
|
const val DIR_COVER_ART = "Cover" // custom
|
||||||
|
const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom
|
||||||
|
|
||||||
// types of metadata
|
// types of metadata
|
||||||
const val TYPE_EXIF = "exif"
|
const val TYPE_EXIF = "exif"
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
package deckers.thibault.aves.metadata
|
package deckers.thibault.aves.metadata
|
||||||
|
|
||||||
import com.drew.lang.Rational
|
import com.drew.lang.Rational
|
||||||
|
import com.drew.lang.SequentialByteArrayReader
|
||||||
import com.drew.metadata.Directory
|
import com.drew.metadata.Directory
|
||||||
import com.drew.metadata.exif.ExifIFD0Directory
|
import com.drew.metadata.exif.ExifIFD0Directory
|
||||||
|
import com.drew.metadata.iptc.IptcReader
|
||||||
|
import com.drew.metadata.png.PngDirectory
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object MetadataExtractorHelper {
|
object MetadataExtractorHelper {
|
||||||
|
const val PNG_ITXT_DIR_NAME = "PNG-iTXt"
|
||||||
|
private const val PNG_TEXT_DIR_NAME = "PNG-tEXt"
|
||||||
const val PNG_TIME_DIR_NAME = "PNG-tIME"
|
const val PNG_TIME_DIR_NAME = "PNG-tIME"
|
||||||
|
private const val PNG_ZTXT_DIR_NAME = "PNG-zTXt"
|
||||||
|
|
||||||
val PNG_LAST_MODIFICATION_TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.ROOT)
|
val PNG_LAST_MODIFICATION_TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.ROOT)
|
||||||
|
|
||||||
|
// Pattern to extract profile name, length, and text data
|
||||||
|
// of raw profiles (EXIF, IPTC, etc.) in PNG `zTXt` chunks
|
||||||
|
// e.g. "iptc [...] 114 [...] 3842494d040400[...]"
|
||||||
|
private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
|
|
||||||
fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) {
|
fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) {
|
||||||
|
@ -59,4 +71,45 @@ object MetadataExtractorHelper {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PNG
|
||||||
|
|
||||||
|
fun Directory.isPngTextDir(): Boolean = this is PngDirectory && setOf(PNG_ITXT_DIR_NAME, PNG_TEXT_DIR_NAME, PNG_ZTXT_DIR_NAME).contains(this.name)
|
||||||
|
|
||||||
|
fun extractPngProfile(key: String, valueString: String): Iterable<Directory>? {
|
||||||
|
when (key) {
|
||||||
|
"Raw profile type iptc" -> {
|
||||||
|
val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString)
|
||||||
|
if (match != null) {
|
||||||
|
val dataString = match.groupValues[3]
|
||||||
|
val hexString = dataString.replace(Regex("[\\r\\n]"), "")
|
||||||
|
val dataBytes = hexStringToByteArray(hexString)
|
||||||
|
if (dataBytes != null) {
|
||||||
|
val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE)
|
||||||
|
if (start != -1) {
|
||||||
|
val segmentBytes = dataBytes.copyOfRange(fromIndex = start, toIndex = dataBytes.size - start)
|
||||||
|
val metadata = com.drew.metadata.Metadata()
|
||||||
|
IptcReader().extract(SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.size.toLong())
|
||||||
|
return metadata.directories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience methods
|
||||||
|
|
||||||
|
private fun hexStringToByteArray(hexString: String): ByteArray? {
|
||||||
|
if (hexString.length % 2 != 0) return null
|
||||||
|
|
||||||
|
val dataBytes = ByteArray(hexString.length / 2)
|
||||||
|
var i = 0
|
||||||
|
while (i < hexString.length) {
|
||||||
|
dataBytes[i / 2] = hexString.substring(i, i + 2).toByte(16)
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
return dataBytes
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -122,7 +122,7 @@ abstract class ImageProvider {
|
||||||
|
|
||||||
var desiredNameWithoutExtension = if (sourceEntry.path != null) {
|
var desiredNameWithoutExtension = if (sourceEntry.path != null) {
|
||||||
val sourceFileName = File(sourceEntry.path).name
|
val sourceFileName = File(sourceEntry.path).name
|
||||||
sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "")
|
sourceFileName.replaceFirst(FILE_EXTENSION_PATTERN, "")
|
||||||
} else {
|
} else {
|
||||||
sourceUri.lastPathSegment!!
|
sourceUri.lastPathSegment!!
|
||||||
}
|
}
|
||||||
|
@ -765,6 +765,8 @@ abstract class ImageProvider {
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<ImageProvider>()
|
private val LOG_TAG = LogUtils.createTag<ImageProvider>()
|
||||||
|
|
||||||
|
val FILE_EXTENSION_PATTERN = Regex("[.][^.]+$")
|
||||||
|
|
||||||
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
|
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
|
||||||
|
|
||||||
// used when skipping a move/creation op because the target file already exists
|
// used when skipping a move/creation op because the target file already exists
|
||||||
|
|
|
@ -345,7 +345,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceFileName = sourceFile.name
|
val sourceFileName = sourceFile.name
|
||||||
val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "")
|
val desiredNameWithoutExtension = sourceFileName.replaceFirst(FILE_EXTENSION_PATTERN, "")
|
||||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
dir = destinationDir,
|
dir = destinationDir,
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
package deckers.thibault.aves.utils
|
package deckers.thibault.aves.utils
|
||||||
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
object LogUtils {
|
object LogUtils {
|
||||||
const val LOG_TAG_MAX_LENGTH = 23
|
const val LOG_TAG_MAX_LENGTH = 23
|
||||||
val LOG_TAG_PACKAGE_PATTERN: Pattern = Pattern.compile("(\\w)(\\w*)\\.")
|
|
||||||
|
val LOG_TAG_PACKAGE_PATTERN = Regex("(\\w)(\\w*)\\.")
|
||||||
|
val LOWER_CASE_PATTERN = Regex("[a-z]")
|
||||||
|
|
||||||
// create an Android logger friendly log tag for the specified class
|
// create an Android logger friendly log tag for the specified class
|
||||||
inline fun <reified T> createTag(): String {
|
inline fun <reified T> createTag(): String {
|
||||||
val kClass = T::class
|
val kClass = T::class
|
||||||
// shorten class name to "a.b.CccDdd"
|
// shorten class name to "a.b.CccDdd"
|
||||||
var logTag = LOG_TAG_PACKAGE_PATTERN.matcher(kClass.qualifiedName!!).replaceAll("$1.")
|
var logTag = LOG_TAG_PACKAGE_PATTERN.replace(kClass.qualifiedName!!, "$1.")
|
||||||
if (logTag.length > LOG_TAG_MAX_LENGTH) {
|
if (logTag.length > LOG_TAG_MAX_LENGTH) {
|
||||||
// shorten class name to "a.b.CD"
|
// shorten class name to "a.b.CD"
|
||||||
val simpleName = kClass.simpleName!!
|
val simpleName = kClass.simpleName!!
|
||||||
val shortSimpleName = simpleName.replace("[a-z]".toRegex(), "")
|
val shortSimpleName = simpleName.replace(LOWER_CASE_PATTERN, "")
|
||||||
logTag = logTag.replace(simpleName, shortSimpleName)
|
logTag = logTag.replace(simpleName, shortSimpleName)
|
||||||
if (logTag.length > LOG_TAG_MAX_LENGTH) {
|
if (logTag.length > LOG_TAG_MAX_LENGTH) {
|
||||||
// shorten class name to "CD"
|
// shorten class name to "CD"
|
||||||
|
|
|
@ -180,5 +180,5 @@ object MimeTypes {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
val tiffExtensionPattern = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
|
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,9 @@ import java.util.regex.Pattern
|
||||||
object StorageUtils {
|
object StorageUtils {
|
||||||
private val LOG_TAG = LogUtils.createTag<StorageUtils>()
|
private val LOG_TAG = LogUtils.createTag<StorageUtils>()
|
||||||
|
|
||||||
|
private const val TREE_URI_ROOT = "content://com.android.externalstorage.documents/tree/"
|
||||||
|
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Volume paths
|
* Volume paths
|
||||||
*/
|
*/
|
||||||
|
@ -269,8 +272,8 @@ object StorageUtils {
|
||||||
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
|
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
|
||||||
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
|
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
|
||||||
fun convertTreeUriToDirPath(context: Context, treeUri: Uri): String? {
|
fun convertTreeUriToDirPath(context: Context, treeUri: Uri): String? {
|
||||||
val encoded = treeUri.toString().substring("content://com.android.externalstorage.documents/tree/".length)
|
val encoded = treeUri.toString().substring(TREE_URI_ROOT.length)
|
||||||
val matcher = Pattern.compile("(.*?):(.*)").matcher(Uri.decode(encoded))
|
val matcher = TREE_URI_PATH_PATTERN.matcher(Uri.decode(encoded))
|
||||||
with(matcher) {
|
with(matcher) {
|
||||||
if (find()) {
|
if (find()) {
|
||||||
val uuid = group(1)
|
val uuid = group(1)
|
||||||
|
|
Loading…
Reference in a new issue