Plugins: bringup
This commit is contained in:
@@ -8,12 +8,14 @@ import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import de.mm20.launcher2.database.daos.PluginDao
|
||||
import de.mm20.launcher2.database.daos.ThemeDao
|
||||
import de.mm20.launcher2.database.entities.CurrencyEntity
|
||||
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||
import de.mm20.launcher2.database.entities.ForecastEntity
|
||||
import de.mm20.launcher2.database.entities.IconEntity
|
||||
import de.mm20.launcher2.database.entities.IconPackEntity
|
||||
import de.mm20.launcher2.database.entities.PluginEntity
|
||||
import de.mm20.launcher2.database.entities.SavedSearchableEntity
|
||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
import de.mm20.launcher2.database.entities.ThemeEntity
|
||||
@@ -33,6 +35,7 @@ import de.mm20.launcher2.database.migrations.Migration_21_22
|
||||
import de.mm20.launcher2.database.migrations.Migration_22_23
|
||||
import de.mm20.launcher2.database.migrations.Migration_23_24
|
||||
import de.mm20.launcher2.database.migrations.Migration_24_25
|
||||
import de.mm20.launcher2.database.migrations.Migration_25_26
|
||||
import de.mm20.launcher2.database.migrations.Migration_6_7
|
||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||
@@ -51,7 +54,8 @@ import java.util.UUID
|
||||
CustomAttributeEntity::class,
|
||||
SearchActionEntity::class,
|
||||
ThemeEntity::class,
|
||||
], version = 25, exportSchema = true
|
||||
PluginEntity::class,
|
||||
], version = 26, exportSchema = true
|
||||
)
|
||||
@TypeConverters(ComponentNameConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -69,6 +73,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun themeDao(): ThemeDao
|
||||
|
||||
abstract fun pluginDao(): PluginDao
|
||||
|
||||
companion object {
|
||||
private var _instance: AppDatabase? = null
|
||||
fun getInstance(context: Context): AppDatabase {
|
||||
@@ -147,6 +153,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration_22_23(),
|
||||
Migration_23_24(),
|
||||
Migration_24_25(context),
|
||||
Migration_25_26(),
|
||||
).build()
|
||||
if (_instance == null) _instance = instance
|
||||
return instance
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.mm20.launcher2.database.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import de.mm20.launcher2.database.entities.PluginEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface PluginDao {
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM Plugins WHERE
|
||||
(type = :type OR :type IS NULL) AND
|
||||
(enabled = :enabled OR :enabled IS NULL) AND
|
||||
(packageName = :packageName OR :packageName IS NULL)
|
||||
""")
|
||||
fun findMany(
|
||||
type: String? = null,
|
||||
enabled: Boolean? = null,
|
||||
packageName: String? = null,
|
||||
): Flow<List<PluginEntity>>
|
||||
|
||||
@Query("SELECT * FROM Plugins WHERE authority = :authority")
|
||||
fun get(authority: String): Flow<PluginEntity>
|
||||
|
||||
@Insert
|
||||
fun insertMany(plugins: List<PluginEntity>)
|
||||
|
||||
@Insert
|
||||
fun insert(plugin: PluginEntity)
|
||||
|
||||
@Update
|
||||
fun update(plugin: PluginEntity)
|
||||
|
||||
@Query("DELETE FROM Plugins")
|
||||
fun deleteMany()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.mm20.launcher2.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "Plugins")
|
||||
data class PluginEntity(
|
||||
@PrimaryKey val authority: String,
|
||||
val label: String,
|
||||
val description: String?,
|
||||
val packageName: String,
|
||||
val className: String,
|
||||
val type: String,
|
||||
val settingsActivity: String?,
|
||||
val enabled: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
internal class Migration_25_26 : Migration(25, 26) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("""
|
||||
CREATE TABLE Plugins
|
||||
(
|
||||
authority TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
description TEXT,
|
||||
packageName TEXT NOT NULL,
|
||||
className TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
settingsActivity TEXT,
|
||||
enabled INTEGER NOT NULL,
|
||||
PRIMARY KEY(`authority`)
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,31 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
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.files.providers.PluginFile
|
||||
import de.mm20.launcher2.files.providers.PluginFileProvider
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.FileMetaType
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
@@ -299,4 +308,86 @@ internal class OwncloudFileDeserializer : SearchableDeserializer {
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class PluginFileSerializer(
|
||||
) : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String? {
|
||||
searchable as PluginFile
|
||||
if (searchable.storageStrategy == StorageStrategy.StoreReference) {
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.id,
|
||||
"authority" to searchable.authority,
|
||||
"strategy" to "ref"
|
||||
).toString()
|
||||
} else {
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.id,
|
||||
"path" to searchable.path,
|
||||
"mimeType" to searchable.mimeType,
|
||||
"size" to searchable.size,
|
||||
"label" to searchable.label,
|
||||
"uri" to searchable.uri.toString(),
|
||||
"thumbnailUri" to searchable.thumbnailUri?.toString(),
|
||||
"isDirectory" to searchable.isDirectory,
|
||||
"authority" to searchable.authority,
|
||||
"strategy" to "copy",
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = PluginFile.Domain
|
||||
|
||||
}
|
||||
|
||||
internal class PluginFileDeserializer(
|
||||
private val context: Context,
|
||||
private val pluginRepository: PluginRepository,
|
||||
): SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val jsonObject = JSONObject(serialized)
|
||||
|
||||
return if (jsonObject.optString("strategy", "ref") == "ref") {
|
||||
getByRef(jsonObject)
|
||||
} else {
|
||||
getByCopy(jsonObject)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getByRef(obj: JSONObject): File? {
|
||||
try {
|
||||
val authority = obj.getString("authority")
|
||||
val id = obj.getString("id")
|
||||
val plugin = pluginRepository.get(authority).firstOrNull() ?: return null
|
||||
val provider = PluginFileProvider(context, plugin)
|
||||
return provider.getFile(id)
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
private fun getByCopy(obj: JSONObject): File? {
|
||||
try {
|
||||
val uri = obj.getString("uri")
|
||||
val thumbnailUri = obj.optString("thumbnailUri")
|
||||
return PluginFile(
|
||||
id = obj.getString("id"),
|
||||
path = obj.getString("path"),
|
||||
mimeType = obj.getString("mimeType"),
|
||||
size = obj.optLong("size", 0L),
|
||||
metaData = persistentMapOf(),
|
||||
label = obj.getString("label"),
|
||||
uri = Uri.parse(uri),
|
||||
thumbnailUri = thumbnailUri.takeIf { it.isNotEmpty() }?.let { Uri.parse(it) },
|
||||
storageStrategy = StorageStrategy.StoreCopy,
|
||||
isDirectory = obj.optBoolean("isDirectory", false),
|
||||
authority = obj.getString("authority"),
|
||||
)
|
||||
} catch (e: JSONException) {
|
||||
CrashReporter.logException(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import de.mm20.launcher2.files.providers.GDriveFileProvider
|
||||
import de.mm20.launcher2.files.providers.LocalFileProvider
|
||||
import de.mm20.launcher2.files.providers.NextcloudFileProvider
|
||||
import de.mm20.launcher2.files.providers.OwncloudFileProvider
|
||||
import de.mm20.launcher2.files.providers.PluginFileProvider
|
||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -19,16 +22,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class FileRepository(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val dataStore: LauncherDataStore,
|
||||
private val pluginRepository: PluginRepository,
|
||||
) : SearchableRepository<File> {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
private val nextcloudClient by lazy {
|
||||
NextcloudApiHelper(context)
|
||||
}
|
||||
@@ -44,23 +47,40 @@ internal class FileRepository(
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
dataStore.data.map { it.fileSearch }.collectLatest {
|
||||
val providers = mutableListOf<FileProvider>()
|
||||
val filePlugins = pluginRepository.findMany(
|
||||
type = PluginType.FileSearch,
|
||||
enabled = true,
|
||||
)
|
||||
|
||||
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))
|
||||
dataStore.data.map { it.fileSearch }
|
||||
.combine(filePlugins) { settings, plugins ->
|
||||
settings to plugins
|
||||
}.collectLatest { (settings, plugins) ->
|
||||
val providers = mutableListOf<FileProvider>()
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
send(persistentListOf())
|
||||
return@collectLatest
|
||||
if (settings.localFiles) providers.add(
|
||||
LocalFileProvider(
|
||||
context,
|
||||
permissionsManager
|
||||
)
|
||||
)
|
||||
if (settings.gdrive) providers.add(GDriveFileProvider(context))
|
||||
if (settings.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
|
||||
if (settings.owncloud) providers.add(OwncloudFileProvider(owncloudClient))
|
||||
|
||||
for (plugin in plugins) {
|
||||
providers.add(PluginFileProvider(context, plugin))
|
||||
}
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
send(persistentListOf())
|
||||
return@collectLatest
|
||||
}
|
||||
val results = mutableListOf<File>()
|
||||
for (provider in providers) {
|
||||
results.addAll(provider.search(query))
|
||||
send(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
val results = mutableListOf<File>()
|
||||
for (provider in providers) {
|
||||
results.addAll(provider.search(query))
|
||||
send(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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.files.providers.PluginFile
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -13,10 +14,11 @@ import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val filesModule = module {
|
||||
factory<SearchableRepository<File>>(named<File>()) { FileRepository(androidContext(), get(), get()) }
|
||||
factory<SearchableRepository<File>>(named<File>()) { FileRepository(androidContext(), get(), 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() }
|
||||
factory<SearchableDeserializer>(named(PluginFile.Domain)) { PluginFileDeserializer(androidContext(), get()) }
|
||||
}
|
||||
@@ -33,8 +33,6 @@ internal data class GDriveFile(
|
||||
|
||||
override val key: String = "$domain://$fileId"
|
||||
|
||||
override val isStoredInCloud = true
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_gdrive
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
|
||||
@@ -56,8 +56,6 @@ internal data class LocalFile(
|
||||
|
||||
override val key = "$domain://$path"
|
||||
|
||||
override val isStoredInCloud = false
|
||||
|
||||
override suspend fun loadIcon(
|
||||
context: Context,
|
||||
size: Int,
|
||||
|
||||
@@ -33,9 +33,6 @@ internal data class NextcloudFile(
|
||||
|
||||
override val key: String = "$domain://$server/$fileId"
|
||||
|
||||
override val isStoredInCloud: Boolean
|
||||
get() = true
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_nextcloud
|
||||
|
||||
private fun getLaunchIntent(context: Context): Intent {
|
||||
|
||||
@@ -34,8 +34,6 @@ internal data class OneDriveFile(
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_onedrive
|
||||
|
||||
override val isStoredInCloud = true
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(webUrl)
|
||||
|
||||
@@ -9,6 +9,7 @@ import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.FileMetaType
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@@ -32,9 +33,6 @@ internal data class OwncloudFile(
|
||||
|
||||
override val key: String = "$domain://$server/$fileId"
|
||||
|
||||
override val isStoredInCloud: Boolean
|
||||
get() = true
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_owncloud
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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.PluginFileSerializer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.FileMetaType
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
data class PluginFile(
|
||||
val id: String,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
override val size: Long,
|
||||
override val metaData: ImmutableMap<FileMetaType, String>,
|
||||
override val label: String,
|
||||
override val isDirectory: Boolean,
|
||||
val uri: Uri,
|
||||
val thumbnailUri: Uri?,
|
||||
val authority: String,
|
||||
internal val storageStrategy: StorageStrategy,
|
||||
override val labelOverride: String? = null,
|
||||
) : File {
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String
|
||||
get() = "$domain://$authority:$id"
|
||||
|
||||
override fun overrideLabel(label: String): SavableSearchable {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||
data = uri
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}, options)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return PluginFileSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "plugin.file"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.CancellationSignal
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.plugin.Plugin
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.plugin.contracts.FilePluginContract
|
||||
import de.mm20.launcher2.plugin.contracts.SearchPluginContract
|
||||
import de.mm20.launcher2.search.File
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class PluginFileProvider(
|
||||
private val context: Context,
|
||||
private val plugin: Plugin,
|
||||
) : FileProvider {
|
||||
override suspend fun search(query: String): List<File> {
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(plugin.authority)
|
||||
.path(SearchPluginContract.Paths.Search)
|
||||
.appendQueryParameter(SearchPluginContract.Paths.QueryParam, query)
|
||||
.build()
|
||||
val cancellationSignal = CancellationSignal()
|
||||
|
||||
return suspendCancellableCoroutine {
|
||||
it.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
}
|
||||
val cursor = context.contentResolver.query(
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
cancellationSignal
|
||||
) ?: return@suspendCancellableCoroutine it.resume(emptyList<File>())
|
||||
|
||||
val results = fromCursor(cursor) ?: emptyList()
|
||||
it.resume(results)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFile(id: String): File? {
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(plugin.authority)
|
||||
.path(SearchPluginContract.Paths.Root)
|
||||
.appendPath(id)
|
||||
.build()
|
||||
val cancellationSignal = CancellationSignal()
|
||||
|
||||
return suspendCancellableCoroutine {
|
||||
it.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
}
|
||||
val cursor = context.contentResolver.query(
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
cancellationSignal
|
||||
) ?: return@suspendCancellableCoroutine it.resume(null)
|
||||
|
||||
val results = fromCursor(cursor)
|
||||
it.resume(results?.firstOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromCursor(cursor: Cursor): List<File>? {
|
||||
val idIndex = cursor
|
||||
.getColumnIndex(FilePluginContract.FileColumns.Id)
|
||||
.takeIf { it >= 0 }
|
||||
?: return null
|
||||
val pathIndex =
|
||||
cursor.getColumnIndex(FilePluginContract.FileColumns.Path).takeIf { it >= 0 }
|
||||
val typeIndex =
|
||||
cursor.getColumnIndex(FilePluginContract.FileColumns.MimeType).takeIf { it >= 0 }
|
||||
val sizeIndex =
|
||||
cursor.getColumnIndex(FilePluginContract.FileColumns.Size).takeIf { it >= 0 }
|
||||
val nameIndex = cursor.getColumnIndex(FilePluginContract.FileColumns.DisplayName)
|
||||
.takeIf { it >= 0 }
|
||||
?: return null
|
||||
val contentUriIndex = cursor.getColumnIndex(FilePluginContract.FileColumns.ContentUri)
|
||||
.takeIf { it >= 0 }
|
||||
?: return null
|
||||
val thumbnailUriIndex =
|
||||
cursor.getColumnIndex(FilePluginContract.FileColumns.ThumbnailUri)
|
||||
.takeIf { it >= 0 }
|
||||
val storageStrategyIndex =
|
||||
cursor.getColumnIndex(FilePluginContract.FileColumns.StorageStrategy)
|
||||
.takeIf { it >= 0 }
|
||||
val directoryIndex =
|
||||
cursor.getColumnIndex(FilePluginContract.FileColumns.IsDirectory).takeIf { it >= 0 }
|
||||
|
||||
val results = mutableListOf<File>()
|
||||
while (cursor.moveToNext()) {
|
||||
results.add(
|
||||
PluginFile(
|
||||
id = cursor.getString(idIndex),
|
||||
path = pathIndex?.let { cursor.getString(it) } ?: "",
|
||||
mimeType = typeIndex?.let { cursor.getString(it) }
|
||||
?: "application/octet-stream",
|
||||
size = sizeIndex?.let { cursor.getLong(it) } ?: 0,
|
||||
metaData = persistentMapOf(),
|
||||
label = cursor.getString(nameIndex),
|
||||
uri = Uri.parse(cursor.getString(contentUriIndex)),
|
||||
thumbnailUri = thumbnailUriIndex?.let {
|
||||
cursor.getStringOrNull(it)
|
||||
}?.let { Uri.parse(it) },
|
||||
storageStrategy = try {
|
||||
storageStrategyIndex?.let {
|
||||
StorageStrategy.valueOf(cursor.getString(it))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
} ?: StorageStrategy.StoreCopy,
|
||||
isDirectory = directoryIndex?.let { cursor.getInt(it) } == 1,
|
||||
authority = plugin.authority,
|
||||
)
|
||||
)
|
||||
}
|
||||
cursor.close()
|
||||
return results
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.data.plugins"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":data:database"))
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.mm20.launcher2.data.plugins
|
||||
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
val dataPluginsModule = module {
|
||||
factory<PluginRepository> { PluginRepositoryImpl(get<AppDatabase>().pluginDao()) }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.mm20.launcher2.data.plugins
|
||||
|
||||
import de.mm20.launcher2.database.entities.PluginEntity
|
||||
import de.mm20.launcher2.plugin.Plugin
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
|
||||
internal fun Plugin(entity: PluginEntity): Plugin? {
|
||||
return Plugin(
|
||||
enabled = entity.enabled,
|
||||
label = entity.label,
|
||||
description = entity.description,
|
||||
settingsActivity = entity.settingsActivity,
|
||||
packageName = entity.packageName,
|
||||
className = entity.className,
|
||||
type = try {
|
||||
PluginType.valueOf(entity.type)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return null
|
||||
},
|
||||
authority = entity.authority,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun PluginEntity(plugin: Plugin): PluginEntity {
|
||||
return PluginEntity(
|
||||
enabled = plugin.enabled,
|
||||
label = plugin.label,
|
||||
description = plugin.description,
|
||||
settingsActivity = plugin.settingsActivity,
|
||||
packageName = plugin.packageName,
|
||||
className = plugin.className,
|
||||
type = plugin.type.name,
|
||||
authority = plugin.authority,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.mm20.launcher2.data.plugins
|
||||
|
||||
import de.mm20.launcher2.database.daos.PluginDao
|
||||
import de.mm20.launcher2.plugin.Plugin
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class PluginRepositoryImpl(
|
||||
private val dao: PluginDao,
|
||||
): PluginRepository {
|
||||
override fun findMany(
|
||||
type: PluginType?,
|
||||
enabled: Boolean?,
|
||||
packageName: String?
|
||||
): Flow<List<Plugin>> {
|
||||
return dao.findMany(
|
||||
type = type?.name,
|
||||
enabled = enabled,
|
||||
packageName = packageName,
|
||||
).map {
|
||||
it.mapNotNull { Plugin(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(authority: String): Flow<Plugin?> {
|
||||
return dao.get(authority).map { Plugin(it) }
|
||||
}
|
||||
|
||||
override fun insertMany(plugins: List<Plugin>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun insert(plugin: Plugin) {
|
||||
dao.insert(PluginEntity(plugin))
|
||||
}
|
||||
|
||||
override fun update(plugin: Plugin) {
|
||||
dao.update(PluginEntity(plugin))
|
||||
}
|
||||
|
||||
override fun deleteMany() {
|
||||
dao.deleteMany()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user