Refactor files

This commit is contained in:
MM20 2021-12-13 21:12:51 +01:00
parent e46d5a7d7e
commit 183ba586b3
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
12 changed files with 377 additions and 371 deletions

View File

@ -44,8 +44,8 @@ val favoritesModule = module {
if (searchable is NextcloudFile) { if (searchable is NextcloudFile) {
return@factory NextcloudFileSerializer() return@factory NextcloudFileSerializer()
} }
if (searchable is File) { if (searchable is LocalFile) {
return@factory FileSerializer() return@factory LocalFileSerializer()
} }
if (searchable is Website) { if (searchable is Website) {
return@factory WebsiteSerializer() return@factory WebsiteSerializer()
@ -83,7 +83,7 @@ val favoritesModule = module {
return@factory OwncloudFileDeserializer() return@factory OwncloudFileDeserializer()
} }
if (type == "file") { if (type == "file") {
return@factory FileDeserializer(androidContext()) return@factory LocalFileDeserializer(androidContext())
} }
if (type == "website") { if (type == "website") {
return@factory WebsiteDeserializer() return@factory WebsiteDeserializer()

View File

@ -10,9 +10,9 @@ import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.*
import org.json.JSONObject import org.json.JSONObject
class FileSerializer : SearchableSerializer { class LocalFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: Searchable): String {
searchable as File searchable as LocalFile
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.id "id" to searchable.id
).toString() ).toString()
@ -22,7 +22,7 @@ class FileSerializer : SearchableSerializer {
get() = "file" get() = "file"
} }
class FileDeserializer( class LocalFileDeserializer(
val context: Context val context: Context
) : SearchableDeserializer { ) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): Searchable? {
@ -48,20 +48,20 @@ class FileDeserializer(
val directory = java.io.File(path).isDirectory val directory = java.io.File(path).isDirectory
val id = cursor.getLong(0) val id = cursor.getLong(0)
val mimeType = cursor.getStringOrNull(3) val mimeType = cursor.getStringOrNull(3)
?: if (directory) "inode/directory" else File.getMimetypeByFileExtension( ?: if (directory) "inode/directory" else LocalFile.getMimetypeByFileExtension(
path.substringAfterLast( path.substringAfterLast(
'.' '.'
) )
) )
val size = cursor.getLong(1) val size = cursor.getLong(1)
cursor.close() cursor.close()
return File( return LocalFile(
path = path, path = path,
mimeType = mimeType, mimeType = mimeType,
size = size, size = size,
isDirectory = directory, isDirectory = directory,
id = id, id = id,
metaData = File.getMetaData(context, mimeType, path) metaData = LocalFile.getMetaData(context, mimeType, path)
) )
} }
cursor.close() cursor.close()

View File

@ -15,6 +15,8 @@ class FilesRepository(
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() { ) : BaseSearchableRepository() {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
val files = MediatorLiveData<List<File>?>() val files = MediatorLiveData<List<File>?>()
private val allFiles = MutableLiveData<List<File>?>(emptyList()) private val allFiles = MutableLiveData<List<File>?>(emptyList())
@ -42,7 +44,7 @@ class FilesRepository(
return return
} }
val localFiles = withContext(Dispatchers.IO) { val localFiles = withContext(Dispatchers.IO) {
File.search(context, query).sorted().toMutableList() LocalFile.search(context, query).sorted().toMutableList()
} }
allFiles.value = localFiles allFiles.value = localFiles
@ -59,7 +61,12 @@ class FilesRepository(
allFiles.value = localFiles + cloudFiles allFiles.value = localFiles + cloudFiles
} }
fun removeFile(file: File) { fun deleteFile(file: File) {
allFiles.value = allFiles.value?.filter { it != file } if (file.isDeletable) {
scope.launch {
file.delete(context)
allFiles.value = allFiles.value?.filter { it != file }
}
}
} }
} }

View File

@ -10,7 +10,7 @@ class FilesViewModel(
val files = filesRepository.files val files = filesRepository.files
fun removeFile(file: File) { fun deleteFile(file: File) {
filesRepository.removeFile(file) filesRepository.deleteFile(file)
} }
} }

View File

@ -1,35 +1,13 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.location.Geocoder
import android.media.MediaMetadataRetriever
import android.media.ThumbnailUtils
import android.os.Build
import android.provider.MediaStore
import android.text.format.DateUtils
import android.util.Size
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.database.getStringOrNull
import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.util.* import java.util.*
import java.io.File as JavaIOFile
open class File( abstract class File(
val id: Long, val id: Long,
val path: String, val path: String,
val mimeType: String, val mimeType: String,
@ -37,95 +15,7 @@ open class File(
val isDirectory: Boolean, val isDirectory: Boolean,
val metaData: List<Pair<Int, String>> val metaData: List<Pair<Int, String>>
) : Searchable() { ) : Searchable() {
abstract val isStoredInCloud: Boolean
override val label = path.substringAfterLast('/')
override val key = "file://$path"
open val isStoredInCloud = false
override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? {
if (!JavaIOFile(path).exists()) return null
when {
mimeType.startsWith("image/") -> {
val thumbnail = withContext(Dispatchers.IO) {
ThumbnailUtils.extractThumbnail(
BitmapFactory.decodeFile(path),
size, size
)
} ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType.startsWith("video/") -> {
val thumbnail = withContext(Dispatchers.IO) {
ThumbnailUtilsCompat.createVideoThumbnail(
JavaIOFile(path),
Size(size, size)
)
} ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType.startsWith("audio/") -> {
val thumbnail = withContext(Dispatchers.IO) {
val mediaMetadataRetriever = MediaMetadataRetriever()
try {
mediaMetadataRetriever.setDataSource(path)
val thumbData = mediaMetadataRetriever.embeddedPicture
if (thumbData != null) {
val thumbnail = ThumbnailUtils.extractThumbnail(
BitmapFactory.decodeByteArray(thumbData, 0, thumbData.size),
size,
size
)
mediaMetadataRetriever.release()
return@withContext thumbnail
}
} catch (e: RuntimeException) {
}
mediaMetadataRetriever.release()
return@withContext null
}
thumbnail ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
val icon = withContext(Dispatchers.IO) {
pkgInfo?.applicationInfo?.loadIcon(context.packageManager)
} ?: return null
when {
Build.VERSION.SDK_INT > Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
return LauncherIcon(
foreground = icon.foreground,
background = icon.background,
foregroundScale = 1.5f,
backgroundScale = 1.5f
)
}
else -> {
return LauncherIcon(
foreground = icon,
foregroundScale = 0.7f,
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
}
}
}
return null
}
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): LauncherIcon {
val (resId, bgColor) = when { val (resId, bgColor) = when {
@ -162,16 +52,6 @@ open class File(
) )
} }
override fun getLaunchIntent(context: Context): Intent? {
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider", JavaIOFile(path)
)
return Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, mimeType)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
fun getFileType(context: Context): String { fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory) if (isDirectory) return context.getString(R.string.file_type_directory)
val resource = when (mimeType) { val resource = when (mimeType) {
@ -227,186 +107,7 @@ open class File(
return context.getString(resource) return context.getString(resource)
} }
companion object { open val isDeletable: Boolean = false
fun search(context: Context, query: String): List<File> { open suspend fun delete(context: Context) {}
val results = mutableListOf<File>()
if (!LauncherPreferences.instance.searchFiles) return results
if (query.isBlank()) return results
if (!PermissionsManager.checkPermission(
context,
PermissionsManager.EXTERNAL_STORAGE
)
) return results
val uri = MediaStore.Files.getContentUri("external").buildUpon()
.appendQueryParameter("limit", "10").build()
val projection = arrayOf(
MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.MIME_TYPE
)
val selection =
if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?"
val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query)
val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC"
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort)
?: return results
while (cursor.moveToNext()) {
if (results.size >= 10) {
break
}
val path = cursor.getString(3)
if (!JavaIOFile(path).exists()) continue
val directory = JavaIOFile(path).isDirectory
val mimeType = (cursor.getStringOrNull(4)
?: if (directory) "inode/directory" else getMimetypeByFileExtension(
path.substringAfterLast(
'.'
)
))
val file = File(
path = path,
mimeType = mimeType,
size = cursor.getLong(2),
isDirectory = directory,
id = cursor.getLong(1),
metaData = getMetaData(context, mimeType, path)
)
results.add(file)
}
cursor.close()
return results.sortedBy { it }
}
internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) {
"apk" -> "application/vnd.android.package-archive"
"zip" -> "application/zip"
"jar" -> "application/java-archive"
"txt" -> "text/plain"
"js" -> "text/javascript"
"html", "htm" -> "text/html"
"css" -> "text/css"
"gif" -> "image/gif"
"png" -> "image/png"
"jpg", "jpeg" -> "image/jpeg"
"bmp" -> "image/bmp"
"webp" -> "image/webp"
"ico" -> "image/x-icon"
"midi" -> "audio/midi"
"mp3" -> "audio/mpeg3"
"webm" -> "audio/webm"
"ogg" -> "audio/ogg"
"wav" -> "audio/wav"
"mp4" -> "video/mp4"
else -> "application/octet-stream"
}
}
internal fun getMetaData(
context: Context,
mimeType: String,
path: String
): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, 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.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
val height =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
?.toLong() ?: 0
metaData.add(R.string.file_meta_dimensions to "${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)
val loc =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
if (Geocoder.isPresent() && loc != null) {
val lon =
loc.substring(0, loc.lastIndexOfAny(charArrayOf('+', '-')))
.toDouble()
val lat = loc.substring(
loc.lastIndexOfAny(charArrayOf('+', '-')),
loc.indexOf('/')
).toDouble()
val list = Geocoder(context).getFromLocation(lon, lat, 1)
if (list.size > 0) {
metaData.add(R.string.file_meta_location to list[0].formatToString())
}
}
retriever.release()
} catch (e: RuntimeException) {
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")
try {
val exif = ExifInterface(path)
val loc = exif.latLong
if (loc != null && Geocoder.isPresent()) {
val list = Geocoder(context).getFromLocation(loc[0], loc[1], 1)
if (list.size > 0) {
metaData.add(R.string.file_meta_location to 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
}
}
} }

View File

@ -0,0 +1,346 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.location.Geocoder
import android.media.MediaMetadataRetriever
import android.media.ThumbnailUtils
import android.os.Build
import android.provider.MediaStore
import android.text.format.DateUtils
import android.util.Size
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.database.getStringOrNull
import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.util.*
import java.io.File as JavaIOFile
open class LocalFile(
id: Long,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>
) : File(id, path, mimeType, size, isDirectory, metaData) {
override val label = path.substringAfterLast('/')
override val key = "file://$path"
override val isStoredInCloud = false
override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? {
if (!JavaIOFile(path).exists()) return null
when {
mimeType.startsWith("image/") -> {
val thumbnail = withContext(Dispatchers.IO) {
ThumbnailUtils.extractThumbnail(
BitmapFactory.decodeFile(path),
size, size
)
} ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType.startsWith("video/") -> {
val thumbnail = withContext(Dispatchers.IO) {
ThumbnailUtilsCompat.createVideoThumbnail(
JavaIOFile(path),
Size(size, size)
)
} ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType.startsWith("audio/") -> {
val thumbnail = withContext(Dispatchers.IO) {
val mediaMetadataRetriever = MediaMetadataRetriever()
try {
mediaMetadataRetriever.setDataSource(path)
val thumbData = mediaMetadataRetriever.embeddedPicture
if (thumbData != null) {
val thumbnail = ThumbnailUtils.extractThumbnail(
BitmapFactory.decodeByteArray(thumbData, 0, thumbData.size),
size,
size
)
mediaMetadataRetriever.release()
return@withContext thumbnail
}
} catch (e: RuntimeException) {
}
mediaMetadataRetriever.release()
return@withContext null
}
thumbnail ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
val icon = withContext(Dispatchers.IO) {
pkgInfo?.applicationInfo?.loadIcon(context.packageManager)
} ?: return null
when {
Build.VERSION.SDK_INT > Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
return LauncherIcon(
foreground = icon.foreground,
background = icon.background,
foregroundScale = 1.5f,
backgroundScale = 1.5f
)
}
else -> {
return LauncherIcon(
foreground = icon,
foregroundScale = 0.7f,
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
}
}
}
return null
}
override fun getLaunchIntent(context: Context): Intent? {
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider", JavaIOFile(path)
)
return Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, mimeType)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
override val isDeletable: Boolean
get() {
val file = java.io.File(path)
return file.canWrite() && file.parentFile?.canWrite() == true
}
override suspend fun delete(context: Context) {
super.delete(context)
val file = java.io.File(path)
withContext(Dispatchers.IO) {
file.deleteRecursively()
context.contentResolver.delete(
MediaStore.Files.getContentUri("external"),
"${MediaStore.Files.FileColumns._ID} = ?",
arrayOf(id.toString()))
}
}
companion object {
fun search(context: Context, query: String): List<LocalFile> {
val results = mutableListOf<LocalFile>()
if (!LauncherPreferences.instance.searchFiles) return results
if (query.isBlank()) return results
if (!PermissionsManager.checkPermission(
context,
PermissionsManager.EXTERNAL_STORAGE
)
) return results
val uri = MediaStore.Files.getContentUri("external").buildUpon()
.appendQueryParameter("limit", "10").build()
val projection = arrayOf(
MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.MIME_TYPE
)
val selection =
if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?"
val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query)
val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC"
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort)
?: return results
while (cursor.moveToNext()) {
if (results.size >= 10) {
break
}
val path = cursor.getString(3)
if (!JavaIOFile(path).exists()) continue
val directory = JavaIOFile(path).isDirectory
val mimeType = (cursor.getStringOrNull(4)
?: if (directory) "inode/directory" else getMimetypeByFileExtension(
path.substringAfterLast(
'.'
)
))
val file = LocalFile(
path = path,
mimeType = mimeType,
size = cursor.getLong(2),
isDirectory = directory,
id = cursor.getLong(1),
metaData = getMetaData(context, mimeType, path)
)
results.add(file)
}
cursor.close()
return results.sortedBy { it }
}
internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) {
"apk" -> "application/vnd.android.package-archive"
"zip" -> "application/zip"
"jar" -> "application/java-archive"
"txt" -> "text/plain"
"js" -> "text/javascript"
"html", "htm" -> "text/html"
"css" -> "text/css"
"gif" -> "image/gif"
"png" -> "image/png"
"jpg", "jpeg" -> "image/jpeg"
"bmp" -> "image/bmp"
"webp" -> "image/webp"
"ico" -> "image/x-icon"
"midi" -> "audio/midi"
"mp3" -> "audio/mpeg3"
"webm" -> "audio/webm"
"ogg" -> "audio/ogg"
"wav" -> "audio/wav"
"mp4" -> "video/mp4"
else -> "application/octet-stream"
}
}
internal fun getMetaData(
context: Context,
mimeType: String,
path: String
): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, 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.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
val height =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
?.toLong() ?: 0
metaData.add(R.string.file_meta_dimensions to "${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)
val loc =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
if (Geocoder.isPresent() && loc != null) {
val lon =
loc.substring(0, loc.lastIndexOfAny(charArrayOf('+', '-')))
.toDouble()
val lat = loc.substring(
loc.lastIndexOfAny(charArrayOf('+', '-')),
loc.indexOf('/')
).toDouble()
val list = Geocoder(context).getFromLocation(lon, lat, 1)
if (list.size > 0) {
metaData.add(R.string.file_meta_location to list[0].formatToString())
}
}
retriever.release()
} catch (e: RuntimeException) {
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")
try {
val exif = ExifInterface(path)
val loc = exif.latLong
if (loc != null && Geocoder.isPresent()) {
val list = Geocoder(context).getFromLocation(loc[0], loc[1], 1)
if (list.size > 0) {
metaData.add(R.string.file_meta_location to 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
}
}
}

View File

@ -5,10 +5,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.helper.NetworkUtils import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class NextcloudFile( class NextcloudFile(
fileId: Long, fileId: Long,

View File

@ -5,10 +5,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.helper.NetworkUtils import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class OwncloudFile( class OwncloudFile(
fileId: Long, fileId: Long,

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.icons package de.mm20.launcher2.ui.icons
import androidx.compose.material3.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View File

@ -8,7 +8,6 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.files.FilesViewModel import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.data.File

View File

@ -72,11 +72,10 @@ class FileDetailRepresentation : Representation, KoinComponent {
val favAction = FavoriteToolbarAction(context, file) val favAction = FavoriteToolbarAction(context, file)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END) toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val jFile = java.io.File(file.path) if (file.isDeletable) {
if (jFile.canWrite() && jFile.parentFile.canWrite()) {
val deleteAction = ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete)) val deleteAction = ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
deleteAction.clickAction = { deleteAction.clickAction = {
delete(context, file, jFile) delete(context, file)
} }
toolbar.addAction(deleteAction, ToolbarView.PLACEMENT_END) toolbar.addAction(deleteAction, ToolbarView.PLACEMENT_END)
} }
@ -93,21 +92,16 @@ class FileDetailRepresentation : Representation, KoinComponent {
} }
} }
private fun delete(context: Context, file: File, jFile: java.io.File) { private fun delete(context: Context, file: File) {
MaterialDialog(context).show { MaterialDialog(context).show {
message(text = context.getString( message(text = context.getString(
if (file.isDirectory) R.string.alert_delete_directory if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file, else R.string.alert_delete_file,
file.path)) file.path))
positiveButton(android.R.string.yes) { positiveButton(android.R.string.yes) {
Thread { jFile.deleteRecursively() }.start()
context.contentResolver.delete(
MediaStore.Files.getContentUri("external"),
"${MediaStore.Files.FileColumns._ID} = ?",
arrayOf(file.id.toString()))
it.dismiss()
val fileViewModel: FilesViewModel by (context as AppCompatActivity).viewModel() val fileViewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
fileViewModel.removeFile(file) it.dismiss()
fileViewModel.deleteFile(file)
} }
negativeButton(android.R.string.no) { negativeButton(android.R.string.no) {
it.dismiss() it.dismiss()

View File

@ -34,7 +34,7 @@ class FileListRepresentation : Representation, KoinComponent {
scene.setEnterAction { scene.setEnterAction {
with(rootView) { with(rootView) {
findViewById<TextView>(R.id.fileLabel).text = file.label findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = getFileType(context, file) findViewById<TextView>(R.id.fileInfo).text = file.getFileType(context)
findViewById<LauncherIconView>(R.id.icon).apply { findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(file.badgeKey) badge = badgeProvider.getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context) shape = LauncherIconView.getDefaultShape(context)
@ -60,40 +60,4 @@ class FileListRepresentation : Representation, KoinComponent {
} }
return scene return scene
} }
fun getFileType(context: Context, file: File): String {
if (file.isDirectory) return context.getString(R.string.file_type_directory)
val mimeType = file.mimeType
val resource = when (mimeType) {
"application/zip", "application/x-gtar", "application/x-tar",
"application/java-archive", "application/x-7z-compressed" -> R.string.file_type_archive
"application/x-gzip", "application/x-bzip2" -> R.string.file_type_compressed
"application/vnd.android.package-archive" -> R.string.file_type_android
"text/x-asm", "text/x-c", "text/x-java-source", "text/x-script.phyton", "text/x-pascal",
"text/x-script.perl", "text/javascript", "application/json" ->
R.string.file_type_source_code
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword", "application/vnd.google-apps.document" -> R.string.file_type_document
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel", "application/vnd.google-apps.spreadsheet" -> R.string.file_type_spreadsheet
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint", "application/vnd.google-apps.presentation" -> R.string.file_type_presentation
"text/plain" -> R.string.file_type_text
"application/vnd.google-apps.drawing" -> R.string.file_type_drawing
"application/vnd.google-apps.form" -> R.string.file_type_form
else -> when {
mimeType.startsWith("image/") -> R.string.file_type_image
mimeType.startsWith("video/") -> R.string.file_type_video
mimeType.startsWith("audio/") -> R.string.file_type_music
else -> R.string.file_type_none
}
}
if (resource == R.string.file_type_none && file.label.matches(Regex(".+\\..+"))) {
val extension = file.label.substringAfterLast(".").uppercase()
return context.getString(R.string.file_type_generic, extension)
}
return context.getString(resource)
}
} }