Refactor file metadata types

This commit is contained in:
MM20 2023-10-29 22:50:33 +01:00
parent 97fca2d014
commit 0a508a37ba
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
11 changed files with 166 additions and 81 deletions

View File

@ -48,6 +48,7 @@ import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.DefaultToolbarAction
@ -136,7 +137,7 @@ fun FileItem(
)
for ((k, v) in file.metaData) {
Text(
text = stringResource(k, v),
text = stringResource(k.labelRes, v),
style = MaterialTheme.typography.bodySmall
)
}
@ -366,3 +367,21 @@ private fun formatFileSize(size: Long): String {
else -> "${DecimalFormat("#,##0.#").format(size / 1000000000000.0)} TB"
}
}
private val FileMetaType.labelRes: Int
get() {
return when (this) {
FileMetaType.Title -> R.string.file_meta_title
FileMetaType.Artist -> R.string.file_meta_artist
FileMetaType.Album -> R.string.file_meta_album
FileMetaType.Duration -> R.string.file_meta_duration
FileMetaType.Year -> R.string.file_meta_year
FileMetaType.Dimensions -> R.string.file_meta_dimensions
FileMetaType.Location -> R.string.file_meta_location
FileMetaType.AppName -> R.string.file_meta_app_name
FileMetaType.AppVersion -> R.string.file_meta_app_version
FileMetaType.AppMinSdk -> R.string.file_meta_app_min_sdk
FileMetaType.AppPackageName -> R.string.file_meta_app_pkgname
FileMetaType.Owner -> R.string.file_meta_owner
}
}

View File

@ -6,6 +6,7 @@ import de.mm20.launcher2.base.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import kotlinx.collections.immutable.ImmutableMap
import java.util.Locale
interface File : SavableSearchable {
@ -13,7 +14,7 @@ interface File : SavableSearchable {
val mimeType: String
val size: Long
val isDirectory: Boolean
val metaData: List<Pair<Int, String>>
val metaData: ImmutableMap<FileMetaType, String>
val isStoredInCloud: Boolean
@ -140,3 +141,18 @@ interface File : SavableSearchable {
suspend fun delete(context: Context) {}
}
enum class FileMetaType {
Title,
Artist,
Album,
Duration,
Year,
Dimensions,
Location,
AppName,
AppVersion,
AppMinSdk,
AppPackageName,
Owner,
}

View File

@ -11,9 +11,12 @@ import de.mm20.launcher2.files.providers.OwncloudFile
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@ -93,8 +96,8 @@ internal class GDriveFileSerializer : SearchableSerializer {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
FileMetaType.Owner -> "owner"
FileMetaType.Dimensions -> "dimensions"
else -> "other"
}, v
)
@ -119,10 +122,10 @@ internal class GDriveFileDeserializer : SearchableDeserializer {
val uri = json.getString("uri")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableListOf<Pair<Int, String>>()
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
val metaData = mutableMapOf<FileMetaType, String>()
owner.takeIf { it.isNotEmpty() }?.let { metaData[FileMetaType.Owner] = it }
dimensions.takeIf { it.isNotEmpty() }
?.let { metaData.add(R.string.file_meta_dimensions to it) }
?.let { metaData[FileMetaType.Dimensions] = it }
return GDriveFile(
fileId = id,
label = label,
@ -132,7 +135,7 @@ internal class GDriveFileDeserializer : SearchableDeserializer {
directoryColor = color,
isDirectory = directory,
viewUri = uri,
metaData = metaData
metaData = metaData.toImmutableMap()
)
}
}
@ -151,8 +154,8 @@ internal class OneDriveFileSerializer : SearchableSerializer {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
FileMetaType.Owner -> "owner"
FileMetaType.Dimensions -> "dimensions"
else -> "other"
}, v
)
@ -175,10 +178,10 @@ internal class OneDriveFileDeserializer : SearchableDeserializer {
val webUrl = json.getString("webUrl")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableListOf<Pair<Int, String>>()
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
val metaData = mutableMapOf<FileMetaType, String>()
owner.takeIf { it.isNotEmpty() }?.let { metaData[FileMetaType.Owner] = it }
dimensions.takeIf { it.isNotEmpty() }
?.let { metaData.add(R.string.file_meta_dimensions to it) }
?.let { metaData[FileMetaType.Dimensions] = it }
return OneDriveFile(
fileId = fileId,
label = label,
@ -186,7 +189,7 @@ internal class OneDriveFileDeserializer : SearchableDeserializer {
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
metaData = metaData,
metaData = metaData.toImmutableMap(),
webUrl = webUrl
)
}
@ -207,7 +210,7 @@ internal class NextcloudFileSerializer : SearchableSerializer {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
FileMetaType.Owner -> "owner"
else -> "other"
}, v
)
@ -239,7 +242,7 @@ internal class NextcloudFileDeserializer : SearchableDeserializer {
size = size,
isDirectory = isDirectory,
server = server,
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
metaData = owner?.let { persistentMapOf(FileMetaType.Owner to it) } ?: persistentMapOf()
)
}
@ -260,7 +263,7 @@ internal class OwncloudFileSerializer : SearchableSerializer {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
FileMetaType.Owner -> "owner"
else -> "other"
}, v
)
@ -292,7 +295,7 @@ internal class OwncloudFileDeserializer : SearchableDeserializer {
size = size,
isDirectory = isDirectory,
server = server,
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
metaData = owner?.let { persistentMapOf(FileMetaType.Owner to it) } ?: persistentMapOf()
)
}

View File

@ -8,7 +8,9 @@ import de.mm20.launcher2.files.GDriveFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.ImmutableMap
internal data class GDriveFile(
val fileId: String,
@ -17,7 +19,7 @@ internal data class GDriveFile(
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: List<Pair<Int, String>>,
override val metaData: ImmutableMap<FileMetaType, String>,
val directoryColor: String?,
val viewUri: String,
override val labelOverride: String? = null,

View File

@ -5,6 +5,9 @@ import de.mm20.launcher2.files.R
import de.mm20.launcher2.gservices.DriveFileMeta
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
internal class GDriveFileProvider(
private val context: Context
@ -27,13 +30,13 @@ internal class GDriveFileProvider(
}
}
private fun getMetadata(file: DriveFileMeta): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>()
private fun getMetadata(file: DriveFileMeta): ImmutableMap<FileMetaType, String> {
val metaData = mutableMapOf<FileMetaType, String>()
val owners = file.owners
metaData.add(R.string.file_meta_owner to owners.joinToString(separator = ", "))
val width = file.width ?: file.width
val height = file.height ?: file.height
if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height")
return metaData
metaData[FileMetaType.Owner] = owners.joinToString(separator = ", ")
val width = file.width
val height = file.height
if (width != null && height != null) metaData[FileMetaType.Dimensions] = "${width}x${height}"
return metaData.toImmutableMap()
}
}

View File

@ -17,13 +17,19 @@ import androidx.core.content.FileProvider
import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.files.LocalFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TransparentLayer
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
@ -36,7 +42,7 @@ internal data class LocalFile(
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: List<Pair<Int, String>>,
override val metaData: ImmutableMap<FileMetaType, String>,
override val labelOverride: String? = null
) : File {
@ -75,6 +81,7 @@ internal data class LocalFile(
backgroundLayer = ColorLayer()
)
}
mimeType.startsWith("video/") -> {
val thumbnail = withContext(Dispatchers.IO) {
ThumbnailUtilsCompat.createVideoThumbnail(
@ -91,6 +98,7 @@ internal data class LocalFile(
backgroundLayer = ColorLayer()
)
}
mimeType.startsWith("audio/") -> {
val thumbnail = withContext(Dispatchers.IO) {
val mediaMetadataRetriever = MediaMetadataRetriever()
@ -123,6 +131,7 @@ internal data class LocalFile(
)
}
mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
val icon = withContext(Dispatchers.IO) {
@ -145,6 +154,7 @@ internal data class LocalFile(
} ?: TransparentLayer,
)
}
else -> {
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
@ -238,48 +248,55 @@ internal data class LocalFile(
context: Context,
mimeType: String,
path: String
): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>()
): ImmutableMap<FileMetaType, String> {
val metaData = mutableMapOf<FileMetaType, String>()
when {
mimeType.startsWith("audio/") -> {
val retriever = MediaMetadataRetriever()
try {
retriever.setDataSource(path)
arrayOf(
R.string.file_meta_title to MediaMetadataRetriever.METADATA_KEY_TITLE,
R.string.file_meta_artist to MediaMetadataRetriever.METADATA_KEY_ARTIST,
R.string.file_meta_album to MediaMetadataRetriever.METADATA_KEY_ALBUM,
R.string.file_meta_year to MediaMetadataRetriever.METADATA_KEY_YEAR
).forEach {
retriever.extractMetadata(it.second)
?.let { m -> metaData.add(it.first to m) }
}
val duration =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong() ?: 0
val d = DateUtils.formatElapsedTime((duration) / 1000)
metaData.add(3, R.string.file_meta_duration to d)
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
?.let { metaData[FileMetaType.Title] = it }
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
?.let { metaData[FileMetaType.Artist] = it }
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
?.let { metaData[FileMetaType.Album] = it }
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)
?.let { metaData[FileMetaType.Year] = it }
retriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLongOrNull()
?.let {
metaData[FileMetaType.Duration] =
DateUtils.formatElapsedTime((it) / 1000)
}
retriever.release()
} catch (e: RuntimeException) {
retriever.release()
}
}
mimeType.startsWith("video/") -> {
val retriever = MediaMetadataRetriever()
try {
retriever.setDataSource(path)
val width =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
?.toLong() ?: 0
?.toLongOrNull()
val height =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
?.toLong() ?: 0
metaData.add(R.string.file_meta_dimensions to "${width}x$height")
?.toLongOrNull()
if (width != null && height != null) {
metaData[FileMetaType.Dimensions] = "${width}x$height"
}
val duration =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong() ?: 0
val d = DateUtils.formatElapsedTime(duration / 1000)
metaData.add(R.string.file_meta_duration to d)
?.toLongOrNull()
?.let {
metaData[FileMetaType.Duration] =
DateUtils.formatElapsedTime((it) / 1000)
}
val loc =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
if (Geocoder.isPresent() && loc != null) {
@ -290,9 +307,15 @@ internal data class LocalFile(
loc.lastIndexOfAny(charArrayOf('+', '-')),
loc.indexOf('/')
).toDouble()
val list = Geocoder(context).getFromLocation(lon, lat, 1)
val list = try {
Geocoder(context).getFromLocation(lon, lat, 1)
} catch (e: IOException) {
null
} catch (e: IllegalArgumentException) {
null
}
if (list != null && list.size > 0) {
metaData.add(R.string.file_meta_location to list[0].formatToString())
metaData[FileMetaType.Location] = list[0].formatToString()
}
}
retriever.release()
@ -304,40 +327,48 @@ internal data class LocalFile(
retriever.release()
}
}
mimeType.startsWith("image/") -> {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, options)
val width = options.outWidth
val height = options.outHeight
metaData.add(R.string.file_meta_dimensions to "${width}x$height")
if (height >= 0 && width >= 0) {
metaData[FileMetaType.Dimensions] = "${width}x$height"
}
try {
val exif = ExifInterface(path)
val loc = exif.latLong
if (loc != null && Geocoder.isPresent()) {
val list = Geocoder(context).getFromLocation(loc[0], loc[1], 1)
val list = try {
Geocoder(context).getFromLocation(loc[0], loc[1], 1)
} catch (e: IllegalArgumentException) {
null
} catch (e: IOException) {
null
}
if (list != null && list.size > 0) {
metaData.add(R.string.file_meta_location to list[0].formatToString())
metaData[FileMetaType.Location] = list[0].formatToString()
}
}
} catch (_: IOException) {
}
}
mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
?: return metaData
metaData.add(
R.string.file_meta_app_name to pkgInfo.applicationInfo.loadLabel(
context.packageManager
).toString()
)
metaData.add(R.string.file_meta_app_pkgname to pkgInfo.packageName)
metaData.add(R.string.file_meta_app_version to pkgInfo.versionName)
metaData.add(R.string.file_meta_app_min_sdk to pkgInfo.applicationInfo.minSdkVersion.toString())
?: return metaData.toImmutableMap()
metaData[FileMetaType.AppName] =
pkgInfo.applicationInfo.loadLabel(context.packageManager).toString()
metaData[FileMetaType.AppPackageName] = pkgInfo.packageName
metaData[FileMetaType.AppVersion] = pkgInfo.versionName
metaData[FileMetaType.AppMinSdk] =
pkgInfo.applicationInfo.minSdkVersion.toString()
}
}
return metaData
return metaData.toImmutableMap()
}
}

View File

@ -9,7 +9,9 @@ import de.mm20.launcher2.files.NextcloudFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.ImmutableMap
internal data class NextcloudFile(
val fileId: Long,
@ -19,7 +21,7 @@ internal data class NextcloudFile(
override val size: Long,
override val isDirectory: Boolean,
val server: String,
override val metaData: List<Pair<Int, String>>,
override val metaData: ImmutableMap<FileMetaType, String>,
override val labelOverride: String? = null,
) : File {

View File

@ -3,6 +3,8 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.files.R
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.min
@ -23,8 +25,8 @@ internal class NextcloudFileProvider(
size = it.size,
isDirectory = it.isDirectory,
server = server,
metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) }
?: emptyList()
metaData = it.owner?.let { persistentMapOf(FileMetaType.Owner to it) }
?: persistentMapOf()
)
}
}

View File

@ -8,18 +8,20 @@ import de.mm20.launcher2.files.OneDriveFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.ImmutableMap
internal data class OneDriveFile(
val fileId: String,
override val label: String,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: List<Pair<Int, String>>,
val webUrl: String,
override val labelOverride: String? = null,
val fileId: String,
override val label: String,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: ImmutableMap<FileMetaType, String>,
val webUrl: String,
override val labelOverride: String? = null,
) : File {
override fun overrideLabel(label: String): OneDriveFile {

View File

@ -8,7 +8,9 @@ import de.mm20.launcher2.files.OwncloudFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.ImmutableMap
internal data class OwncloudFile(
val fileId: Long,
@ -18,7 +20,7 @@ internal data class OwncloudFile(
override val size: Long,
override val isDirectory: Boolean,
val server: String,
override val metaData: List<Pair<Int, String>>,
override val metaData: ImmutableMap<FileMetaType, String>,
override val labelOverride: String? = null,
) : File {

View File

@ -3,6 +3,8 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.files.R
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import kotlinx.collections.immutable.persistentMapOf
internal class OwncloudFileProvider(
private val owncloudClient: OwncloudClient
@ -19,7 +21,8 @@ internal class OwncloudFileProvider(
size = it.size,
isDirectory = it.isDirectory,
server = server,
metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
metaData = it.owner?.let { persistentMapOf(FileMetaType.Owner to it) }
?: persistentMapOf()
)
}
}