Implement custom permission system for plugins
This commit is contained in:
parent
1c542f38ba
commit
cb7f6b6693
@ -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))
|
||||
}
|
||||
|
||||
@ -19,6 +19,8 @@ sealed class PluginState {
|
||||
|
||||
data object Error: PluginState()
|
||||
|
||||
data object NoPermission: PluginState()
|
||||
|
||||
companion object {
|
||||
fun fromBundle(bundle: Bundle): PluginState? {
|
||||
val type = bundle.getString("type") ?: return null
|
||||
|
||||
@ -64,7 +64,6 @@ enum class PermissionGroup {
|
||||
Notifications,
|
||||
AppShortcuts,
|
||||
Accessibility,
|
||||
Plugins,
|
||||
}
|
||||
|
||||
internal class PermissionsManagerImpl(
|
||||
@ -85,9 +84,6 @@ 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(
|
||||
@ -158,14 +154,6 @@ internal class PermissionsManagerImpl(
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
PermissionGroup.Plugins -> {
|
||||
ActivityCompat.requestPermissions(
|
||||
context,
|
||||
pluginPermissions,
|
||||
permissionGroup.ordinal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,10 +171,6 @@ 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()
|
||||
@ -218,7 +202,6 @@ internal class PermissionsManagerImpl(
|
||||
PermissionGroup.Notifications -> notificationsPermissionState
|
||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
||||
PermissionGroup.Accessibility -> accessibilityPermissionState
|
||||
PermissionGroup.Plugins -> pluginsPermissionState
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,7 +220,6 @@ internal class PermissionsManagerImpl(
|
||||
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
|
||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
||||
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
|
||||
PermissionGroup.Plugins -> pluginsPermissionState.value = granted
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,6 +262,5 @@ internal class PermissionsManagerImpl(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
private val pluginPermissions = arrayOf(PluginContract.Permission)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
object PluginContract {
|
||||
|
||||
const val Permission = "de.mm20.launcher2.permission.USE_PLUGINS"
|
||||
object Methods {
|
||||
const val GetType = "getType"
|
||||
const val GetState = "getState"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@ -0,0 +1 @@
|
||||
package de.mm20.launcher2.weather.settings
|
||||
@ -0,0 +1,27 @@
|
||||
package de.mm20.launcher2.weather.settings
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
data class LatLon(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProviderSettings(
|
||||
val lastUpdate: Long = 0,
|
||||
val locationId: String? = null,
|
||||
val locationName: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WeatherSettingsData(
|
||||
val provider: String = "metno",
|
||||
val autoLocation: Boolean = true,
|
||||
val location: LatLon? = null,
|
||||
val locationName: String? = null,
|
||||
val lastLocation: LatLon? = null,
|
||||
val providerSettings: Map<String, ProviderSettings>
|
||||
)
|
||||
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -14,6 +15,10 @@ android {
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
@ -46,6 +51,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
api(project(":core:shared"))
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.bundles.kotlin)
|
||||
}
|
||||
|
||||
|
||||
13
plugins/sdk/src/main/AndroidManifest.xml
Normal file
13
plugins/sdk/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".permissions.RequestPermissionActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.DeviceDefault.Dialog.NoActionBar.MinWidth">
|
||||
<intent-filter>
|
||||
<action android:name="de.mm20.launcher2.plugin.REQUEST_PERMISSION" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@ -8,17 +8,22 @@ import android.os.Bundle
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.plugin.contracts.PluginContract
|
||||
import de.mm20.launcher2.sdk.PluginState
|
||||
import de.mm20.launcher2.sdk.permissions.PluginPermissionManager
|
||||
import de.mm20.launcher2.sdk.permissions.permissionsDataStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
abstract class BasePluginProvider : ContentProvider() {
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
val context = context ?: return null
|
||||
return when (method) {
|
||||
PluginContract.Methods.GetType -> Bundle().apply {
|
||||
putString("type", getPluginType().name)
|
||||
}
|
||||
|
||||
PluginContract.Methods.GetState -> {
|
||||
checkPermissionOrThrow(context)
|
||||
val state = runBlocking {
|
||||
getPluginState()
|
||||
}
|
||||
@ -27,6 +32,7 @@ abstract class BasePluginProvider : ContentProvider() {
|
||||
}
|
||||
|
||||
PluginContract.Methods.GetConfig -> {
|
||||
checkPermissionOrThrow(context)
|
||||
getPluginConfig()
|
||||
}
|
||||
|
||||
@ -45,7 +51,12 @@ abstract class BasePluginProvider : ContentProvider() {
|
||||
}
|
||||
|
||||
internal fun checkPermissionOrThrow(context: Context) {
|
||||
if (context.checkCallingPermission(PluginContract.Permission) == PackageManager.PERMISSION_GRANTED) {
|
||||
val callingPackage = callingPackage ?: throw IllegalArgumentException("No calling package")
|
||||
val permissionManager = PluginPermissionManager(context)
|
||||
val hasPermission = runBlocking {
|
||||
permissionManager.hasPermission(callingPackage).first()
|
||||
}
|
||||
if (hasPermission) {
|
||||
return
|
||||
}
|
||||
throw SecurityException("Caller does not have permission to use plugins")
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
package de.mm20.launcher2.sdk.permissions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.dataStore
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal val Context.permissionsDataStore by dataStore(
|
||||
fileName = "plugin_permissions",
|
||||
serializer = PermissionsSerializer,
|
||||
)
|
||||
|
||||
internal object PermissionsSerializer : Serializer<Set<String>> {
|
||||
override val defaultValue: Set<String>
|
||||
get() = emptySet()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): Set<String> {
|
||||
return input.bufferedReader().readLines().toSet()
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: Set<String>, output: OutputStream) {
|
||||
output.bufferedWriter().write(t.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package de.mm20.launcher2.sdk.permissions
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PluginPermissionManager(
|
||||
context: Context,
|
||||
) {
|
||||
private val dataStore = context.applicationContext.permissionsDataStore
|
||||
|
||||
fun hasPermission(pluginPackage: String): Flow<Boolean> {
|
||||
return dataStore.data.map { it.contains(pluginPackage) }
|
||||
}
|
||||
|
||||
fun grantPermission(pluginPackage: String) {
|
||||
runBlocking {
|
||||
dataStore.updateData {
|
||||
it + pluginPackage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun revokePermission(pluginPackage: String) {
|
||||
runBlocking {
|
||||
dataStore.updateData {
|
||||
it - pluginPackage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package de.mm20.launcher2.sdk.permissions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import de.mm20.launcher2.sdk.R
|
||||
import de.mm20.launcher2.sdk.databinding.ActivityRequestPermissionBinding
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class RequestPermissionActivity: Activity() {
|
||||
|
||||
private lateinit var binding: ActivityRequestPermissionBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val callingPackage = callingPackage ?: throw IllegalArgumentException("No calling package")
|
||||
|
||||
val callingPackageInfo = try {
|
||||
packageManager.getApplicationInfo(callingPackage, 0)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
throw IllegalArgumentException("Invalid calling package")
|
||||
}
|
||||
|
||||
val myPackageInfo = try {
|
||||
packageManager.getApplicationInfo(packageName, 0)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
throw IllegalStateException("Invalid package")
|
||||
}
|
||||
|
||||
|
||||
val permissionManager = PluginPermissionManager(this)
|
||||
|
||||
val hasPermission = runBlocking {
|
||||
permissionManager.hasPermission(callingPackage).first()
|
||||
}
|
||||
|
||||
if (hasPermission) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding = ActivityRequestPermissionBinding.inflate(LayoutInflater.from(this))
|
||||
val text = getString(
|
||||
R.string.request_permission_message,
|
||||
callingPackageInfo.loadLabel(packageManager),
|
||||
myPackageInfo.loadLabel(packageManager)
|
||||
)
|
||||
binding.textView.text = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
|
||||
setContentView(binding.root)
|
||||
binding.grantButton.setOnClickListener {
|
||||
permissionManager.grantPermission(callingPackage)
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
binding.denyButton.setOnClickListener {
|
||||
permissionManager.revokePermission(callingPackage)
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@android:style/TextAppearance.DeviceDefault"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/deny_button"
|
||||
style="@android:style/Widget.DeviceDefault.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/request_permission_deny_button" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/grant_button"
|
||||
style="@android:style/Widget.DeviceDefault.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/request_permission_allow_button" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
6
plugins/sdk/src/main/res/values/strings.xml
Normal file
6
plugins/sdk/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="request_permission_message"><![CDATA[<b>%1$s</b> wants to access data from <b>%2$s</b>.]]></string>
|
||||
<string name="request_permission_deny_button">Deny</string>
|
||||
<string name="request_permission_allow_button">Allow</string>
|
||||
</resources>
|
||||
@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val servicesPluginsModule = module {
|
||||
single<PluginService> { PluginServiceImpl(androidContext(), get(), get()) }
|
||||
single<PluginService> { PluginServiceImpl(androidContext(), get()) }
|
||||
}
|
||||
@ -13,8 +13,6 @@ import android.os.Build
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.Plugin
|
||||
import de.mm20.launcher2.plugin.PluginPackage
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
@ -25,8 +23,6 @@ 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.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@ -50,8 +46,6 @@ interface PluginService {
|
||||
enabled: Boolean? = null,
|
||||
): Flow<List<PluginWithState>>
|
||||
|
||||
fun isPluginHostInstalled(): Flow<Boolean>
|
||||
|
||||
fun getPluginPackages(): Flow<List<PluginPackage>>
|
||||
fun getPluginPackage(packageName: String): Flow<PluginPackage?>
|
||||
suspend fun getPluginState(plugin: Plugin): PluginState?
|
||||
@ -65,13 +59,10 @@ interface PluginService {
|
||||
internal class PluginServiceImpl(
|
||||
private val context: Context,
|
||||
private val repository: PluginRepository,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
) : PluginService {
|
||||
|
||||
private val scope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
private val pluginHostInstalled = MutableStateFlow(false)
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
init {
|
||||
@ -84,14 +75,6 @@ internal class PluginServiceImpl(
|
||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
|
||||
scope.launch {
|
||||
permissionsManager.hasPermission(PermissionGroup.Plugins).collectLatest {
|
||||
if (it) {
|
||||
refreshPlugins()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enablePluginPackage(plugin: PluginPackage) {
|
||||
@ -113,14 +96,6 @@ internal class PluginServiceImpl(
|
||||
private fun refreshPlugins() {
|
||||
Log.d("PluginService", "Refreshing plugins")
|
||||
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 enabledPluginPackages =
|
||||
repository.findMany(enabled = true).first().map { it.packageName }.distinct()
|
||||
@ -157,24 +132,25 @@ internal class PluginServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun getPluginState(plugin: Plugin): PluginState {
|
||||
val bundle = withContext(Dispatchers.IO) {
|
||||
context.contentResolver.call(
|
||||
Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(plugin.authority)
|
||||
.build(),
|
||||
PluginContract.Methods.GetState,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} ?: return PluginState.Error
|
||||
val bundle =
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.call(
|
||||
Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(plugin.authority)
|
||||
.build(),
|
||||
PluginContract.Methods.GetState,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} ?: return PluginState.Error
|
||||
} catch (e: SecurityException) {
|
||||
return PluginState.NoPermission
|
||||
}
|
||||
return PluginState.fromBundle(bundle) ?: PluginState.Error
|
||||
}
|
||||
|
||||
override fun isPluginHostInstalled(): Flow<Boolean> {
|
||||
return pluginHostInstalled
|
||||
}
|
||||
|
||||
override suspend fun getPluginIcon(plugin: Plugin): Drawable? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val info = try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user