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()