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) {
return@factory NextcloudFileSerializer()
}
if (searchable is File) {
return@factory FileSerializer()
if (searchable is LocalFile) {
return@factory LocalFileSerializer()
}
if (searchable is Website) {
return@factory WebsiteSerializer()
@ -83,7 +83,7 @@ val favoritesModule = module {
return@factory OwncloudFileDeserializer()
}
if (type == "file") {
return@factory FileDeserializer(androidContext())
return@factory LocalFileDeserializer(androidContext())
}
if (type == "website") {
return@factory WebsiteDeserializer()

View File

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

View File

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

View File

@ -1,35 +1,13 @@
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 File(
abstract class File(
val id: Long,
val path: String,
val mimeType: String,
@ -37,95 +15,7 @@ open class File(
val isDirectory: Boolean,
val metaData: List<Pair<Int, String>>
) : Searchable() {
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
}
abstract val isStoredInCloud: Boolean
override fun getPlaceholderIcon(context: Context): LauncherIcon {
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 {
if (isDirectory) return context.getString(R.string.file_type_directory)
val resource = when (mimeType) {
@ -227,186 +107,7 @@ open class File(
return context.getString(resource)
}
companion object {
fun search(context: Context, query: String): List<File> {
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"
open val isDeletable: Boolean = false
open suspend fun delete(context: Context) {}
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 de.mm20.launcher2.files.R
import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class NextcloudFile(
fileId: Long,

View File

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

View File

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

View File

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

View File

@ -72,11 +72,10 @@ class FileDetailRepresentation : Representation, KoinComponent {
val favAction = FavoriteToolbarAction(context, file)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val jFile = java.io.File(file.path)
if (jFile.canWrite() && jFile.parentFile.canWrite()) {
if (file.isDeletable) {
val deleteAction = ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
deleteAction.clickAction = {
delete(context, file, jFile)
delete(context, file)
}
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 {
message(text = context.getString(
if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file,
file.path))
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()
fileViewModel.removeFile(file)
it.dismiss()
fileViewModel.deleteFile(file)
}
negativeButton(android.R.string.no) {
it.dismiss()

View File

@ -34,7 +34,7 @@ class FileListRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
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 {
badge = badgeProvider.getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
@ -60,40 +60,4 @@ class FileListRepresentation : Representation, KoinComponent {
}
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)
}
}