From e7ae751340cac40c430d3f2f2a1dd4f2621aaac0 Mon Sep 17 00:00:00 2001
From: MM20 <15646950+MM2-0@users.noreply.github.com>
Date: Mon, 6 Nov 2023 19:30:41 +0100
Subject: [PATCH] Add plugin settings screen
---
app/app/src/release/AndroidManifest.xml | 8 +-
app/ui/build.gradle.kts | 1 +
.../launcher2/ui/component/LargeMessage.kt | 9 +-
.../launcher2/ui/settings/SettingsActivity.kt | 39 +++---
.../ui/settings/main/MainSettingsScreen.kt | 9 ++
.../settings/plugins/PluginsSettingsScreen.kt | 119 ++++++++++++++++++
.../plugins/PluginsSettingsScreenVM.kt | 42 +++++++
.../mm20/launcher2/plugin/PluginRepository.kt | 10 +-
core/i18n/src/main/res/values/strings.xml | 5 +
.../permissions/PermissionsManager.kt | 22 +++-
.../mm20/launcher2/database/daos/PluginDao.kt | 13 +-
data/files/build.gradle.kts | 1 +
.../launcher2/files/providers/PluginFile.kt | 22 ++++
.../files/providers/PluginFileProvider.kt | 9 +-
.../data/plugins/PluginRepositoryImpl.kt | 32 +++--
.../launcher2/sdk/base/BasePluginProvider.kt | 9 ++
.../sdk/base/SearchPluginProvider.kt | 12 +-
.../mm20/launcher2/sdk/files/FileProvider.kt | 7 +-
.../mm20/launcher2/plugins/PluginScanner.kt | 69 +++++-----
.../mm20/launcher2/plugins/PluginService.kt | 45 ++++++-
20 files changed, 398 insertions(+), 85 deletions(-)
create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreen.kt
create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreenVM.kt
diff --git a/app/app/src/release/AndroidManifest.xml b/app/app/src/release/AndroidManifest.xml
index 84b2faa0..c50fdf00 100644
--- a/app/app/src/release/AndroidManifest.xml
+++ b/app/app/src/release/AndroidManifest.xml
@@ -1,9 +1,15 @@
+
diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts
index 6a95b8bd..32086072 100644
--- a/app/ui/build.gradle.kts
+++ b/app/ui/build.gradle.kts
@@ -143,6 +143,7 @@ dependencies {
implementation(project(":libs:g-services"))
implementation(project(":libs:owncloud"))
implementation(project(":services:accounts"))
+ implementation(project(":services:plugins"))
implementation(project(":services:backup"))
implementation(project(":data:search-actions"))
implementation(project(":services:global-actions"))
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/LargeMessage.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/LargeMessage.kt
index a7de8300..c0e0fae7 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/LargeMessage.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/LargeMessage.kt
@@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -18,7 +20,8 @@ import androidx.compose.ui.unit.dp
fun LargeMessage(
modifier: Modifier = Modifier,
icon: ImageVector,
- text: String
+ text: String,
+ color: Color = LocalContentColor.current
) {
Column(
modifier = modifier,
@@ -30,12 +33,14 @@ fun LargeMessage(
contentDescription = null,
modifier = Modifier
.padding(bottom = 24.dp)
- .size(64.dp)
+ .size(64.dp),
+ tint = color
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
+ color = color
)
}
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
index ff5ada3f..fed13cdd 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
@@ -2,21 +2,23 @@ package de.mm20.launcher2.ui.settings
import android.os.Bundle
import androidx.activity.compose.setContent
-import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.view.WindowCompat
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
-import com.google.accompanist.navigation.animation.AnimatedNavHost
-import com.google.accompanist.navigation.animation.composable
-import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import de.mm20.launcher2.licenses.AppLicense
import de.mm20.launcher2.licenses.OpenSourceLicenses
-import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.locals.LocalNavController
@@ -45,6 +47,7 @@ import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.log.LogScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
import de.mm20.launcher2.ui.settings.media.MediaIntegrationSettingsScreen
+import de.mm20.launcher2.ui.settings.plugins.PluginsSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
@@ -53,20 +56,17 @@ import de.mm20.launcher2.ui.settings.weather.WeatherIntegrationSettingsScreen
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
import de.mm20.launcher2.ui.theme.LauncherTheme
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
-import org.koin.android.ext.android.inject
import java.net.URLDecoder
import java.util.UUID
class SettingsActivity : BaseActivity() {
- private val dataStore: LauncherDataStore by inject()
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
- val navController = rememberAnimatedNavController()
+ val navController = rememberNavController()
LaunchedEffect(intent) {
intent.getStringExtra(EXTRA_ROUTE)
@@ -80,13 +80,21 @@ class SettingsActivity : BaseActivity() {
ProvideSettings {
LauncherTheme {
OverlayHost {
- AnimatedNavHost(
+ NavHost(
navController = navController,
startDestination = "settings",
- exitTransition = { fadeOut(tween(300, 300)) },
- enterTransition = { fadeIn(tween(200)) },
- popEnterTransition = { fadeIn(tween(0)) },
- popExitTransition = { fadeOut(tween(200)) },
+ exitTransition = {
+ fadeOut() + scaleOut(targetScale = 0.5f)
+ },
+ enterTransition = {
+ slideInHorizontally { it }
+ },
+ popEnterTransition = {
+ fadeIn() + scaleIn(initialScale = 0.5f)
+ },
+ popExitTransition = {
+ slideOutHorizontally { it }
+ },
) {
composable("settings") {
MainSettingsScreen()
@@ -156,6 +164,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/integrations") {
IntegrationsSettingsScreen()
}
+ composable("settings/plugins") {
+ PluginsSettingsScreen()
+ }
composable("settings/about") {
AboutSettingsScreen()
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt
index 55c4d171..6d67fea4 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt
@@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.settings.main
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Apps
import androidx.compose.material.icons.rounded.BugReport
+import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.Gesture
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Info
@@ -74,6 +75,14 @@ fun MainSettingsScreen() {
navController?.navigate("settings/integrations")
}
)
+ Preference(
+ icon = Icons.Rounded.Extension,
+ title = stringResource(id = R.string.preference_screen_plugins),
+ summary = stringResource(id = R.string.preference_screen_plugins_summary),
+ onClick = {
+ navController?.navigate("settings/plugins")
+ }
+ )
Preference(
icon = Icons.Rounded.SettingsBackupRestore,
title = stringResource(id = R.string.preference_screen_backup),
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreen.kt
new file mode 100644
index 00000000..b44f4508
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreen.kt
@@ -0,0 +1,119 @@
+package de.mm20.launcher2.ui.settings.plugins
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Extension
+import androidx.compose.material.icons.rounded.ExtensionOff
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.component.LargeMessage
+import de.mm20.launcher2.ui.component.MissingPermissionBanner
+import de.mm20.launcher2.ui.component.preferences.Preference
+import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
+
+@Composable
+fun PluginsSettingsScreen() {
+ val viewModel: PluginsSettingsScreenVM = viewModel()
+ val hostInstalled by viewModel.hostInstalled.collectAsState(null)
+ val hasPermission by viewModel.hasPermission.collectAsState(null)
+ val context = LocalContext.current
+ val plugins by viewModel.plugins.collectAsState(null)
+ PreferenceScreen(title = stringResource(R.string.preference_screen_plugins)) {
+ when {
+ hostInstalled == false -> {
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillParentMaxHeight()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ LargeMessage(
+ icon = Icons.Rounded.ExtensionOff,
+ text = stringResource(R.string.plugin_host_not_installed),
+ color = MaterialTheme.colorScheme.secondary
+ )
+ }
+ }
+ }
+
+ hasPermission == false -> {
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ MissingPermissionBanner(
+ text = stringResource(R.string.missing_permission_plugins),
+ onClick = { viewModel.requestPermission(context) }
+ )
+ }
+ }
+ }
+
+ plugins?.isEmpty() == true -> {
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillParentMaxHeight()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ LargeMessage(
+ icon = Icons.Rounded.Extension,
+ text = stringResource(R.string.no_plugins_installed),
+ color = MaterialTheme.colorScheme.secondary
+ )
+ }
+ }
+ }
+
+ plugins != null -> {
+ items(plugins!!) { item ->
+ val icon by remember(item.plugin.authority) {
+ viewModel.getIcon(item.plugin)
+ }.collectAsState(null)
+ Preference(
+ title = { Text(item.plugin.label) },
+ summary = item.plugin.description?.let { { Text(it) } },
+ controls = {
+ Switch(checked = item.plugin.enabled, onCheckedChange = {
+ viewModel.setPluginEnabled(item.plugin, it)
+ })
+ },
+ icon = {
+ AsyncImage(model = icon, contentDescription = null, modifier = Modifier.size(36.dp))
+ },
+ onClick = {
+ viewModel.setPluginEnabled(item.plugin, !item.plugin.enabled)
+ }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreenVM.kt
new file mode 100644
index 00000000..7625fb24
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginsSettingsScreenVM.kt
@@ -0,0 +1,42 @@
+package de.mm20.launcher2.ui.settings.plugins
+
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.ViewModel
+import de.mm20.launcher2.permissions.PermissionGroup
+import de.mm20.launcher2.permissions.PermissionsManager
+import de.mm20.launcher2.plugin.Plugin
+import de.mm20.launcher2.plugins.PluginService
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class PluginsSettingsScreenVM : ViewModel(), KoinComponent {
+
+ private val pluginService: PluginService by inject()
+ private val permissionsManager: PermissionsManager by inject()
+
+ val hostInstalled = pluginService.isPluginHostInstalled()
+ val hasPermission = permissionsManager.hasPermission(PermissionGroup.Plugins)
+ val plugins = hasPermission.flatMapLatest {
+ if (it) pluginService.getPluginsWithState() else emptyFlow()
+ }
+
+ fun setPluginEnabled(plugin: Plugin, value: Boolean) {
+ if (value) {
+ pluginService.enablePlugin(plugin)
+ } else {
+ pluginService.disablePlugin(plugin)
+ }
+ }
+
+ fun requestPermission(context: Context) {
+ permissionsManager.requestPermission(context as AppCompatActivity, PermissionGroup.Plugins)
+ }
+
+ fun getIcon(plugin: Plugin) = flow {
+ emit(pluginService.getPluginIcon(plugin))
+ }
+}
\ No newline at end of file
diff --git a/core/base/src/main/java/de/mm20/launcher2/plugin/PluginRepository.kt b/core/base/src/main/java/de/mm20/launcher2/plugin/PluginRepository.kt
index d687bb64..8706faab 100644
--- a/core/base/src/main/java/de/mm20/launcher2/plugin/PluginRepository.kt
+++ b/core/base/src/main/java/de/mm20/launcher2/plugin/PluginRepository.kt
@@ -1,5 +1,7 @@
package de.mm20.launcher2.plugin
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
interface PluginRepository {
@@ -10,8 +12,8 @@ interface PluginRepository {
): Flow>
fun get(authority: String): Flow
- fun insertMany(plugins: List)
- fun insert(plugin: Plugin)
- fun update(plugin: Plugin)
- fun deleteMany()
+ fun insertMany(plugins: List): Job
+ fun insert(plugin: Plugin): Job
+ fun update(plugin: Plugin): Job
+ fun deleteMany(): Job
}
\ No newline at end of file
diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml
index b6772029..9b91bf13 100644
--- a/core/i18n/src/main/res/values/strings.xml
+++ b/core/i18n/src/main/res/values/strings.xml
@@ -421,6 +421,7 @@
Set %1$s as default home app to search app shortcuts.
Set %1$s as default home app to create shortcuts.
+ Plugin permission is required to use plugins.
Grant
@@ -503,6 +504,10 @@
Hexagon
Search bar
Integrations
+ Plugins
+ Manage installed extensions
+ No plugins installed
+ Plugin host not installed
Follow system
Themed icons
Color icons with the application\'s color scheme
diff --git a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt
index e6287d34..cf34b24c 100644
--- a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt
+++ b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt
@@ -17,6 +17,7 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
+import de.mm20.launcher2.plugin.contracts.PluginContract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -63,6 +64,7 @@ enum class PermissionGroup {
Notifications,
AppShortcuts,
Accessibility,
+ Plugins,
}
internal class PermissionsManagerImpl(
@@ -83,6 +85,9 @@ internal class PermissionsManagerImpl(
private val locationPermissionState = MutableStateFlow(
checkPermissionOnce(PermissionGroup.Location)
)
+ private val pluginsPermissionState = MutableStateFlow(
+ checkPermissionOnce(PermissionGroup.Plugins)
+ )
private val notificationsPermissionState = MutableStateFlow(false)
private val accessibilityPermissionState = MutableStateFlow(false)
private val appShortcutsPermissionState = MutableStateFlow(
@@ -153,6 +158,14 @@ internal class PermissionsManagerImpl(
CrashReporter.logException(e)
}
}
+
+ PermissionGroup.Plugins -> {
+ ActivityCompat.requestPermissions(
+ context,
+ pluginPermissions,
+ permissionGroup.ordinal
+ )
+ }
}
}
@@ -170,6 +183,10 @@ internal class PermissionsManagerImpl(
contactPermissions.all { context.checkPermission(it) }
}
+ PermissionGroup.Plugins -> {
+ pluginPermissions.all { context.checkPermission(it) }
+ }
+
PermissionGroup.ExternalStorage -> {
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
Environment.isExternalStorageManager()
@@ -201,6 +218,7 @@ internal class PermissionsManagerImpl(
PermissionGroup.Notifications -> notificationsPermissionState
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
PermissionGroup.Accessibility -> accessibilityPermissionState
+ PermissionGroup.Plugins -> pluginsPermissionState
}
}
@@ -209,7 +227,7 @@ internal class PermissionsManagerImpl(
permissions: Array,
grantResults: IntArray
) {
- val permissionGroup = PermissionGroup.values().getOrNull(requestCode) ?: return
+ val permissionGroup = PermissionGroup.entries.getOrNull(requestCode) ?: return
val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
when (permissionGroup) {
PermissionGroup.Calendar -> calendarPermissionState.value = granted
@@ -219,6 +237,7 @@ internal class PermissionsManagerImpl(
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
+ PermissionGroup.Plugins -> pluginsPermissionState.value = granted
}
}
@@ -261,5 +280,6 @@ internal class PermissionsManagerImpl(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
+ private val pluginPermissions = arrayOf(PluginContract.Permission)
}
}
diff --git a/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt b/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt
index ead0386f..6c6eba33 100644
--- a/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt
+++ b/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt
@@ -3,6 +3,7 @@ package de.mm20.launcher2.database.daos
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
+import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import de.mm20.launcher2.database.entities.PluginEntity
@@ -26,15 +27,15 @@ interface PluginDao {
@Query("SELECT * FROM Plugins WHERE authority = :authority")
fun get(authority: String): Flow
- @Insert
- fun insertMany(plugins: List)
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertMany(plugins: List)
- @Insert
- fun insert(plugin: PluginEntity)
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(plugin: PluginEntity)
@Update
- fun update(plugin: PluginEntity)
+ suspend fun update(plugin: PluginEntity)
@Query("DELETE FROM Plugins")
- fun deleteMany()
+ suspend fun deleteMany()
}
\ No newline at end of file
diff --git a/data/files/build.gradle.kts b/data/files/build.gradle.kts
index d491455d..6c987e1e 100644
--- a/data/files/build.gradle.kts
+++ b/data/files/build.gradle.kts
@@ -42,6 +42,7 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
+ implementation(libs.coil.core)
implementation(project(":core:base"))
implementation(project(":core:ktx"))
diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt
index 4d52fbb5..43305fa1 100644
--- a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt
+++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt
@@ -2,9 +2,16 @@ package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
+import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
+import coil.imageLoader
+import coil.request.ImageRequest
import de.mm20.launcher2.files.PluginFileSerializer
+import de.mm20.launcher2.icons.ColorLayer
+import de.mm20.launcher2.icons.LauncherIcon
+import de.mm20.launcher2.icons.StaticIconLayer
+import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.plugin.config.StorageStrategy
import de.mm20.launcher2.search.File
@@ -47,6 +54,21 @@ data class PluginFile(
return PluginFileSerializer()
}
+ override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? {
+ if (thumbnailUri != null) {
+ val request = ImageRequest.Builder(context)
+ .data(thumbnailUri)
+ .build()
+ val result = context.imageLoader.execute(request)
+ val drawable = result.drawable ?: return null
+ return StaticLauncherIcon(
+ foregroundLayer = StaticIconLayer(icon = drawable, scale = 1.5f),
+ backgroundLayer = ColorLayer(),
+ )
+ }
+ return null
+ }
+
companion object {
const val Domain = "plugin.file"
}
diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt
index 2951921b..a3384664 100644
--- a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt
+++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.CancellationSignal
+import android.util.Log
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.plugin.Plugin
import de.mm20.launcher2.plugin.config.StorageStrategy
@@ -36,7 +37,13 @@ class PluginFileProvider(
null,
null,
cancellationSignal
- ) ?: return@suspendCancellableCoroutine it.resume(emptyList())
+ )
+
+ if (cursor == null) {
+ Log.e("MM20", "Plugin ${plugin.authority} returned null cursor")
+ it.resume(emptyList())
+ return@suspendCancellableCoroutine
+ }
val results = fromCursor(cursor) ?: emptyList()
it.resume(results)
diff --git a/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt b/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt
index 443ec0f8..6b9d2a4d 100644
--- a/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt
+++ b/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt
@@ -4,12 +4,18 @@ 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.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
internal class PluginRepositoryImpl(
private val dao: PluginDao,
-): PluginRepository {
+) : PluginRepository {
+
+ private val scope = CoroutineScope(Job() + Dispatchers.IO)
override fun findMany(
type: PluginType?,
enabled: Boolean?,
@@ -28,19 +34,27 @@ internal class PluginRepositoryImpl(
return dao.get(authority).map { Plugin(it) }
}
- override fun insertMany(plugins: List) {
- TODO("Not yet implemented")
+ override fun insertMany(plugins: List): Job {
+ return scope.launch {
+ dao.insertMany(plugins.map { PluginEntity(it) })
+ }
}
- override fun insert(plugin: Plugin) {
- dao.insert(PluginEntity(plugin))
+ override fun insert(plugin: Plugin): Job {
+ return scope.launch {
+ dao.insert(PluginEntity(plugin))
+ }
}
- override fun update(plugin: Plugin) {
- dao.update(PluginEntity(plugin))
+ override fun update(plugin: Plugin): Job {
+ return scope.launch {
+ dao.update(PluginEntity(plugin))
+ }
}
- override fun deleteMany() {
- dao.deleteMany()
+ override fun deleteMany(): Job {
+ return scope.launch {
+ dao.deleteMany()
+ }
}
}
\ No newline at end of file
diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/BasePluginProvider.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/BasePluginProvider.kt
index edd17205..dfb6375d 100644
--- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/BasePluginProvider.kt
+++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/BasePluginProvider.kt
@@ -1,6 +1,8 @@
package de.mm20.launcher2.sdk.base
import android.content.ContentProvider
+import android.content.Context
+import android.content.pm.PackageManager
import android.os.Bundle
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
@@ -43,4 +45,11 @@ abstract class BasePluginProvider : ContentProvider() {
return PluginState.Ready
}
+ internal fun checkPermissionOrThrow(context: Context) {
+ if (context.checkCallingPermission(PluginContract.Permission) == PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ throw SecurityException("Caller does not have permission to use plugins")
+ }
+
}
\ No newline at end of file
diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/SearchPluginProvider.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/SearchPluginProvider.kt
index 81f5c1ad..b7cb5309 100644
--- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/SearchPluginProvider.kt
+++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/base/SearchPluginProvider.kt
@@ -45,7 +45,7 @@ abstract class SearchPluginProvider : BasePluginProvider() {
val context = context ?: return null
checkPermissionOrThrow(context)
when {
- uri.path == SearchPluginContract.Paths.Search -> {
+ uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Search -> {
val query =
uri.getQueryParameter(SearchPluginContract.Paths.QueryParam) ?: return null
val results = search(query, cancellationSignal)
@@ -53,7 +53,7 @@ abstract class SearchPluginProvider : BasePluginProvider() {
for (result in results) {
writeToCursor(cursor, result)
}
- return null
+ return cursor
}
uri.pathSegments.size == 2 && uri.pathSegments.first() == SearchPluginContract.Paths.Root -> {
val id = uri.pathSegments[1]
@@ -110,12 +110,4 @@ abstract class SearchPluginProvider : BasePluginProvider() {
internal abstract fun createCursor(capacity: Int): MatrixCursor
internal abstract fun writeToCursor(cursor: MatrixCursor, item: T)
-
-
- private fun checkPermissionOrThrow(context: Context) {
- if (context.checkCallingPermission(PluginContract.Permission) == PackageManager.PERMISSION_GRANTED) {
- return
- }
- throw SecurityException("Caller does not have permission to use plugins")
- }
}
\ No newline at end of file
diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/files/FileProvider.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/files/FileProvider.kt
index 3fded0c2..537f1af4 100644
--- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/files/FileProvider.kt
+++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/files/FileProvider.kt
@@ -1,16 +1,15 @@
package de.mm20.launcher2.sdk.files
-import android.database.Cursor
import android.database.MatrixCursor
-import android.net.Uri
+import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugin.contracts.FilePluginContract
import de.mm20.launcher2.sdk.base.SearchPluginProvider
abstract class FileProvider : SearchPluginProvider() {
abstract override suspend fun search(query: String): List
- final override fun getPluginType(): de.mm20.launcher2.plugin.PluginType {
- return de.mm20.launcher2.plugin.PluginType.FileSearch
+ final override fun getPluginType(): PluginType {
+ return PluginType.FileSearch
}
override fun createCursor(capacity: Int): MatrixCursor {
diff --git a/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginScanner.kt b/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginScanner.kt
index aff24763..8322ccd5 100644
--- a/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginScanner.kt
+++ b/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginScanner.kt
@@ -2,6 +2,8 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
+import android.util.Log
+import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.plugin.Plugin
import de.mm20.launcher2.plugin.PluginType
@@ -16,37 +18,44 @@ class PluginScanner(
val plugins = mutableListOf()
for (cr in contentResolvers) {
- val providerInfo = cr.providerInfo ?: continue
- val authority = providerInfo.authority ?: continue
- val bundle = context.contentResolver.call(
- Uri.Builder()
- .scheme("content")
- .authority(authority)
- .build(),
- "getType",
- null,
- null,
- ) ?: continue
- val type = bundle.getString("type")
- ?.let {
- try {
- PluginType.valueOf(it)
- } catch (e: IllegalArgumentException) {
- null
- }
- } ?: continue
- plugins.add(
- Plugin(
- label = cr.loadLabel(context.packageManager).toString(),
- description = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.description"),
- packageName = providerInfo.packageName,
- className = providerInfo.name,
- type = type,
- authority = authority,
- settingsActivity = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.settings"),
- enabled = false,
+ try {
+ val providerInfo = cr.providerInfo ?: continue
+ val authority = providerInfo.authority ?: continue
+ val bundle = context.contentResolver.call(
+ Uri.Builder()
+ .scheme("content")
+ .authority(authority)
+ .build(),
+ "getType",
+ null,
+ null,
+ ) ?: continue
+ val type = bundle.getString("type")
+ ?.let {
+ try {
+ PluginType.valueOf(it)
+ } catch (e: IllegalArgumentException) {
+ null
+ }
+ } ?: continue
+ plugins.add(
+ Plugin(
+ label = cr.loadLabel(context.packageManager).toString(),
+ description = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.description"),
+ packageName = providerInfo.packageName,
+ className = providerInfo.name,
+ type = type,
+ authority = authority,
+ settingsActivity = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.settings"),
+ enabled = false,
+ )
)
- )
+ } catch (e: SecurityException) {
+ continue
+ } catch (e: Exception) {
+ CrashReporter.logException(e)
+ continue
+ }
}
return plugins
diff --git a/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt b/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt
index 62166f2d..5634ba2a 100644
--- a/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt
+++ b/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt
@@ -2,9 +2,12 @@ package de.mm20.launcher2.plugins
import PluginScanner
import android.content.BroadcastReceiver
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.content.ContextCompat
import de.mm20.launcher2.plugin.Plugin
@@ -16,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -32,7 +36,11 @@ interface PluginService {
fun enablePlugin(plugin: Plugin)
fun disablePlugin(plugin: Plugin)
fun getPluginsWithState(type: PluginType? = null): Flow>
+
+ fun isPluginHostInstalled(): Flow
suspend fun getPluginState(plugin: Plugin): PluginState?
+
+ suspend fun getPluginIcon(plugin: Plugin): Drawable?
}
internal class PluginServiceImpl(
@@ -42,6 +50,10 @@ internal class PluginServiceImpl(
private val scope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Default)
+ private val pluginHostInstalled = MutableStateFlow(false)
+
+ private val mutex = Mutex()
+
init {
refreshPlugins()
ContextCompat.registerReceiver(
@@ -65,9 +77,16 @@ internal class PluginServiceImpl(
repository.update(plugin.copy(enabled = false))
}
- private val mutex = Mutex()
private fun refreshPlugins() {
scope.launch {
+ try {
+ val permission =
+ context.packageManager.getPermissionInfo(PluginContract.Permission, 0)
+ pluginHostInstalled.value = permission != null
+ } catch (e: PackageManager.NameNotFoundException) {
+ pluginHostInstalled.value = false
+ return@launch
+ }
mutex.withLock {
val enabledPlugins =
repository.findMany(enabled = true).first().map { it.authority }
@@ -79,8 +98,8 @@ internal class PluginServiceImpl(
it
}
}
- repository.deleteMany()
- repository.insertMany(plugins)
+ repository.deleteMany().join()
+ repository.insertMany(plugins).join()
}
}
}
@@ -128,6 +147,26 @@ internal class PluginServiceImpl(
}
}
+ override fun isPluginHostInstalled(): Flow {
+ return pluginHostInstalled
+ }
+
+ override suspend fun getPluginIcon(plugin: Plugin): Drawable? {
+ return withContext(Dispatchers.IO) {
+ val info = try {
+ context.packageManager.getProviderInfo(
+ ComponentName(
+ plugin.packageName,
+ plugin.className
+ ), 0
+ )
+ } catch (e: PackageManager.NameNotFoundException) {
+ return@withContext null
+ }
+ info.loadIcon(context.packageManager) ?: info.applicationInfo?.loadIcon(context.packageManager)
+ }
+ }
+
private inner class AppUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
refreshPlugins()