Implement custom permission system for plugins

This commit is contained in:
MM20 2023-12-18 16:51:48 +01:00
parent 1c542f38ba
commit cb7f6b6693
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
20 changed files with 294 additions and 126 deletions

View File

@ -1,6 +1,10 @@
package de.mm20.launcher2.ui.settings.plugins package de.mm20.launcher2.ui.settings.plugins
import android.app.Activity
import android.app.PendingIntent 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.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState 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.automirrored.rounded.InsertDriveFile
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Error 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.Info
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Verified 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -66,12 +67,25 @@ fun PluginSettingsScreen(pluginId: String) {
val icon by viewModel.icon.collectAsStateWithLifecycle(null) val icon by viewModel.icon.collectAsStateWithLifecycle(null)
val types by viewModel.types.collectAsStateWithLifecycle(emptyList()) val types by viewModel.types.collectAsStateWithLifecycle(emptyList())
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(
null,
minActiveState = Lifecycle.State.RESUMED
)
val filePlugins by viewModel.filePlugins.collectAsStateWithLifecycle( val filePlugins by viewModel.filePlugins.collectAsStateWithLifecycle(
emptyList(), emptyList(),
minActiveState = Lifecycle.State.RESUMED 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( Scaffold(
topBar = { topBar = {
@ -217,6 +231,7 @@ fun PluginSettingsScreen(pluginId: String) {
Icon( Icon(
when (type) { when (type) {
PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile
PluginType.Weather -> Icons.Rounded.LightMode
}, },
null, null,
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
@ -225,6 +240,7 @@ fun PluginSettingsScreen(pluginId: String) {
Text( Text(
when (type) { when (type) {
PluginType.FileSearch -> "File search" PluginType.FileSearch -> "File search"
PluginType.Weather -> "Weather provider"
}, },
modifier = Modifier.padding(horizontal = 4.dp), modifier = Modifier.padding(horizontal = 4.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
@ -249,16 +265,25 @@ fun PluginSettingsScreen(pluginId: String) {
color = surfaceColor, color = surfaceColor,
) { ) {
SwitchPreference( SwitchPreference(
enabled = pluginPackage != null, enabled = pluginPackage != null && hasPermission != null,
iconPadding = false, iconPadding = false,
title = "Enable plugin", title = "Enable plugin",
value = pluginPackage?.enabled == true, value = pluginPackage?.enabled == true && hasPermission == true,
onValueChanged = { 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()) { if (filePlugins.isNotEmpty()) {
PreferenceCategory( PreferenceCategory(
"File search", "File search",
@ -299,7 +324,10 @@ fun PluginSettingsScreen(pluginId: String) {
?: plugin.plugin.description, ?: plugin.plugin.description,
value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onValueChanged = { onValueChanged = {
viewModel.setFileSearchPluginEnabled(plugin.plugin.authority, it) viewModel.setFileSearchPluginEnabled(
plugin.plugin.authority,
it
)
}, },
iconPadding = false, iconPadding = false,
) )

View File

@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -50,17 +51,24 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it?.plugins?.map { it.type }?.distinct() ?: emptyList() it?.plugins?.map { it.type }?.distinct() ?: emptyList()
} }
val filePlugins = pluginPackage val states = pluginPackage
.map { .map {
it?.plugins?.mapNotNull { it?.plugins?.map {
if (it.type == PluginType.FileSearch) { val state = pluginService.getPluginState(it)
val state = pluginService.getPluginState(it) PluginWithState(it, state)
PluginWithState(it, state)
} else {
null
}
} ?: emptyList() } ?: 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) { fun init(pluginId: String) {

View File

@ -36,50 +36,12 @@ import de.mm20.launcher2.ui.locals.LocalNavController
@Composable @Composable
fun PluginsSettingsScreen() { fun PluginsSettingsScreen() {
val viewModel: PluginsSettingsScreenVM = viewModel() 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 context = LocalContext.current
val pluginPackages by viewModel.pluginPackages.collectAsState(null) val pluginPackages by viewModel.pluginPackages.collectAsState(null)
val enabledPackages by viewModel.enabledPluginPackages.collectAsState(emptyList()) val enabledPackages by viewModel.enabledPluginPackages.collectAsState(emptyList())
val disabledPackages by viewModel.disabledPluginPackages.collectAsState(emptyList()) val disabledPackages by viewModel.disabledPluginPackages.collectAsState(emptyList())
PreferenceScreen(title = stringResource(R.string.preference_screen_plugins)) { PreferenceScreen(title = stringResource(R.string.preference_screen_plugins)) {
when { 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 -> { pluginPackages?.isEmpty() == true -> {
item { item {
Column( Column(

View File

@ -19,10 +19,7 @@ import org.koin.core.component.inject
class PluginsSettingsScreenVM : ViewModel(), KoinComponent { class PluginsSettingsScreenVM : ViewModel(), KoinComponent {
private val pluginService: PluginService by inject() 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 val pluginPackages = pluginService
.getPluginPackages() .getPluginPackages()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100), 1) .shareIn(viewModelScope, SharingStarted.WhileSubscribed(100), 1)
@ -35,10 +32,6 @@ class PluginsSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { !it.enabled }.sortedBy { it.label } it.filter { !it.enabled }.sortedBy { it.label }
} }
fun requestPermission(context: Context) {
permissionsManager.requestPermission(context as AppCompatActivity, PermissionGroup.Plugins)
}
fun getIcon(plugin: PluginPackage) = flow { fun getIcon(plugin: PluginPackage) = flow {
emit(pluginService.getPluginPackageIcon(plugin)) emit(pluginService.getPluginPackageIcon(plugin))
} }

View File

@ -19,6 +19,8 @@ sealed class PluginState {
data object Error: PluginState() data object Error: PluginState()
data object NoPermission: PluginState()
companion object { companion object {
fun fromBundle(bundle: Bundle): PluginState? { fun fromBundle(bundle: Bundle): PluginState? {
val type = bundle.getString("type") ?: return null val type = bundle.getString("type") ?: return null

View File

@ -64,7 +64,6 @@ enum class PermissionGroup {
Notifications, Notifications,
AppShortcuts, AppShortcuts,
Accessibility, Accessibility,
Plugins,
} }
internal class PermissionsManagerImpl( internal class PermissionsManagerImpl(
@ -85,9 +84,6 @@ internal class PermissionsManagerImpl(
private val locationPermissionState = MutableStateFlow( private val locationPermissionState = MutableStateFlow(
checkPermissionOnce(PermissionGroup.Location) checkPermissionOnce(PermissionGroup.Location)
) )
private val pluginsPermissionState = MutableStateFlow(
checkPermissionOnce(PermissionGroup.Plugins)
)
private val notificationsPermissionState = MutableStateFlow(false) private val notificationsPermissionState = MutableStateFlow(false)
private val accessibilityPermissionState = MutableStateFlow(false) private val accessibilityPermissionState = MutableStateFlow(false)
private val appShortcutsPermissionState = MutableStateFlow( private val appShortcutsPermissionState = MutableStateFlow(
@ -158,14 +154,6 @@ internal class PermissionsManagerImpl(
CrashReporter.logException(e) CrashReporter.logException(e)
} }
} }
PermissionGroup.Plugins -> {
ActivityCompat.requestPermissions(
context,
pluginPermissions,
permissionGroup.ordinal
)
}
} }
} }
@ -183,10 +171,6 @@ internal class PermissionsManagerImpl(
contactPermissions.all { context.checkPermission(it) } contactPermissions.all { context.checkPermission(it) }
} }
PermissionGroup.Plugins -> {
pluginPermissions.all { context.checkPermission(it) }
}
PermissionGroup.ExternalStorage -> { PermissionGroup.ExternalStorage -> {
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) { if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
Environment.isExternalStorageManager() Environment.isExternalStorageManager()
@ -218,7 +202,6 @@ internal class PermissionsManagerImpl(
PermissionGroup.Notifications -> notificationsPermissionState PermissionGroup.Notifications -> notificationsPermissionState
PermissionGroup.AppShortcuts -> appShortcutsPermissionState PermissionGroup.AppShortcuts -> appShortcutsPermissionState
PermissionGroup.Accessibility -> accessibilityPermissionState PermissionGroup.Accessibility -> accessibilityPermissionState
PermissionGroup.Plugins -> pluginsPermissionState
} }
} }
@ -237,7 +220,6 @@ internal class PermissionsManagerImpl(
PermissionGroup.Notifications -> notificationsPermissionState.value = granted PermissionGroup.Notifications -> notificationsPermissionState.value = granted
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
PermissionGroup.Accessibility -> accessibilityPermissionState.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.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
private val pluginPermissions = arrayOf(PluginContract.Permission)
} }
} }

View File

@ -1,8 +1,6 @@
package de.mm20.launcher2.plugin.contracts package de.mm20.launcher2.plugin.contracts
object PluginContract { object PluginContract {
const val Permission = "de.mm20.launcher2.permission.USE_PLUGINS"
object Methods { object Methods {
const val GetType = "getType" const val GetType = "getType"
const val GetState = "getState" const val GetState = "getState"

View File

@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
} }
android { android {

View File

@ -0,0 +1 @@
package de.mm20.launcher2.weather.settings

View File

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

View File

@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
`maven-publish` `maven-publish`
} }
@ -14,6 +15,10 @@ android {
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
} }
buildFeatures {
viewBinding = true
}
buildTypes { buildTypes {
release { release {
proguardFiles( proguardFiles(
@ -46,6 +51,8 @@ android {
dependencies { dependencies {
api(project(":core:shared")) api(project(":core:shared"))
implementation(libs.androidx.datastore)
implementation(libs.kotlinx.serialization.json)
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
} }

View 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>

View File

@ -8,17 +8,22 @@ import android.os.Bundle
import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugin.contracts.PluginContract import de.mm20.launcher2.plugin.contracts.PluginContract
import de.mm20.launcher2.sdk.PluginState 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 import kotlinx.coroutines.runBlocking
abstract class BasePluginProvider : ContentProvider() { abstract class BasePluginProvider : ContentProvider() {
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
val context = context ?: return null
return when (method) { return when (method) {
PluginContract.Methods.GetType -> Bundle().apply { PluginContract.Methods.GetType -> Bundle().apply {
putString("type", getPluginType().name) putString("type", getPluginType().name)
} }
PluginContract.Methods.GetState -> { PluginContract.Methods.GetState -> {
checkPermissionOrThrow(context)
val state = runBlocking { val state = runBlocking {
getPluginState() getPluginState()
} }
@ -27,6 +32,7 @@ abstract class BasePluginProvider : ContentProvider() {
} }
PluginContract.Methods.GetConfig -> { PluginContract.Methods.GetConfig -> {
checkPermissionOrThrow(context)
getPluginConfig() getPluginConfig()
} }
@ -45,7 +51,12 @@ abstract class BasePluginProvider : ContentProvider() {
} }
internal fun checkPermissionOrThrow(context: Context) { 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 return
} }
throw SecurityException("Caller does not have permission to use plugins") throw SecurityException("Caller does not have permission to use plugins")

View File

@ -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"))
}
}

View File

@ -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
}
}
}
}

View File

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

View File

@ -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>

View 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>

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val servicesPluginsModule = module { val servicesPluginsModule = module {
single<PluginService> { PluginServiceImpl(androidContext(), get(), get()) } single<PluginService> { PluginServiceImpl(androidContext(), get()) }
} }

View File

@ -13,8 +13,6 @@ import android.os.Build
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import de.mm20.launcher2.ktx.tryStartActivity 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.Plugin
import de.mm20.launcher2.plugin.PluginPackage import de.mm20.launcher2.plugin.PluginPackage
import de.mm20.launcher2.plugin.PluginRepository import de.mm20.launcher2.plugin.PluginRepository
@ -25,8 +23,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -50,8 +46,6 @@ interface PluginService {
enabled: Boolean? = null, enabled: Boolean? = null,
): Flow<List<PluginWithState>> ): Flow<List<PluginWithState>>
fun isPluginHostInstalled(): Flow<Boolean>
fun getPluginPackages(): Flow<List<PluginPackage>> fun getPluginPackages(): Flow<List<PluginPackage>>
fun getPluginPackage(packageName: String): Flow<PluginPackage?> fun getPluginPackage(packageName: String): Flow<PluginPackage?>
suspend fun getPluginState(plugin: Plugin): PluginState? suspend fun getPluginState(plugin: Plugin): PluginState?
@ -65,13 +59,10 @@ interface PluginService {
internal class PluginServiceImpl( internal class PluginServiceImpl(
private val context: Context, private val context: Context,
private val repository: PluginRepository, private val repository: PluginRepository,
private val permissionsManager: PermissionsManager,
) : PluginService { ) : PluginService {
private val scope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Default) private val scope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Default)
private val pluginHostInstalled = MutableStateFlow(false)
private val mutex = Mutex() private val mutex = Mutex()
init { init {
@ -84,14 +75,6 @@ internal class PluginServiceImpl(
addAction(Intent.ACTION_PACKAGE_CHANGED) addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package") addDataScheme("package")
}) })
scope.launch {
permissionsManager.hasPermission(PermissionGroup.Plugins).collectLatest {
if (it) {
refreshPlugins()
}
}
}
} }
override fun enablePluginPackage(plugin: PluginPackage) { override fun enablePluginPackage(plugin: PluginPackage) {
@ -113,14 +96,6 @@ internal class PluginServiceImpl(
private fun refreshPlugins() { private fun refreshPlugins() {
Log.d("PluginService", "Refreshing plugins") Log.d("PluginService", "Refreshing plugins")
scope.launch { 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 { mutex.withLock {
val enabledPluginPackages = val enabledPluginPackages =
repository.findMany(enabled = true).first().map { it.packageName }.distinct() repository.findMany(enabled = true).first().map { it.packageName }.distinct()
@ -157,24 +132,25 @@ internal class PluginServiceImpl(
} }
override suspend fun getPluginState(plugin: Plugin): PluginState { override suspend fun getPluginState(plugin: Plugin): PluginState {
val bundle = withContext(Dispatchers.IO) { val bundle =
context.contentResolver.call( try {
Uri.Builder() withContext(Dispatchers.IO) {
.scheme("content") context.contentResolver.call(
.authority(plugin.authority) Uri.Builder()
.build(), .scheme("content")
PluginContract.Methods.GetState, .authority(plugin.authority)
null, .build(),
null PluginContract.Methods.GetState,
) null,
} ?: return PluginState.Error null
)
} ?: return PluginState.Error
} catch (e: SecurityException) {
return PluginState.NoPermission
}
return PluginState.fromBundle(bundle) ?: PluginState.Error return PluginState.fromBundle(bundle) ?: PluginState.Error
} }
override fun isPluginHostInstalled(): Flow<Boolean> {
return pluginHostInstalled
}
override suspend fun getPluginIcon(plugin: Plugin): Drawable? { override suspend fun getPluginIcon(plugin: Plugin): Drawable? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val info = try { val info = try {