Plugins: bringup

This commit is contained in:
MM20
2023-11-05 19:02:59 +01:00
parent 7da84a747f
commit 801caf9dd6
54 changed files with 1335 additions and 88 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,8 @@
package de.mm20.launcher2.plugins
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val servicesPluginsModule = module {
single<PluginService> { PluginServiceImpl(androidContext(), get()) }
}
@@ -0,0 +1,54 @@
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import de.mm20.launcher2.plugin.Plugin
import de.mm20.launcher2.plugin.PluginType
class PluginScanner(
private val context: Context,
) {
suspend fun findPlugins(): List<Plugin> {
val contentResolvers = context.packageManager.queryIntentContentProviders(
Intent("de.mm20.launcher2.action.PLUGIN"),
PackageManager.GET_META_DATA,
)
val plugins = mutableListOf<Plugin>()
for (cr in contentResolvers) {
val providerInfo = cr.providerInfo ?: continue
val authority = providerInfo.authority ?: continue
val bundle = context.contentResolver.call(
Uri.Builder()
.scheme("content")
.authority(authority)
.build(),
"getType",
null,
null,
) ?: continue
val type = bundle.getString("type")
?.let {
try {
PluginType.valueOf(it)
} catch (e: IllegalArgumentException) {
null
}
} ?: continue
plugins.add(
Plugin(
label = cr.loadLabel(context.packageManager).toString(),
description = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.description"),
packageName = providerInfo.packageName,
className = providerInfo.name,
type = type,
authority = authority,
settingsActivity = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.settings"),
enabled = false,
)
)
}
return plugins
}
}
@@ -0,0 +1,136 @@
package de.mm20.launcher2.plugins
import PluginScanner
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import androidx.core.content.ContextCompat
import de.mm20.launcher2.plugin.Plugin
import de.mm20.launcher2.plugin.PluginRepository
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugin.contracts.PluginContract
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
data class PluginWithState(
val plugin: Plugin,
val state: PluginState?,
)
interface PluginService {
fun enablePlugin(plugin: Plugin)
fun disablePlugin(plugin: Plugin)
fun getPluginsWithState(type: PluginType? = null): Flow<List<PluginWithState>>
suspend fun getPluginState(plugin: Plugin): PluginState?
}
internal class PluginServiceImpl(
private val context: Context,
private val repository: PluginRepository,
) : PluginService {
private val scope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Default)
init {
refreshPlugins()
ContextCompat.registerReceiver(
context,
AppUpdateReceiver(),
IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
},
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
override fun enablePlugin(plugin: Plugin) {
repository.update(plugin.copy(enabled = true))
}
override fun disablePlugin(plugin: Plugin) {
repository.update(plugin.copy(enabled = false))
}
private val mutex = Mutex()
private fun refreshPlugins() {
scope.launch {
mutex.withLock {
val enabledPlugins =
repository.findMany(enabled = true).first().map { it.authority }
val scanner = PluginScanner(context)
val plugins = scanner.findPlugins().map {
if (it.authority in enabledPlugins) {
it.copy(enabled = true)
} else {
it
}
}
repository.deleteMany()
repository.insertMany(plugins)
}
}
}
override fun getPluginsWithState(
type: PluginType?,
): Flow<List<PluginWithState>> {
return repository.findMany(
type = type,
).map {
it.map {
PluginWithState(
plugin = it,
state = getPluginState(it),
)
}
}
}
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 null
val type = bundle.getString("type") ?: return null
return when (type) {
"Ready" -> PluginState.Ready
"SetupRequired" -> {
val setupActivity = bundle.getString("setupActivity") ?: return null
val message = bundle.getString("message")
PluginState.SetupRequired(
setupActivity = setupActivity,
message = message,
)
}
else -> null
}
}
private inner class AppUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
refreshPlugins()
}
}
}