From 183ba586b395c3bd0aa5fc02a24d5a9900218486 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:12:51 +0100 Subject: [PATCH] Refactor files --- .../de/mm20/launcher2/favorites/Module.kt | 6 +- .../mm20/launcher2/files/FileSerialization.kt | 12 +- .../mm20/launcher2/files/FilesRepository.kt | 13 +- .../de/mm20/launcher2/files/FilesViewModel.kt | 4 +- .../de/mm20/launcher2/search/data/File.kt | 307 +--------------- .../mm20/launcher2/search/data/LocalFile.kt | 346 ++++++++++++++++++ .../launcher2/search/data/NextcloudFile.kt | 2 - .../launcher2/search/data/OwncloudFile.kt | 2 - .../de/mm20/launcher2/ui/icons/Searchable.kt | 1 - .../launcher2/ui/legacy/component/FileView.kt | 1 - .../legacy/search/FileDetailRepresentation.kt | 16 +- .../legacy/search/FileListRepresentation.kt | 38 +- 12 files changed, 377 insertions(+), 371 deletions(-) create mode 100644 files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt index 6c824dd3..4d85e7a5 100644 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt @@ -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() diff --git a/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt b/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt index bde44d72..9aa6e69a 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt @@ -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() diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt index 38c4953b..9f8a9c1f 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -15,6 +15,8 @@ class FilesRepository( hiddenItemsRepository: HiddenItemsRepository ) : BaseSearchableRepository() { + private val scope = CoroutineScope(Job() + Dispatchers.Main) + val files = MediatorLiveData?>() private val allFiles = MutableLiveData?>(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 } + } + } } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt b/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt index a2931b80..e9585585 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt @@ -10,7 +10,7 @@ class FilesViewModel( val files = filesRepository.files - fun removeFile(file: File) { - filesRepository.removeFile(file) + fun deleteFile(file: File) { + filesRepository.deleteFile(file) } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/File.kt b/files/src/main/java/de/mm20/launcher2/search/data/File.kt index a2d05b1d..2c7e7ff9 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/File.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/File.kt @@ -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> ) : 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 { - val results = mutableListOf() - 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> { - val metaData = mutableListOf>() - 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 - } - } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt new file mode 100644 index 00000000..8d2e7e38 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt @@ -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> +) : 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 { + val results = mutableListOf() + 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> { + val metaData = mutableListOf>() + 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 + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt index 177418a5..cddeffbd 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt @@ -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, diff --git a/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt index 8e47a2c6..f0f5fcfc 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt @@ -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, diff --git a/ui/src/main/java/de/mm20/launcher2/ui/icons/Searchable.kt b/ui/src/main/java/de/mm20/launcher2/ui/icons/Searchable.kt index 8027a140..d8cb1432 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/icons/Searchable.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/icons/Searchable.kt @@ -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 diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt index d20971c5..72b70182 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt @@ -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 diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileDetailRepresentation.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileDetailRepresentation.kt index 91062b6a..40853332 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileDetailRepresentation.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileDetailRepresentation.kt @@ -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() diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileListRepresentation.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileListRepresentation.kt index dcb0cebd..057b7af0 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileListRepresentation.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/FileListRepresentation.kt @@ -34,7 +34,7 @@ class FileListRepresentation : Representation, KoinComponent { scene.setEnterAction { with(rootView) { findViewById(R.id.fileLabel).text = file.label - findViewById(R.id.fileInfo).text = getFileType(context, file) + findViewById(R.id.fileInfo).text = file.getFileType(context) findViewById(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) - } } \ No newline at end of file