Add plugin settings screen
This commit is contained in:
parent
7c20d541cd
commit
e7ae751340
@ -1,9 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<permission-group
|
||||||
|
android:name="de.mm20.launcher2.permission-group.PLUGINS"
|
||||||
|
android:label="Kvaesitso plugins" />
|
||||||
<permission
|
<permission
|
||||||
android:name="de.mm20.launcher2.permission.USE_PLUGINS"
|
android:name="de.mm20.launcher2.permission.USE_PLUGINS"
|
||||||
android:label="Use Kvaesitso plugins"
|
android:label="@string/app_name"
|
||||||
|
android:description="@string/app_name"
|
||||||
|
android:icon="@drawable/ic_launcher_monochrome"
|
||||||
|
android:permissionGroup="de.mm20.launcher2.permission-group.PLUGINS"
|
||||||
android:protectionLevel="dangerous" />
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -143,6 +143,7 @@ dependencies {
|
|||||||
implementation(project(":libs:g-services"))
|
implementation(project(":libs:g-services"))
|
||||||
implementation(project(":libs:owncloud"))
|
implementation(project(":libs:owncloud"))
|
||||||
implementation(project(":services:accounts"))
|
implementation(project(":services:accounts"))
|
||||||
|
implementation(project(":services:plugins"))
|
||||||
implementation(project(":services:backup"))
|
implementation(project(":services:backup"))
|
||||||
implementation(project(":data:search-actions"))
|
implementation(project(":data:search-actions"))
|
||||||
implementation(project(":services:global-actions"))
|
implementation(project(":services:global-actions"))
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -18,7 +20,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
fun LargeMessage(
|
fun LargeMessage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
text: String
|
text: String,
|
||||||
|
color: Color = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@ -30,12 +33,14 @@ fun LargeMessage(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 24.dp)
|
.padding(bottom = 24.dp)
|
||||||
.size(64.dp)
|
.size(64.dp),
|
||||||
|
tint = color
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
color = color
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,21 +2,23 @@ package de.mm20.launcher2.ui.settings
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
|
||||||
import com.google.accompanist.navigation.animation.composable
|
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
|
||||||
import de.mm20.launcher2.licenses.AppLicense
|
import de.mm20.launcher2.licenses.AppLicense
|
||||||
import de.mm20.launcher2.licenses.OpenSourceLicenses
|
import de.mm20.launcher2.licenses.OpenSourceLicenses
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
|
||||||
import de.mm20.launcher2.ui.base.BaseActivity
|
import de.mm20.launcher2.ui.base.BaseActivity
|
||||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
import de.mm20.launcher2.ui.base.ProvideSettings
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
@ -45,6 +47,7 @@ import de.mm20.launcher2.ui.settings.license.LicenseScreen
|
|||||||
import de.mm20.launcher2.ui.settings.log.LogScreen
|
import de.mm20.launcher2.ui.settings.log.LogScreen
|
||||||
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
|
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.media.MediaIntegrationSettingsScreen
|
import de.mm20.launcher2.ui.settings.media.MediaIntegrationSettingsScreen
|
||||||
|
import de.mm20.launcher2.ui.settings.plugins.PluginsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
|
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
|
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
|
||||||
@ -53,20 +56,17 @@ import de.mm20.launcher2.ui.settings.weather.WeatherIntegrationSettingsScreen
|
|||||||
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||||
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
|
|
||||||
private val dataStore: LauncherDataStore by inject()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberAnimatedNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
LaunchedEffect(intent) {
|
LaunchedEffect(intent) {
|
||||||
intent.getStringExtra(EXTRA_ROUTE)
|
intent.getStringExtra(EXTRA_ROUTE)
|
||||||
@ -80,13 +80,21 @@ class SettingsActivity : BaseActivity() {
|
|||||||
ProvideSettings {
|
ProvideSettings {
|
||||||
LauncherTheme {
|
LauncherTheme {
|
||||||
OverlayHost {
|
OverlayHost {
|
||||||
AnimatedNavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "settings",
|
startDestination = "settings",
|
||||||
exitTransition = { fadeOut(tween(300, 300)) },
|
exitTransition = {
|
||||||
enterTransition = { fadeIn(tween(200)) },
|
fadeOut() + scaleOut(targetScale = 0.5f)
|
||||||
popEnterTransition = { fadeIn(tween(0)) },
|
},
|
||||||
popExitTransition = { fadeOut(tween(200)) },
|
enterTransition = {
|
||||||
|
slideInHorizontally { it }
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
fadeIn() + scaleIn(initialScale = 0.5f)
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
slideOutHorizontally { it }
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
MainSettingsScreen()
|
MainSettingsScreen()
|
||||||
@ -156,6 +164,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/integrations") {
|
composable("settings/integrations") {
|
||||||
IntegrationsSettingsScreen()
|
IntegrationsSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/plugins") {
|
||||||
|
PluginsSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/about") {
|
composable("settings/about") {
|
||||||
AboutSettingsScreen()
|
AboutSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.settings.main
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Apps
|
import androidx.compose.material.icons.rounded.Apps
|
||||||
import androidx.compose.material.icons.rounded.BugReport
|
import androidx.compose.material.icons.rounded.BugReport
|
||||||
|
import androidx.compose.material.icons.rounded.Extension
|
||||||
import androidx.compose.material.icons.rounded.Gesture
|
import androidx.compose.material.icons.rounded.Gesture
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
@ -74,6 +75,14 @@ fun MainSettingsScreen() {
|
|||||||
navController?.navigate("settings/integrations")
|
navController?.navigate("settings/integrations")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Preference(
|
||||||
|
icon = Icons.Rounded.Extension,
|
||||||
|
title = stringResource(id = R.string.preference_screen_plugins),
|
||||||
|
summary = stringResource(id = R.string.preference_screen_plugins_summary),
|
||||||
|
onClick = {
|
||||||
|
navController?.navigate("settings/plugins")
|
||||||
|
}
|
||||||
|
)
|
||||||
Preference(
|
Preference(
|
||||||
icon = Icons.Rounded.SettingsBackupRestore,
|
icon = Icons.Rounded.SettingsBackupRestore,
|
||||||
title = stringResource(id = R.string.preference_screen_backup),
|
title = stringResource(id = R.string.preference_screen_backup),
|
||||||
|
|||||||
@ -0,0 +1,119 @@
|
|||||||
|
package de.mm20.launcher2.ui.settings.plugins
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Extension
|
||||||
|
import androidx.compose.material.icons.rounded.ExtensionOff
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.LargeMessage
|
||||||
|
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||||
|
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||||
|
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PluginsSettingsScreen() {
|
||||||
|
val viewModel: PluginsSettingsScreenVM = viewModel()
|
||||||
|
val hostInstalled by viewModel.hostInstalled.collectAsState(null)
|
||||||
|
val hasPermission by viewModel.hasPermission.collectAsState(null)
|
||||||
|
val context = LocalContext.current
|
||||||
|
val plugins by viewModel.plugins.collectAsState(null)
|
||||||
|
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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins?.isEmpty() == true -> {
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillParentMaxHeight()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
LargeMessage(
|
||||||
|
icon = Icons.Rounded.Extension,
|
||||||
|
text = stringResource(R.string.no_plugins_installed),
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins != null -> {
|
||||||
|
items(plugins!!) { item ->
|
||||||
|
val icon by remember(item.plugin.authority) {
|
||||||
|
viewModel.getIcon(item.plugin)
|
||||||
|
}.collectAsState(null)
|
||||||
|
Preference(
|
||||||
|
title = { Text(item.plugin.label) },
|
||||||
|
summary = item.plugin.description?.let { { Text(it) } },
|
||||||
|
controls = {
|
||||||
|
Switch(checked = item.plugin.enabled, onCheckedChange = {
|
||||||
|
viewModel.setPluginEnabled(item.plugin, it)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
AsyncImage(model = icon, contentDescription = null, modifier = Modifier.size(36.dp))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.setPluginEnabled(item.plugin, !item.plugin.enabled)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package de.mm20.launcher2.ui.settings.plugins
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
|
import de.mm20.launcher2.plugin.Plugin
|
||||||
|
import de.mm20.launcher2.plugins.PluginService
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
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 plugins = hasPermission.flatMapLatest {
|
||||||
|
if (it) pluginService.getPluginsWithState() else emptyFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPluginEnabled(plugin: Plugin, value: Boolean) {
|
||||||
|
if (value) {
|
||||||
|
pluginService.enablePlugin(plugin)
|
||||||
|
} else {
|
||||||
|
pluginService.disablePlugin(plugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestPermission(context: Context) {
|
||||||
|
permissionsManager.requestPermission(context as AppCompatActivity, PermissionGroup.Plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIcon(plugin: Plugin) = flow {
|
||||||
|
emit(pluginService.getPluginIcon(plugin))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package de.mm20.launcher2.plugin
|
package de.mm20.launcher2.plugin
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PluginRepository {
|
interface PluginRepository {
|
||||||
@ -10,8 +12,8 @@ interface PluginRepository {
|
|||||||
): Flow<List<Plugin>>
|
): Flow<List<Plugin>>
|
||||||
fun get(authority: String): Flow<Plugin?>
|
fun get(authority: String): Flow<Plugin?>
|
||||||
|
|
||||||
fun insertMany(plugins: List<Plugin>)
|
fun insertMany(plugins: List<Plugin>): Job
|
||||||
fun insert(plugin: Plugin)
|
fun insert(plugin: Plugin): Job
|
||||||
fun update(plugin: Plugin)
|
fun update(plugin: Plugin): Job
|
||||||
fun deleteMany()
|
fun deleteMany(): Job
|
||||||
}
|
}
|
||||||
@ -421,6 +421,7 @@
|
|||||||
<string name="missing_permission_appshortcuts_search">Set %1$s as default home app to search app shortcuts.</string>
|
<string name="missing_permission_appshortcuts_search">Set %1$s as default home app to search app shortcuts.</string>
|
||||||
<!-- Missing permission app shortcuts permission, used when creating a shortcut in the edit favorites sheet -->
|
<!-- Missing permission app shortcuts permission, used when creating a shortcut in the edit favorites sheet -->
|
||||||
<string name="missing_permission_appshortcuts_create">Set %1$s as default home app to create shortcuts.</string>
|
<string name="missing_permission_appshortcuts_create">Set %1$s as default home app to create shortcuts.</string>
|
||||||
|
<string name="missing_permission_plugins">Plugin permission is required to use plugins.</string>
|
||||||
<!-- Grant a permission, shown in permission banners -->
|
<!-- Grant a permission, shown in permission banners -->
|
||||||
<string name="grant_permission">Grant</string>
|
<string name="grant_permission">Grant</string>
|
||||||
<!-- Appearance preference title -->
|
<!-- Appearance preference title -->
|
||||||
@ -503,6 +504,10 @@
|
|||||||
<string name="preference_icon_shape_hexagon">Hexagon</string>
|
<string name="preference_icon_shape_hexagon">Hexagon</string>
|
||||||
<string name="preference_category_searchbar">Search bar</string>
|
<string name="preference_category_searchbar">Search bar</string>
|
||||||
<string name="preference_screen_integrations">Integrations</string>
|
<string name="preference_screen_integrations">Integrations</string>
|
||||||
|
<string name="preference_screen_plugins">Plugins</string>
|
||||||
|
<string name="preference_screen_plugins_summary">Manage installed extensions</string>
|
||||||
|
<string name="no_plugins_installed">No plugins installed</string>
|
||||||
|
<string name="plugin_host_not_installed">Plugin host not installed</string>
|
||||||
<string name="preference_theme_system">Follow system</string>
|
<string name="preference_theme_system">Follow system</string>
|
||||||
<string name="preference_themed_icons">Themed icons</string>
|
<string name="preference_themed_icons">Themed icons</string>
|
||||||
<string name="preference_themed_icons_summary">Color icons with the application\'s color scheme</string>
|
<string name="preference_themed_icons_summary">Color icons with the application\'s color scheme</string>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import de.mm20.launcher2.crashreporter.CrashReporter
|
|||||||
import de.mm20.launcher2.ktx.checkPermission
|
import de.mm20.launcher2.ktx.checkPermission
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.plugin.contracts.PluginContract
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ enum class PermissionGroup {
|
|||||||
Notifications,
|
Notifications,
|
||||||
AppShortcuts,
|
AppShortcuts,
|
||||||
Accessibility,
|
Accessibility,
|
||||||
|
Plugins,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PermissionsManagerImpl(
|
internal class PermissionsManagerImpl(
|
||||||
@ -83,6 +85,9 @@ 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(
|
||||||
@ -153,6 +158,14 @@ internal class PermissionsManagerImpl(
|
|||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionGroup.Plugins -> {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
context,
|
||||||
|
pluginPermissions,
|
||||||
|
permissionGroup.ordinal
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +183,10 @@ 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()
|
||||||
@ -201,6 +218,7 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.Notifications -> notificationsPermissionState
|
PermissionGroup.Notifications -> notificationsPermissionState
|
||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
||||||
PermissionGroup.Accessibility -> accessibilityPermissionState
|
PermissionGroup.Accessibility -> accessibilityPermissionState
|
||||||
|
PermissionGroup.Plugins -> pluginsPermissionState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +227,7 @@ internal class PermissionsManagerImpl(
|
|||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
grantResults: IntArray
|
grantResults: IntArray
|
||||||
) {
|
) {
|
||||||
val permissionGroup = PermissionGroup.values().getOrNull(requestCode) ?: return
|
val permissionGroup = PermissionGroup.entries.getOrNull(requestCode) ?: return
|
||||||
val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||||
when (permissionGroup) {
|
when (permissionGroup) {
|
||||||
PermissionGroup.Calendar -> calendarPermissionState.value = granted
|
PermissionGroup.Calendar -> calendarPermissionState.value = granted
|
||||||
@ -219,6 +237,7 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,5 +280,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.mm20.launcher2.database.daos
|
|||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import de.mm20.launcher2.database.entities.PluginEntity
|
import de.mm20.launcher2.database.entities.PluginEntity
|
||||||
@ -26,15 +27,15 @@ interface PluginDao {
|
|||||||
@Query("SELECT * FROM Plugins WHERE authority = :authority")
|
@Query("SELECT * FROM Plugins WHERE authority = :authority")
|
||||||
fun get(authority: String): Flow<PluginEntity>
|
fun get(authority: String): Flow<PluginEntity>
|
||||||
|
|
||||||
@Insert
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insertMany(plugins: List<PluginEntity>)
|
suspend fun insertMany(plugins: List<PluginEntity>)
|
||||||
|
|
||||||
@Insert
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(plugin: PluginEntity)
|
suspend fun insert(plugin: PluginEntity)
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
fun update(plugin: PluginEntity)
|
suspend fun update(plugin: PluginEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM Plugins")
|
@Query("DELETE FROM Plugins")
|
||||||
fun deleteMany()
|
suspend fun deleteMany()
|
||||||
}
|
}
|
||||||
@ -42,6 +42,7 @@ dependencies {
|
|||||||
implementation(libs.bundles.androidx.lifecycle)
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.coil.core)
|
||||||
|
|
||||||
implementation(project(":core:base"))
|
implementation(project(":core:base"))
|
||||||
implementation(project(":core:ktx"))
|
implementation(project(":core:ktx"))
|
||||||
|
|||||||
@ -2,9 +2,16 @@ package de.mm20.launcher2.files.providers
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
import de.mm20.launcher2.files.PluginFileSerializer
|
import de.mm20.launcher2.files.PluginFileSerializer
|
||||||
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.icons.StaticIconLayer
|
||||||
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||||
import de.mm20.launcher2.search.File
|
import de.mm20.launcher2.search.File
|
||||||
@ -47,6 +54,21 @@ data class PluginFile(
|
|||||||
return PluginFileSerializer()
|
return PluginFileSerializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? {
|
||||||
|
if (thumbnailUri != null) {
|
||||||
|
val request = ImageRequest.Builder(context)
|
||||||
|
.data(thumbnailUri)
|
||||||
|
.build()
|
||||||
|
val result = context.imageLoader.execute(request)
|
||||||
|
val drawable = result.drawable ?: return null
|
||||||
|
return StaticLauncherIcon(
|
||||||
|
foregroundLayer = StaticIconLayer(icon = drawable, scale = 1.5f),
|
||||||
|
backgroundLayer = ColorLayer(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val Domain = "plugin.file"
|
const val Domain = "plugin.file"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import de.mm20.launcher2.plugin.Plugin
|
import de.mm20.launcher2.plugin.Plugin
|
||||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||||
@ -36,7 +37,13 @@ class PluginFileProvider(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
cancellationSignal
|
cancellationSignal
|
||||||
) ?: return@suspendCancellableCoroutine it.resume(emptyList<File>())
|
)
|
||||||
|
|
||||||
|
if (cursor == null) {
|
||||||
|
Log.e("MM20", "Plugin ${plugin.authority} returned null cursor")
|
||||||
|
it.resume(emptyList())
|
||||||
|
return@suspendCancellableCoroutine
|
||||||
|
}
|
||||||
|
|
||||||
val results = fromCursor(cursor) ?: emptyList()
|
val results = fromCursor(cursor) ?: emptyList()
|
||||||
it.resume(results)
|
it.resume(results)
|
||||||
|
|||||||
@ -4,12 +4,18 @@ import de.mm20.launcher2.database.daos.PluginDao
|
|||||||
import de.mm20.launcher2.plugin.Plugin
|
import de.mm20.launcher2.plugin.Plugin
|
||||||
import de.mm20.launcher2.plugin.PluginRepository
|
import de.mm20.launcher2.plugin.PluginRepository
|
||||||
import de.mm20.launcher2.plugin.PluginType
|
import de.mm20.launcher2.plugin.PluginType
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
internal class PluginRepositoryImpl(
|
internal class PluginRepositoryImpl(
|
||||||
private val dao: PluginDao,
|
private val dao: PluginDao,
|
||||||
): PluginRepository {
|
) : PluginRepository {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
override fun findMany(
|
override fun findMany(
|
||||||
type: PluginType?,
|
type: PluginType?,
|
||||||
enabled: Boolean?,
|
enabled: Boolean?,
|
||||||
@ -28,19 +34,27 @@ internal class PluginRepositoryImpl(
|
|||||||
return dao.get(authority).map { Plugin(it) }
|
return dao.get(authority).map { Plugin(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insertMany(plugins: List<Plugin>) {
|
override fun insertMany(plugins: List<Plugin>): Job {
|
||||||
TODO("Not yet implemented")
|
return scope.launch {
|
||||||
|
dao.insertMany(plugins.map { PluginEntity(it) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insert(plugin: Plugin) {
|
override fun insert(plugin: Plugin): Job {
|
||||||
dao.insert(PluginEntity(plugin))
|
return scope.launch {
|
||||||
|
dao.insert(PluginEntity(plugin))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(plugin: Plugin) {
|
override fun update(plugin: Plugin): Job {
|
||||||
dao.update(PluginEntity(plugin))
|
return scope.launch {
|
||||||
|
dao.update(PluginEntity(plugin))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMany() {
|
override fun deleteMany(): Job {
|
||||||
dao.deleteMany()
|
return scope.launch {
|
||||||
|
dao.deleteMany()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package de.mm20.launcher2.sdk.base
|
package de.mm20.launcher2.sdk.base
|
||||||
|
|
||||||
import android.content.ContentProvider
|
import android.content.ContentProvider
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.plugin.PluginState
|
import de.mm20.launcher2.plugin.PluginState
|
||||||
import de.mm20.launcher2.plugin.PluginType
|
import de.mm20.launcher2.plugin.PluginType
|
||||||
@ -43,4 +45,11 @@ abstract class BasePluginProvider : ContentProvider() {
|
|||||||
return PluginState.Ready
|
return PluginState.Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun checkPermissionOrThrow(context: Context) {
|
||||||
|
if (context.checkCallingPermission(PluginContract.Permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw SecurityException("Caller does not have permission to use plugins")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ abstract class SearchPluginProvider<T> : BasePluginProvider() {
|
|||||||
val context = context ?: return null
|
val context = context ?: return null
|
||||||
checkPermissionOrThrow(context)
|
checkPermissionOrThrow(context)
|
||||||
when {
|
when {
|
||||||
uri.path == SearchPluginContract.Paths.Search -> {
|
uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Search -> {
|
||||||
val query =
|
val query =
|
||||||
uri.getQueryParameter(SearchPluginContract.Paths.QueryParam) ?: return null
|
uri.getQueryParameter(SearchPluginContract.Paths.QueryParam) ?: return null
|
||||||
val results = search(query, cancellationSignal)
|
val results = search(query, cancellationSignal)
|
||||||
@ -53,7 +53,7 @@ abstract class SearchPluginProvider<T> : BasePluginProvider() {
|
|||||||
for (result in results) {
|
for (result in results) {
|
||||||
writeToCursor(cursor, result)
|
writeToCursor(cursor, result)
|
||||||
}
|
}
|
||||||
return null
|
return cursor
|
||||||
}
|
}
|
||||||
uri.pathSegments.size == 2 && uri.pathSegments.first() == SearchPluginContract.Paths.Root -> {
|
uri.pathSegments.size == 2 && uri.pathSegments.first() == SearchPluginContract.Paths.Root -> {
|
||||||
val id = uri.pathSegments[1]
|
val id = uri.pathSegments[1]
|
||||||
@ -110,12 +110,4 @@ abstract class SearchPluginProvider<T> : BasePluginProvider() {
|
|||||||
|
|
||||||
internal abstract fun createCursor(capacity: Int): MatrixCursor
|
internal abstract fun createCursor(capacity: Int): MatrixCursor
|
||||||
internal abstract fun writeToCursor(cursor: MatrixCursor, item: T)
|
internal abstract fun writeToCursor(cursor: MatrixCursor, item: T)
|
||||||
|
|
||||||
|
|
||||||
private fun checkPermissionOrThrow(context: Context) {
|
|
||||||
if (context.checkCallingPermission(PluginContract.Permission) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw SecurityException("Caller does not have permission to use plugins")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,16 +1,15 @@
|
|||||||
package de.mm20.launcher2.sdk.files
|
package de.mm20.launcher2.sdk.files
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.MatrixCursor
|
import android.database.MatrixCursor
|
||||||
import android.net.Uri
|
import de.mm20.launcher2.plugin.PluginType
|
||||||
import de.mm20.launcher2.plugin.contracts.FilePluginContract
|
import de.mm20.launcher2.plugin.contracts.FilePluginContract
|
||||||
import de.mm20.launcher2.sdk.base.SearchPluginProvider
|
import de.mm20.launcher2.sdk.base.SearchPluginProvider
|
||||||
|
|
||||||
abstract class FileProvider : SearchPluginProvider<File>() {
|
abstract class FileProvider : SearchPluginProvider<File>() {
|
||||||
abstract override suspend fun search(query: String): List<File>
|
abstract override suspend fun search(query: String): List<File>
|
||||||
|
|
||||||
final override fun getPluginType(): de.mm20.launcher2.plugin.PluginType {
|
final override fun getPluginType(): PluginType {
|
||||||
return de.mm20.launcher2.plugin.PluginType.FileSearch
|
return PluginType.FileSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCursor(capacity: Int): MatrixCursor {
|
override fun createCursor(capacity: Int): MatrixCursor {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.plugin.Plugin
|
import de.mm20.launcher2.plugin.Plugin
|
||||||
import de.mm20.launcher2.plugin.PluginType
|
import de.mm20.launcher2.plugin.PluginType
|
||||||
|
|
||||||
@ -16,37 +18,44 @@ class PluginScanner(
|
|||||||
val plugins = mutableListOf<Plugin>()
|
val plugins = mutableListOf<Plugin>()
|
||||||
|
|
||||||
for (cr in contentResolvers) {
|
for (cr in contentResolvers) {
|
||||||
val providerInfo = cr.providerInfo ?: continue
|
try {
|
||||||
val authority = providerInfo.authority ?: continue
|
val providerInfo = cr.providerInfo ?: continue
|
||||||
val bundle = context.contentResolver.call(
|
val authority = providerInfo.authority ?: continue
|
||||||
Uri.Builder()
|
val bundle = context.contentResolver.call(
|
||||||
.scheme("content")
|
Uri.Builder()
|
||||||
.authority(authority)
|
.scheme("content")
|
||||||
.build(),
|
.authority(authority)
|
||||||
"getType",
|
.build(),
|
||||||
null,
|
"getType",
|
||||||
null,
|
null,
|
||||||
) ?: continue
|
null,
|
||||||
val type = bundle.getString("type")
|
) ?: continue
|
||||||
?.let {
|
val type = bundle.getString("type")
|
||||||
try {
|
?.let {
|
||||||
PluginType.valueOf(it)
|
try {
|
||||||
} catch (e: IllegalArgumentException) {
|
PluginType.valueOf(it)
|
||||||
null
|
} catch (e: IllegalArgumentException) {
|
||||||
}
|
null
|
||||||
} ?: continue
|
}
|
||||||
plugins.add(
|
} ?: continue
|
||||||
Plugin(
|
plugins.add(
|
||||||
label = cr.loadLabel(context.packageManager).toString(),
|
Plugin(
|
||||||
description = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.description"),
|
label = cr.loadLabel(context.packageManager).toString(),
|
||||||
packageName = providerInfo.packageName,
|
description = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.description"),
|
||||||
className = providerInfo.name,
|
packageName = providerInfo.packageName,
|
||||||
type = type,
|
className = providerInfo.name,
|
||||||
authority = authority,
|
type = type,
|
||||||
settingsActivity = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.settings"),
|
authority = authority,
|
||||||
enabled = false,
|
settingsActivity = providerInfo.metaData?.getString("de.mm20.launcher2.plugin.settings"),
|
||||||
|
enabled = false,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} catch (e: SecurityException) {
|
||||||
|
continue
|
||||||
|
} catch (e: Exception) {
|
||||||
|
CrashReporter.logException(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
|
|||||||
@ -2,9 +2,12 @@ package de.mm20.launcher2.plugins
|
|||||||
|
|
||||||
import PluginScanner
|
import PluginScanner
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import de.mm20.launcher2.plugin.Plugin
|
import de.mm20.launcher2.plugin.Plugin
|
||||||
@ -16,6 +19,7 @@ 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.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -32,7 +36,11 @@ interface PluginService {
|
|||||||
fun enablePlugin(plugin: Plugin)
|
fun enablePlugin(plugin: Plugin)
|
||||||
fun disablePlugin(plugin: Plugin)
|
fun disablePlugin(plugin: Plugin)
|
||||||
fun getPluginsWithState(type: PluginType? = null): Flow<List<PluginWithState>>
|
fun getPluginsWithState(type: PluginType? = null): Flow<List<PluginWithState>>
|
||||||
|
|
||||||
|
fun isPluginHostInstalled(): Flow<Boolean>
|
||||||
suspend fun getPluginState(plugin: Plugin): PluginState?
|
suspend fun getPluginState(plugin: Plugin): PluginState?
|
||||||
|
|
||||||
|
suspend fun getPluginIcon(plugin: Plugin): Drawable?
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PluginServiceImpl(
|
internal class PluginServiceImpl(
|
||||||
@ -42,6 +50,10 @@ internal class PluginServiceImpl(
|
|||||||
|
|
||||||
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()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refreshPlugins()
|
refreshPlugins()
|
||||||
ContextCompat.registerReceiver(
|
ContextCompat.registerReceiver(
|
||||||
@ -65,9 +77,16 @@ internal class PluginServiceImpl(
|
|||||||
repository.update(plugin.copy(enabled = false))
|
repository.update(plugin.copy(enabled = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private fun refreshPlugins() {
|
private fun refreshPlugins() {
|
||||||
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 enabledPlugins =
|
val enabledPlugins =
|
||||||
repository.findMany(enabled = true).first().map { it.authority }
|
repository.findMany(enabled = true).first().map { it.authority }
|
||||||
@ -79,8 +98,8 @@ internal class PluginServiceImpl(
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repository.deleteMany()
|
repository.deleteMany().join()
|
||||||
repository.insertMany(plugins)
|
repository.insertMany(plugins).join()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,6 +147,26 @@ internal class PluginServiceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isPluginHostInstalled(): Flow<Boolean> {
|
||||||
|
return pluginHostInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPluginIcon(plugin: Plugin): Drawable? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val info = try {
|
||||||
|
context.packageManager.getProviderInfo(
|
||||||
|
ComponentName(
|
||||||
|
plugin.packageName,
|
||||||
|
plugin.className
|
||||||
|
), 0
|
||||||
|
)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
info.loadIcon(context.packageManager) ?: info.applicationInfo?.loadIcon(context.packageManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class AppUpdateReceiver : BroadcastReceiver() {
|
private inner class AppUpdateReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
refreshPlugins()
|
refreshPlugins()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user