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