Implement custom permission system for plugins

This commit is contained in:
MM20
2023-12-18 16:51:48 +01:00
parent 1c542f38ba
commit cb7f6b6693
20 changed files with 294 additions and 126 deletions
@@ -1,6 +1,10 @@
package de.mm20.launcher2.ui.settings.plugins
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
@@ -18,13 +22,10 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.FileCopy
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Verified
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material.icons.rounded.WarningAmber
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -66,12 +67,25 @@ fun PluginSettingsScreen(pluginId: String) {
val icon by viewModel.icon.collectAsStateWithLifecycle(null)
val types by viewModel.types.collectAsStateWithLifecycle(emptyList())
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(
null,
minActiveState = Lifecycle.State.RESUMED
)
val filePlugins by viewModel.filePlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val enabledFileSearchPlugins by viewModel.enabledFileSearchPlugins.collectAsStateWithLifecycle(null)
val requestPermissionStarter =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.setPluginEnabled(true)
}
}
val enabledFileSearchPlugins by viewModel.enabledFileSearchPlugins.collectAsStateWithLifecycle(
null
)
Scaffold(
topBar = {
@@ -217,6 +231,7 @@ fun PluginSettingsScreen(pluginId: String) {
Icon(
when (type) {
PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile
PluginType.Weather -> Icons.Rounded.LightMode
},
null,
modifier = Modifier.size(16.dp),
@@ -225,6 +240,7 @@ fun PluginSettingsScreen(pluginId: String) {
Text(
when (type) {
PluginType.FileSearch -> "File search"
PluginType.Weather -> "Weather provider"
},
modifier = Modifier.padding(horizontal = 4.dp),
style = MaterialTheme.typography.labelMedium,
@@ -249,16 +265,25 @@ fun PluginSettingsScreen(pluginId: String) {
color = surfaceColor,
) {
SwitchPreference(
enabled = pluginPackage != null,
enabled = pluginPackage != null && hasPermission != null,
iconPadding = false,
title = "Enable plugin",
value = pluginPackage?.enabled == true,
value = pluginPackage?.enabled == true && hasPermission == true,
onValueChanged = {
viewModel.setPluginEnabled(it)
if (hasPermission == true) {
viewModel.setPluginEnabled(it)
} else {
requestPermissionStarter.launch(
Intent().apply {
`package` = pluginPackage?.packageName
action = "de.mm20.launcher2.plugin.REQUEST_PERMISSION"
}
)
}
}
)
}
AnimatedVisibility(pluginPackage?.enabled == true) {
AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) {
if (filePlugins.isNotEmpty()) {
PreferenceCategory(
"File search",
@@ -299,7 +324,10 @@ fun PluginSettingsScreen(pluginId: String) {
?: plugin.plugin.description,
value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onValueChanged = {
viewModel.setFileSearchPluginEnabled(plugin.plugin.authority, it)
viewModel.setFileSearchPluginEnabled(
plugin.plugin.authority,
it
)
},
iconPadding = false,
)
@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -50,17 +51,24 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it?.plugins?.map { it.type }?.distinct() ?: emptyList()
}
val filePlugins = pluginPackage
val states = pluginPackage
.map {
it?.plugins?.mapNotNull {
if (it.type == PluginType.FileSearch) {
val state = pluginService.getPluginState(it)
PluginWithState(it, state)
} else {
null
}
it?.plugins?.map {
val state = pluginService.getPluginState(it)
PluginWithState(it, state)
} ?: emptyList()
}
.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
val hasPermission = states
.map {
it.none { it.state is PluginState.NoPermission }
}
val filePlugins = states
.map {
it.filter { it.plugin.type == PluginType.FileSearch }
}
fun init(pluginId: String) {
@@ -36,50 +36,12 @@ import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun PluginsSettingsScreen() {
val viewModel: PluginsSettingsScreenVM = viewModel()
val navController = LocalNavController.current
val hostInstalled by viewModel.hostInstalled.collectAsState(null)
val hasPermission by viewModel.hasPermission.collectAsState(null)
val context = LocalContext.current
val pluginPackages by viewModel.pluginPackages.collectAsState(null)
val enabledPackages by viewModel.enabledPluginPackages.collectAsState(emptyList())
val disabledPackages by viewModel.disabledPluginPackages.collectAsState(emptyList())
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) }
)
}
}
}
pluginPackages?.isEmpty() == true -> {
item {
Column(
@@ -19,10 +19,7 @@ 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 pluginPackages = pluginService
.getPluginPackages()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100), 1)
@@ -35,10 +32,6 @@ class PluginsSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { !it.enabled }.sortedBy { it.label }
}
fun requestPermission(context: Context) {
permissionsManager.requestPermission(context as AppCompatActivity, PermissionGroup.Plugins)
}
fun getIcon(plugin: PluginPackage) = flow {
emit(pluginService.getPluginPackageIcon(plugin))
}