Annual refactor

This commit is contained in:
MM20
2023-10-28 14:20:16 +02:00
parent 4f64652aae
commit 957358c79a
110 changed files with 1786 additions and 1663 deletions
+1
View File
@@ -51,4 +51,5 @@ dependencies {
implementation(project(":core:i18n"))
implementation(project(":core:permissions"))
implementation(project(":core:crashreporter"))
implementation(project(":core:preferences"))
}
@@ -3,18 +3,22 @@ package de.mm20.launcher2.files
import android.content.Context
import android.provider.MediaStore
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.files.providers.GDriveFile
import de.mm20.launcher2.files.providers.LocalFile
import de.mm20.launcher2.files.providers.NextcloudFile
import de.mm20.launcher2.files.providers.OneDriveFile
import de.mm20.launcher2.files.providers.OwncloudFile
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.*
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class LocalFileSerializer : SearchableSerializer {
internal class LocalFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as LocalFile
return jsonObjectOf(
@@ -26,10 +30,10 @@ class LocalFileSerializer : SearchableSerializer {
get() = "file"
}
class LocalFileDeserializer(
internal class LocalFileDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): SavableSearchable? {
override suspend fun deserialize(serialized: String): SavableSearchable? {
val permissionsManager: PermissionsManager = get()
if (!permissionsManager.checkPermissionOnce(
PermissionGroup.ExternalStorage
@@ -73,7 +77,7 @@ class LocalFileDeserializer(
}
}
class GDriveFileSerializer : SearchableSerializer {
internal class GDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as GDriveFile
return jsonObjectOf(
@@ -102,8 +106,8 @@ class GDriveFileSerializer : SearchableSerializer {
get() = "gdrive"
}
class GDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable {
internal class GDriveFileDeserializer : SearchableDeserializer {
override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized)
val id = json.getString("id")
val label = json.getString("label")
@@ -133,7 +137,7 @@ class GDriveFileDeserializer : SearchableDeserializer {
}
}
class OneDriveFileSerializer : SearchableSerializer {
internal class OneDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as OneDriveFile
return jsonObjectOf(
@@ -160,8 +164,8 @@ class OneDriveFileSerializer : SearchableSerializer {
get() = "onedrive"
}
class OneDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable {
internal class OneDriveFileDeserializer : SearchableDeserializer {
override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized)
val fileId = json.getString("id")
val label = json.getString("label")
@@ -188,7 +192,7 @@ class OneDriveFileDeserializer : SearchableDeserializer {
}
}
class NextcloudFileSerializer : SearchableSerializer {
internal class NextcloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as NextcloudFile
return jsonObjectOf(
@@ -215,8 +219,8 @@ class NextcloudFileSerializer : SearchableSerializer {
get() = "nextcloud"
}
class NextcloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable {
internal class NextcloudFileDeserializer : SearchableDeserializer {
override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
@@ -241,7 +245,7 @@ class NextcloudFileDeserializer : SearchableDeserializer {
}
}
class OwncloudFileSerializer : SearchableSerializer {
internal class OwncloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as OwncloudFile
return jsonObjectOf(
@@ -268,8 +272,8 @@ class OwncloudFileSerializer : SearchableSerializer {
get() = "owncloud"
}
class OwncloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable {
internal class OwncloudFileDeserializer : SearchableDeserializer {
override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
@@ -9,34 +9,23 @@ import de.mm20.launcher2.files.providers.OwncloudFileProvider
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File
import kotlinx.collections.immutable.ImmutableList
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
interface FileRepository {
fun search(
query: String,
local: Boolean = true,
gdrive: Boolean = true,
onedrive: Boolean = true,
nextcloud: Boolean = true,
owncloud: Boolean = true,
): Flow<ImmutableList<File>>
fun deleteFile(file: File)
}
internal class FileRepositoryImpl(
internal class FileRepository(
private val context: Context,
private val permissionsManager: PermissionsManager,
) : FileRepository {
private val dataStore: LauncherDataStore,
) : SearchableRepository<File> {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
@@ -49,39 +38,28 @@ internal class FileRepositoryImpl(
override fun search(
query: String,
local: Boolean,
gdrive: Boolean,
onedrive: Boolean,
nextcloud: Boolean,
owncloud: Boolean
) = channelFlow {
if (query.isBlank()) {
send(persistentListOf())
return@channelFlow
}
val providers = mutableListOf<FileProvider>()
dataStore.data.map { it.fileSearch }.collectLatest {
val providers = mutableListOf<FileProvider>()
if (local) providers.add(LocalFileProvider(context, permissionsManager))
if (gdrive) providers.add(GDriveFileProvider(context))
if (nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
if (owncloud) providers.add(OwncloudFileProvider(owncloudClient))
if (it.localFiles) providers.add(LocalFileProvider(context, permissionsManager))
if (it.gdrive) providers.add(GDriveFileProvider(context))
if (it.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
if (it.owncloud) providers.add(OwncloudFileProvider(owncloudClient))
if (providers.isEmpty()) {
send(persistentListOf())
return@channelFlow
}
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toImmutableList())
}
}
override fun deleteFile(file: File) {
scope.launch {
if (file.isDeletable) {
file.delete(context)
if (providers.isEmpty()) {
send(persistentListOf())
return@collectLatest
}
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toImmutableList())
}
}
}
@@ -1,8 +1,22 @@
package de.mm20.launcher2.files
import de.mm20.launcher2.files.providers.GDriveFile
import de.mm20.launcher2.files.providers.LocalFile
import de.mm20.launcher2.files.providers.NextcloudFile
import de.mm20.launcher2.files.providers.OneDriveFile
import de.mm20.launcher2.files.providers.OwncloudFile
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
val filesModule = module {
single<FileRepository> { FileRepositoryImpl(androidContext(), get()) }
factory<SearchableRepository<File>>(named<File>()) { FileRepository(androidContext(), get(), get()) }
factory<SearchableDeserializer>(named(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() }
factory<SearchableDeserializer>(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() }
factory<SearchableDeserializer>(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() }
factory<SearchableDeserializer>(named(GDriveFile.Domain)) { GDriveFileDeserializer() }
}
@@ -1,7 +1,7 @@
package de.mm20.launcher2.files.providers
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.File
interface FileProvider {
internal interface FileProvider {
suspend fun search(query: String): List<File>
}
@@ -1,13 +1,16 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.GDriveFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableSerializer
data class GDriveFile(
internal data class GDriveFile(
val fileId: String,
override val label: String,
override val path: String,
@@ -43,6 +46,10 @@ data class GDriveFile(
return context.tryStartActivity(getLaunchIntent(), options)
}
override fun getSerializer(): SearchableSerializer {
return GDriveFileSerializer()
}
companion object {
const val Domain = "gdrive"
}
@@ -4,8 +4,7 @@ import android.content.Context
import de.mm20.launcher2.files.R
import de.mm20.launcher2.gservices.DriveFileMeta
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.GDriveFile
import de.mm20.launcher2.search.File
internal class GDriveFileProvider(
private val context: Context
@@ -1,4 +1,4 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
@@ -16,17 +16,21 @@ import android.util.Size
import androidx.core.content.FileProvider
import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.files.LocalFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.File as JavaIOFile
data class LocalFile(
internal data class LocalFile(
val id: Long,
override val path: String,
override val mimeType: String,
@@ -186,7 +190,7 @@ data class LocalFile(
val file = java.io.File(path)
withContext(Dispatchers.IO) {
withContext(NonCancellable + Dispatchers.IO) {
file.deleteRecursively()
context.contentResolver.delete(
@@ -352,4 +356,8 @@ data class LocalFile(
shareIntent.type = mimeType
context.startActivity(Intent.createChooser(shareIntent, null))
}
override fun getSerializer(): SearchableSerializer {
return LocalFileSerializer()
}
}
@@ -1,13 +1,11 @@
package de.mm20.launcher2.files.providers
import android.content.Context
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.LocalFile
import de.mm20.launcher2.search.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -1,14 +1,17 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.NextcloudFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableSerializer
data class NextcloudFile(
internal data class NextcloudFile(
val fileId: Long,
override val label: String,
override val path: String,
@@ -45,6 +48,10 @@ data class NextcloudFile(
return context.tryStartActivity(getLaunchIntent(context), options)
}
override fun getSerializer(): SearchableSerializer {
return NextcloudFileSerializer()
}
companion object {
const val Domain = "nextcloud"
@@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.files.R
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.NextcloudFile
import de.mm20.launcher2.search.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.min
@@ -1,13 +1,16 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.OneDriveFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableSerializer
data class OneDriveFile(
internal data class OneDriveFile(
val fileId: String,
override val label: String,
override val path: String,
@@ -42,6 +45,10 @@ data class OneDriveFile(
return context.tryStartActivity(getLaunchIntent(), options)
}
override fun getSerializer(): SearchableSerializer {
return OneDriveFileSerializer()
}
companion object {
const val Domain = "onedrive"
}
@@ -1,13 +1,16 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.OwncloudFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableSerializer
data class OwncloudFile(
internal data class OwncloudFile(
val fileId: Long,
override val label: String,
override val path: String,
@@ -42,6 +45,10 @@ data class OwncloudFile(
return context.tryStartActivity(getLaunchIntent(), options)
}
override fun getSerializer(): SearchableSerializer {
return OwncloudFileSerializer()
}
companion object {
const val Domain = "owncloud"
}
@@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.files.R
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.OwncloudFile
import de.mm20.launcher2.search.File
internal class OwncloudFileProvider(
private val owncloudClient: OwncloudClient
@@ -1,143 +0,0 @@
package de.mm20.launcher2.search.data
import android.content.Context
import androidx.core.content.ContextCompat
import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.SavableSearchable
import java.util.*
interface File : SavableSearchable {
val path: String
val mimeType: String
val size: Long
val isDirectory: Boolean
val metaData: List<Pair<Int, String>>
val isStoredInCloud: Boolean
override val preferDetailsOverLaunch: Boolean
get() = false
open val providerIconRes: Int?
get() = null
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when {
isDirectory -> R.drawable.ic_file_folder to R.color.lightblue
mimeType.startsWith("image/") -> R.drawable.ic_file_picture to R.color.teal
mimeType.startsWith("audio/") -> R.drawable.ic_file_music to R.color.orange
mimeType.startsWith("video/") -> R.drawable.ic_file_video to R.color.purple
else -> when (mimeType) {
"application/zip", "application/x-gtar", "application/x-tar",
"application/java-archive", "application/x-7z-compressed",
"application/x-compressed-tar", "application/x-gzip", "application/x-bzip2" -> R.drawable.ic_file_archive to R.color.brown
"application/pdf" -> R.drawable.ic_file_pdf to R.color.red
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword", "text/plain", "application/vnd.google-apps.document" -> R.drawable.ic_file_document to R.color.blue
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel", "application/vnd.google-apps.spreadsheet" -> R.drawable.ic_file_spreadsheet to R.color.green
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint", "application/vnd.google-apps.presentation" -> R.drawable.ic_file_presentation to R.color.amber
"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.drawable.ic_file_code to R.color.pink
"text/xml", "text/html" -> R.drawable.ic_file_markup to R.color.deeporange
"application/vnd.android.package-archive" -> R.drawable.ic_file_android to R.color.lightgreen
"application/vnd.google-apps.form" -> R.drawable.ic_file_form to R.color.deeppurple
"application/vnd.google-apps.drawing" -> R.drawable.ic_file_picture to R.color.teal
"application/vnd.de.mm20.launcher2.backup" -> R.drawable.ic_file_backup to R.color.brown
"application/vnd.de.mm20.launcher2.theme" -> R.drawable.ic_file_theme to R.color.amber
else -> R.drawable.ic_file_generic to R.color.bluegrey
}
}
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, resId)!!,
scale = 0.5f,
color = ContextCompat.getColor(context, bgColor)
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, bgColor))
)
}
fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory)
if (mimeType == "application/vnd.de.mm20.launcher2.backup") {
return context.getString(
R.string.file_type_launcherbackup,
context.getString(R.string.app_name)
)
}
if (mimeType == "application/vnd.de.mm20.launcher2.theme") {
return context.getString(
R.string.file_type_launchertheme,
context.getString(R.string.app_name)
)
}
val resource = when (mimeType) {
"application/zip",
"application/x-zip-compressed",
"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/x-iwork-pages-sffpages",
"application/vnd.apple.pages",
"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/x-iwork-numbers-sffnumbers",
"application/vnd.apple.numbers",
"application/vnd.google-apps.spreadsheet" -> R.string.file_type_spreadsheet
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint",
"application/x-iwork-keynote-sffkey",
"application/vnd.apple.keynote",
"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
"application/epub+zip" -> R.string.file_type_ebook
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 && label.matches(Regex(".+\\..+"))) {
val extension = label.substringAfterLast(".").uppercase(Locale.getDefault())
return context.getString(R.string.file_type_generic, extension)
}
return context.getString(resource)
}
val isDeletable: Boolean
get() = false
val canShare: Boolean
get() = false
fun share(context: Context) {}
suspend fun delete(context: Context) {}
}