Implement custom permission system for plugins
This commit is contained in:
+38
-10
@@ -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,
|
||||
)
|
||||
|
||||
+16
-8
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user