manager: support App Profile template
This commit is contained in:
@@ -45,7 +45,6 @@ object Natives {
|
||||
external fun setAppProfile(profile: Profile?): Boolean
|
||||
|
||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||
private const val ROOT_DEFAULT_PROFILE_KEY = "#"
|
||||
private const val NOBODY_UID = 9999
|
||||
|
||||
fun setDefaultUmountModules(umountModules: Boolean): Boolean {
|
||||
@@ -90,16 +89,16 @@ object Natives {
|
||||
val groups: List<Int> = mutableListOf(),
|
||||
val capabilities: List<Int> = mutableListOf(),
|
||||
val context: String = "u:r:su:s0",
|
||||
val namespace: Int = Namespace.Inherited.ordinal,
|
||||
val namespace: Int = Namespace.INHERITED.ordinal,
|
||||
|
||||
val nonRootUseDefault: Boolean = true,
|
||||
val umountModules: Boolean = true,
|
||||
var rules: String = "", // this field is save in ksud!!
|
||||
) : Parcelable {
|
||||
enum class Namespace {
|
||||
Inherited,
|
||||
Global,
|
||||
Individual,
|
||||
INHERITED,
|
||||
GLOBAL,
|
||||
INDIVIDUAL,
|
||||
}
|
||||
|
||||
constructor() : this("")
|
||||
|
||||
@@ -74,9 +74,9 @@ fun RootProfileConfig(
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val currentNamespace = when (profile.namespace) {
|
||||
Natives.Profile.Namespace.Inherited.ordinal -> stringResource(R.string.profile_namespace_inherited)
|
||||
Natives.Profile.Namespace.Global.ordinal -> stringResource(R.string.profile_namespace_global)
|
||||
Natives.Profile.Namespace.Individual.ordinal -> stringResource(R.string.profile_namespace_individual)
|
||||
Natives.Profile.Namespace.INHERITED.ordinal -> stringResource(R.string.profile_namespace_inherited)
|
||||
Natives.Profile.Namespace.GLOBAL.ordinal -> stringResource(R.string.profile_namespace_global)
|
||||
Natives.Profile.Namespace.INDIVIDUAL.ordinal -> stringResource(R.string.profile_namespace_individual)
|
||||
else -> stringResource(R.string.profile_namespace_inherited)
|
||||
}
|
||||
ListItem(headlineContent = {
|
||||
@@ -104,21 +104,21 @@ fun RootProfileConfig(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_namespace_inherited)) },
|
||||
onClick = {
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Inherited.ordinal))
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INHERITED.ordinal))
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_namespace_global)) },
|
||||
onClick = {
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Global.ordinal))
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.GLOBAL.ordinal))
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_namespace_individual)) },
|
||||
onClick = {
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Individual.ordinal))
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INDIVIDUAL.ordinal))
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package me.weishu.kernelsu.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowDropUp
|
||||
import androidx.compose.material.icons.filled.ReadMore
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.util.listAppProfileTemplates
|
||||
import me.weishu.kernelsu.ui.util.setSepolicy
|
||||
import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/21.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TemplateConfig(
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var template by rememberSaveable {
|
||||
mutableStateOf(profile.rootTemplate ?: "")
|
||||
}
|
||||
val profileTemplates = listAppProfileTemplates()
|
||||
|
||||
ListItem(headlineContent = {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
value = template,
|
||||
onValueChange = {},
|
||||
trailingIcon = {
|
||||
if (expanded) Icon(Icons.Filled.ArrowDropUp, null)
|
||||
else Icon(Icons.Filled.ArrowDropDown, null)
|
||||
},
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
profileTemplates.forEach { tid ->
|
||||
val templateInfo =
|
||||
getTemplateInfoById(tid) ?: return@forEach
|
||||
DropdownMenuItem(
|
||||
text = { Text(tid) },
|
||||
onClick = {
|
||||
template = tid
|
||||
if (setSepolicy(tid, templateInfo.rules.joinToString("\n"))) {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
rootTemplate = tid,
|
||||
rootUseDefault = false,
|
||||
uid = templateInfo.uid,
|
||||
gid = templateInfo.gid,
|
||||
groups = templateInfo.groups,
|
||||
capabilities = templateInfo.capabilities,
|
||||
context = templateInfo.context,
|
||||
namespace = templateInfo.namespace,
|
||||
)
|
||||
)
|
||||
}
|
||||
expanded = false
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
onViewTemplate(tid)
|
||||
}) {
|
||||
Icon(Icons.Filled.ReadMore, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,19 +18,15 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowDropUp
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -61,6 +57,8 @@ import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.SwitchItem
|
||||
import me.weishu.kernelsu.ui.component.profile.AppProfileConfig
|
||||
import me.weishu.kernelsu.ui.component.profile.RootProfileConfig
|
||||
import me.weishu.kernelsu.ui.component.profile.TemplateConfig
|
||||
import me.weishu.kernelsu.ui.screen.destinations.TemplateEditorScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||
import me.weishu.kernelsu.ui.util.forceStopApp
|
||||
import me.weishu.kernelsu.ui.util.getSepolicy
|
||||
@@ -68,6 +66,7 @@ import me.weishu.kernelsu.ui.util.launchApp
|
||||
import me.weishu.kernelsu.ui.util.restartApp
|
||||
import me.weishu.kernelsu.ui.util.setSepolicy
|
||||
import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
|
||||
import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -107,9 +106,7 @@ fun AppProfileScreen(
|
||||
appLabel = appInfo.label,
|
||||
appIcon = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(appInfo.packageInfo)
|
||||
.crossfade(true)
|
||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true)
|
||||
.build(),
|
||||
contentDescription = appInfo.label,
|
||||
modifier = Modifier
|
||||
@@ -119,6 +116,11 @@ fun AppProfileScreen(
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onViewTemplate = {
|
||||
getTemplateInfoById(it)?.let { info->
|
||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||
}
|
||||
},
|
||||
onProfileChange = {
|
||||
scope.launch {
|
||||
if (it.allowSu && !it.rootUseDefault && it.rules.isNotEmpty()) {
|
||||
@@ -138,7 +140,6 @@ fun AppProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AppProfileInner(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -146,6 +147,7 @@ private fun AppProfileInner(
|
||||
appLabel: String,
|
||||
appIcon: @Composable () -> Unit,
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
@@ -179,7 +181,7 @@ private fun AppProfileInner(
|
||||
var mode by remember {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
ProfileBox(mode, false) {
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
@@ -188,43 +190,9 @@ private fun AppProfileInner(
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
if (currentMode == Mode.Template) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val templateNone = "None"
|
||||
var template by rememberSaveable {
|
||||
mutableStateOf(
|
||||
profile.rootTemplate
|
||||
?: templateNone
|
||||
)
|
||||
}
|
||||
ListItem(headlineContent = {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.menuAnchor(),
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
value = template,
|
||||
onValueChange = {
|
||||
if (template != templateNone) {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
rootTemplate = it,
|
||||
rootUseDefault = false
|
||||
)
|
||||
)
|
||||
template = it
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (expanded) Icon(Icons.Filled.ArrowDropUp, null)
|
||||
else Icon(Icons.Filled.ArrowDropDown, null)
|
||||
},
|
||||
)
|
||||
// TODO: Template
|
||||
}
|
||||
})
|
||||
TemplateConfig(profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onProfileChange = onProfileChange)
|
||||
} else if (mode == Mode.Custom) {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
@@ -254,9 +222,7 @@ private fun AppProfileInner(
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default),
|
||||
Template(R.string.profile_template),
|
||||
Custom(R.string.profile_custom);
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
|
||||
val text: String
|
||||
@Composable get() = stringResource(res)
|
||||
@@ -292,8 +258,7 @@ private fun ProfileBox(
|
||||
Divider(thickness = Dp.Hairline)
|
||||
ListItem(headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
@@ -331,8 +296,7 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
) {
|
||||
}) {
|
||||
|
||||
content()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.ContactPage
|
||||
import androidx.compose.material.icons.filled.Fence
|
||||
import androidx.compose.material.icons.filled.RemoveModerator
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -27,6 +28,7 @@ import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.AboutDialog
|
||||
import me.weishu.kernelsu.ui.component.LoadingDialog
|
||||
import me.weishu.kernelsu.ui.component.SwitchItem
|
||||
import me.weishu.kernelsu.ui.screen.destinations.AppProfileTemplateScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.LocalDialogHost
|
||||
import me.weishu.kernelsu.ui.util.getBugreportFile
|
||||
|
||||
@@ -56,6 +58,16 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val dialogHost = LocalDialogHost.current
|
||||
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
||||
headlineContent = { Text(profileTemplate) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary))},
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.screen.destinations.TemplateEditorScreenDestination
|
||||
import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/20.
|
||||
*/
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun AppProfileTemplateScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<TemplateViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.templateList.isEmpty()) {
|
||||
viewModel.fetchTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(onBack = { navigator.popBackStack() },
|
||||
onSync = {
|
||||
scope.launch { viewModel.fetchTemplates(true) }
|
||||
},
|
||||
onClickCreate = {
|
||||
navigator.navigate(
|
||||
TemplateEditorScreenDestination(
|
||||
TemplateViewModel.TemplateInfo(),
|
||||
false
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
) { innerPadding ->
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = viewModel.isRefreshing,
|
||||
onRefresh = { scope.launch { viewModel.fetchTemplates() } },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.pullRefresh(refreshState)
|
||||
) {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
items(viewModel.templateList, key = { it.id }) { app ->
|
||||
TemplateItem(navigator, app)
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.isRefreshing,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun TemplateItem(
|
||||
navigator: DestinationsNavigator,
|
||||
template: TemplateViewModel.TemplateInfo
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(TemplateEditorScreenDestination(template, !template.local))
|
||||
},
|
||||
headlineContent = { Text(template.name) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context)
|
||||
if (template.local) {
|
||||
LabelText(label = "local")
|
||||
} else {
|
||||
LabelText(label = "remote")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit, onSync: () -> Unit, onClickCreate: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.settings_profile_template))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSync) {
|
||||
Icon(Icons.Filled.Sync, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = onClickCreate) {
|
||||
Icon(Icons.Filled.Create, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.profile.Capabilities
|
||||
import me.weishu.kernelsu.profile.Groups
|
||||
import me.weishu.kernelsu.ui.component.profile.RootProfileConfig
|
||||
import me.weishu.kernelsu.ui.util.deleteAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.util.getAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.util.setAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/20.
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun TemplateEditorScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
initialTemplate: TemplateViewModel.TemplateInfo,
|
||||
readOnly: Boolean = true,
|
||||
) {
|
||||
|
||||
val isCreation = initialTemplate.id.isBlank()
|
||||
val autoSave = !isCreation
|
||||
|
||||
var template by rememberSaveable {
|
||||
mutableStateOf(initialTemplate)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val author =
|
||||
if (initialTemplate.author.isNotEmpty()) "@${initialTemplate.author}" else ""
|
||||
val readOnlyHint = if (readOnly) {
|
||||
" - ${stringResource(id = R.string.app_profile_template_readonly)}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val titleSummary = "${initialTemplate.id}$author$readOnlyHint"
|
||||
|
||||
TopBar(
|
||||
title = if (isCreation) {
|
||||
stringResource(R.string.app_profile_template_create)
|
||||
} else if (readOnly) {
|
||||
stringResource(R.string.app_profile_template_view)
|
||||
} else {
|
||||
stringResource(R.string.app_profile_template_edit)
|
||||
},
|
||||
readOnly = readOnly,
|
||||
summary = titleSummary,
|
||||
onBack = { navigator.popBackStack() },
|
||||
onDelete = {
|
||||
if (deleteAppProfileTemplate(template.id)) {
|
||||
navigator.popBackStack()
|
||||
}
|
||||
},
|
||||
onSave = {
|
||||
if (saveTemplate(template, isCreation)) {
|
||||
navigator.popBackStack()
|
||||
}
|
||||
})
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.pointerInteropFilter {
|
||||
// disable click and ripple if readOnly
|
||||
readOnly
|
||||
}
|
||||
) {
|
||||
if (isCreation) {
|
||||
var errorHint by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val idConflictError = stringResource(id = R.string.app_profile_template_id_exist)
|
||||
val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid)
|
||||
TextEdit(
|
||||
label = stringResource(id = R.string.app_profile_template_id),
|
||||
text = template.id,
|
||||
errorHint = errorHint,
|
||||
isError = errorHint.isNotEmpty()
|
||||
) { value ->
|
||||
errorHint = if (isTemplateExist(value)) {
|
||||
idConflictError
|
||||
} else if (!isValidTemplateId(value)) {
|
||||
idInvalidError
|
||||
} else {
|
||||
""
|
||||
}
|
||||
template = template.copy(id = value)
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit(
|
||||
label = stringResource(id = R.string.app_profile_template_name),
|
||||
text = template.name
|
||||
) { value ->
|
||||
template.copy(name = value).run {
|
||||
if (autoSave) {
|
||||
if (!saveTemplate(this)) {
|
||||
// failed
|
||||
return@run
|
||||
}
|
||||
}
|
||||
template = this
|
||||
}
|
||||
}
|
||||
TextEdit(
|
||||
label = stringResource(id = R.string.app_profile_template_description),
|
||||
text = template.description
|
||||
) { value ->
|
||||
template.copy(description = value).run {
|
||||
if (autoSave) {
|
||||
if (!saveTemplate(this)) {
|
||||
// failed
|
||||
return@run
|
||||
}
|
||||
}
|
||||
template = this
|
||||
}
|
||||
}
|
||||
|
||||
RootProfileConfig(fixedName = true,
|
||||
profile = toNativeProfile(template),
|
||||
onProfileChange = {
|
||||
template.copy(
|
||||
uid = it.uid,
|
||||
gid = it.gid,
|
||||
groups = it.groups,
|
||||
capabilities = it.capabilities,
|
||||
context = it.context,
|
||||
namespace = it.namespace,
|
||||
rules = it.rules.split("\n")
|
||||
).run {
|
||||
if (autoSave) {
|
||||
if (!saveTemplate(this)) {
|
||||
// failed
|
||||
return@run
|
||||
}
|
||||
}
|
||||
template = this
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toNativeProfile(templateInfo: TemplateViewModel.TemplateInfo): Natives.Profile {
|
||||
return Natives.Profile().copy(rootTemplate = templateInfo.id,
|
||||
uid = templateInfo.uid,
|
||||
gid = templateInfo.gid,
|
||||
groups = templateInfo.groups,
|
||||
capabilities = templateInfo.capabilities,
|
||||
context = templateInfo.context,
|
||||
namespace = templateInfo.namespace,
|
||||
rules = templateInfo.rules.joinToString("\n").ifBlank { "" })
|
||||
}
|
||||
|
||||
fun isTemplateValid(template: TemplateViewModel.TemplateInfo): Boolean {
|
||||
if (template.id.isBlank()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isValidTemplateId(template.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = false): Boolean {
|
||||
if (!isTemplateValid(template)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isCreation && isTemplateExist(template.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
template.apply {
|
||||
val json = JSONObject().apply {
|
||||
put("id", id)
|
||||
put("name", name.ifBlank { id })
|
||||
put("description", description.ifBlank { id })
|
||||
put("local", true)
|
||||
|
||||
put("uid", uid)
|
||||
put("gid", gid)
|
||||
put("groups", JSONArray().apply {
|
||||
Groups.values().forEach { group ->
|
||||
if (group.gid in groups) {
|
||||
put(group.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
put("capabilities", JSONArray().apply {
|
||||
Capabilities.values().forEach { capability ->
|
||||
if (capability.cap in capabilities) {
|
||||
put(capability.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
put("context", context)
|
||||
put("namespace", Natives.Profile.Namespace.values()[namespace].name)
|
||||
put("rules", JSONArray().apply {
|
||||
rules.forEach { rule ->
|
||||
put(rule)
|
||||
}
|
||||
})
|
||||
}
|
||||
return setAppProfileTemplate(id, json.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
title: String,
|
||||
readOnly: Boolean,
|
||||
summary: String = "",
|
||||
onBack: () -> Unit,
|
||||
onDelete: () -> Unit = {},
|
||||
onSave: () -> Unit = {}
|
||||
) {
|
||||
TopAppBar(title = {
|
||||
Column {
|
||||
Text(title)
|
||||
if (summary.isNotBlank()) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}, navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
|
||||
}, actions = {
|
||||
if (readOnly) {
|
||||
return@TopAppBar
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Filled.DeleteForever,
|
||||
contentDescription = stringResource(id = R.string.app_profile_template_delete)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = stringResource(id = R.string.app_profile_template_save)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun TextEdit(
|
||||
label: String,
|
||||
text: String,
|
||||
errorHint: String = "",
|
||||
isError: Boolean = false,
|
||||
onValueChange: (String) -> Unit = {}
|
||||
) {
|
||||
ListItem(headlineContent = {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(label) },
|
||||
suffix =
|
||||
if (errorHint.isNotBlank()) {
|
||||
{
|
||||
Text(
|
||||
text = if (isError) errorHint else "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
isError = isError,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
}),
|
||||
onValueChange = onValueChange
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun isValidTemplateId(id: String): Boolean {
|
||||
return Regex("""^([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*$""").matches(id)
|
||||
}
|
||||
|
||||
private fun isTemplateExist(id: String): Boolean {
|
||||
return getAppProfileTemplate(id).isNotBlank()
|
||||
}
|
||||
@@ -91,7 +91,12 @@ fun uninstallModule(id: String): Boolean {
|
||||
return result
|
||||
}
|
||||
|
||||
fun installModule(uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit): Boolean {
|
||||
fun installModule(
|
||||
uri: Uri,
|
||||
onFinish: (Boolean) -> Unit,
|
||||
onStdout: (String) -> Unit,
|
||||
onStderr: (String) -> Unit
|
||||
): Boolean {
|
||||
val resolver = ksuApp.contentResolver
|
||||
with(resolver.openInputStream(uri)) {
|
||||
val file = File(ksuApp.cacheDir, "module.zip")
|
||||
@@ -115,7 +120,8 @@ fun installModule(uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> U
|
||||
}
|
||||
|
||||
val result =
|
||||
shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback).exec()
|
||||
shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback)
|
||||
.exec()
|
||||
Log.i("KernelSU", "install module $uri result: $result")
|
||||
|
||||
file.delete()
|
||||
@@ -153,14 +159,16 @@ fun isSepolicyValid(rules: String?): Boolean {
|
||||
}
|
||||
val shell = getRootShell()
|
||||
val result =
|
||||
shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null).exec()
|
||||
shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null)
|
||||
.exec()
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun getSepolicy(pkg: String): String {
|
||||
val shell = getRootShell()
|
||||
val result =
|
||||
shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null).exec()
|
||||
shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null)
|
||||
.exec()
|
||||
Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}")
|
||||
return result.out.joinToString("\n")
|
||||
}
|
||||
@@ -168,11 +176,39 @@ fun getSepolicy(pkg: String): String {
|
||||
fun setSepolicy(pkg: String, rules: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
val result =
|
||||
shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'").to(ArrayList(), null).exec()
|
||||
shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'")
|
||||
.to(ArrayList(), null).exec()
|
||||
Log.i(TAG, "set sepolicy result: ${result.code}")
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun listAppProfileTemplates(): List<String> {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null)
|
||||
.exec().out
|
||||
}
|
||||
|
||||
fun getAppProfileTemplate(id: String): String {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'")
|
||||
.to(ArrayList(), null)
|
||||
.exec().out.joinToString("\n")
|
||||
}
|
||||
|
||||
fun setAppProfileTemplate(id: String, template: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile set-template '${id}' '${template}'")
|
||||
.to(ArrayList(), null)
|
||||
.exec().isSuccess
|
||||
}
|
||||
|
||||
fun deleteAppProfileTemplate(id: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'")
|
||||
.to(ArrayList(), null)
|
||||
.exec().isSuccess
|
||||
}
|
||||
|
||||
fun forceStopApp(packageName: String) {
|
||||
val shell = getRootShell()
|
||||
val result = shell.newJob().add("am force-stop $packageName").exec()
|
||||
@@ -182,7 +218,8 @@ fun forceStopApp(packageName: String) {
|
||||
fun launchApp(packageName: String) {
|
||||
|
||||
val shell = getRootShell()
|
||||
val result = shell.newJob().add("monkey -p $packageName -c android.intent.category.LAUNCHER 1").exec()
|
||||
val result =
|
||||
shell.newJob().add("monkey -p $packageName -c android.intent.category.LAUNCHER 1").exec()
|
||||
Log.i(TAG, "launch $packageName result: $result")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package me.weishu.kernelsu.ui.viewmodel
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.profile.Capabilities
|
||||
import me.weishu.kernelsu.profile.Groups
|
||||
import me.weishu.kernelsu.ui.util.getAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.util.listAppProfileTemplates
|
||||
import me.weishu.kernelsu.ui.util.setAppProfileTemplate
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/20.
|
||||
*/
|
||||
|
||||
//const val TEMPLATE_INDEX_URL = "https://kernelsu.org/templates/index.json"
|
||||
//const val TEMPLATE_URL = "https://kernelsu.org/templates/%s"
|
||||
|
||||
const val TEMPLATE_INDEX_URL = "http://192.168.31.99/templates/index.json"
|
||||
const val TEMPLATE_URL = "http://192.168.31.99/templates/%s"
|
||||
const val TAG = "TemplateViewModel"
|
||||
|
||||
class TemplateViewModel : ViewModel() {
|
||||
companion object {
|
||||
|
||||
private var templates by mutableStateOf<List<TemplateInfo>>(emptyList())
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class TemplateInfo(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val description: String = "",
|
||||
val author: String = "",
|
||||
val local: Boolean = true,
|
||||
|
||||
val namespace: Int = Natives.Profile.Namespace.INHERITED.ordinal,
|
||||
val uid: Int = 0,
|
||||
val gid: Int = 0,
|
||||
val groups: List<Int> = mutableListOf(),
|
||||
val capabilities: List<Int> = mutableListOf(),
|
||||
val context: String = "u:r:su:s0",
|
||||
val rules: List<String> = mutableListOf(),
|
||||
) : Parcelable
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
val templateList by derivedStateOf {
|
||||
val comparator = compareBy(TemplateInfo::local).then(
|
||||
compareBy(
|
||||
Collator.getInstance(Locale.getDefault()), TemplateInfo::name
|
||||
)
|
||||
)
|
||||
templates.sortedWith(comparator).apply {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchTemplates(sync: Boolean = false) {
|
||||
isRefreshing = true
|
||||
withContext(Dispatchers.IO) {
|
||||
val localTemplateIds = listAppProfileTemplates()
|
||||
Log.i(TAG, "localTemplateIds: $localTemplateIds")
|
||||
if (localTemplateIds.isEmpty() || sync) {
|
||||
// if no templates, fetch remote templates
|
||||
fetchRemoteTemplates()
|
||||
}
|
||||
|
||||
// fetch templates again
|
||||
templates = listAppProfileTemplates().mapNotNull(::getTemplateInfoById)
|
||||
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchRemoteTemplates() {
|
||||
OkHttpClient().newCall(
|
||||
Request.Builder().url(TEMPLATE_INDEX_URL).build()
|
||||
).runCatching {
|
||||
execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return
|
||||
}
|
||||
val remoteTemplateIds = JSONArray(response.body!!.string())
|
||||
Log.i(TAG, "fetchRemoteTemplates: $remoteTemplateIds")
|
||||
0.until(remoteTemplateIds.length()).forEach { i ->
|
||||
val id = remoteTemplateIds.getString(i)
|
||||
val templateJson = OkHttpClient().newCall(
|
||||
Request.Builder().url(TEMPLATE_URL.format(id)).build()
|
||||
).runCatching {
|
||||
execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return@forEach
|
||||
}
|
||||
response.body!!.string()
|
||||
}
|
||||
}.getOrNull() ?: return@forEach
|
||||
Log.i(TAG, "template: $templateJson")
|
||||
|
||||
// validate remote template
|
||||
runCatching {
|
||||
val json = JSONObject(templateJson)
|
||||
fromJSON(json)?.let {
|
||||
// force local template
|
||||
json.put("local", false)
|
||||
setAppProfileTemplate(id, json.toString())
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e(TAG, "ignore invalid template: $it", it)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e(TAG, "fetchRemoteTemplates error", it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T, R> JSONArray.mapCatching(
|
||||
transform: (T) -> R, onFail: (Throwable) -> Unit
|
||||
): List<R> {
|
||||
return List(length()) { i -> get(i) as T }.mapNotNull { element ->
|
||||
runCatching {
|
||||
transform(element)
|
||||
}.onFailure(onFail).getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> getEnumOrdinals(
|
||||
jsonArray: JSONArray, enumClass: Class<T>
|
||||
): List<T> {
|
||||
return jsonArray.mapCatching<String, T>({ name ->
|
||||
enumValueOf(name.uppercase())
|
||||
}, {
|
||||
Log.e(TAG, "ignore invalid enum ${enumClass.simpleName}: $it", it)
|
||||
})
|
||||
}
|
||||
|
||||
fun getTemplateInfoById(id: String): TemplateViewModel.TemplateInfo? {
|
||||
return runCatching {
|
||||
fromJSON(JSONObject(getAppProfileTemplate(id)))
|
||||
}.onFailure {
|
||||
Log.e(TAG, "ignore invalid template: $it", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun fromJSON(templateJson: JSONObject): TemplateViewModel.TemplateInfo? {
|
||||
return runCatching {
|
||||
val groupsJsonArray = templateJson.getJSONArray("groups")
|
||||
val capabilitiesJsonArray = templateJson.getJSONArray("capabilities")
|
||||
val rulesJsonArray = templateJson.optJSONArray("rules")
|
||||
val templateInfo = TemplateViewModel.TemplateInfo(
|
||||
id = templateJson.getString("id"),
|
||||
name = templateJson.getString("name"),
|
||||
description = templateJson.getString("description"),
|
||||
local = templateJson.getBoolean("local"),
|
||||
namespace = Natives.Profile.Namespace.valueOf(
|
||||
templateJson.getString("namespace").uppercase()
|
||||
).ordinal,
|
||||
uid = templateJson.getInt("uid"),
|
||||
gid = templateJson.getInt("gid"),
|
||||
groups = getEnumOrdinals(groupsJsonArray, Groups::class.java).map { it.gid },
|
||||
capabilities = getEnumOrdinals(
|
||||
capabilitiesJsonArray, Capabilities::class.java
|
||||
).map { it.cap },
|
||||
context = templateJson.getString("context"),
|
||||
rules = rulesJsonArray?.mapCatching<String, String>({ it }, {
|
||||
Log.e(TAG, "ignore invalid rule: $it", it)
|
||||
}).orEmpty()
|
||||
)
|
||||
templateInfo
|
||||
}.onFailure {
|
||||
Log.e(TAG, "ignore invalid template: $it", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun generateTemplates() {
|
||||
val templateJson = JSONObject()
|
||||
templateJson.put("id", "com.example")
|
||||
templateJson.put("name", "Example")
|
||||
templateJson.put("description", "This is an example template")
|
||||
templateJson.put("local", true)
|
||||
templateJson.put("namespace", Natives.Profile.Namespace.INHERITED.name)
|
||||
templateJson.put("uid", 0)
|
||||
templateJson.put("gid", 0)
|
||||
|
||||
templateJson.put("groups", JSONArray().apply { put(Groups.INET.name) })
|
||||
templateJson.put("capabilities", JSONArray().apply { put(Capabilities.CAP_NET_RAW.name) })
|
||||
templateJson.put("context", "u:r:su:s0")
|
||||
Log.i(TAG, "$templateJson")
|
||||
}
|
||||
@@ -80,4 +80,17 @@
|
||||
<string name="restart_app">重新启动</string>
|
||||
<string name="failed_to_update_sepolicy">为:%s 更新翻译失败</string>
|
||||
<string name="module_changelog">更新日志</string>
|
||||
<string name="settings_profile_template">App Profile 模版</string>
|
||||
<string name="settings_profile_template_summary">管理本地和在线的 App Profile 模版</string>
|
||||
<string name="app_profile_template_create">创建模版</string>
|
||||
<string name="app_profile_template_edit">编辑模版</string>
|
||||
<string name="app_profile_template_id">模版 id</string>
|
||||
<string name="app_profile_template_id_invalid">模版 id 不合法!</string>
|
||||
<string name="app_profile_template_name">名字</string>
|
||||
<string name="app_profile_template_description">描述</string>
|
||||
<string name="app_profile_template_save">保存</string>
|
||||
<string name="app_profile_template_delete">删除</string>
|
||||
<string name="app_profile_template_view">查看模版</string>
|
||||
<string name="app_profile_template_readonly">只读</string>
|
||||
<string name="app_profile_template_id_exist">模版 id 已存在!</string>
|
||||
</resources>
|
||||
@@ -85,4 +85,17 @@
|
||||
<string name="restart_app">Restart</string>
|
||||
<string name="failed_to_update_sepolicy">Failed to update SELinux rules for: %s</string>
|
||||
<string name="module_changelog">Changelog</string>
|
||||
<string name="settings_profile_template">App Profile Template</string>
|
||||
<string name="settings_profile_template_summary">Manage local and online template of App Profile</string>
|
||||
<string name="app_profile_template_create">Create Template</string>
|
||||
<string name="app_profile_template_edit">Edit Template</string>
|
||||
<string name="app_profile_template_id">id</string>
|
||||
<string name="app_profile_template_id_invalid">Invalid template id</string>
|
||||
<string name="app_profile_template_name">Name</string>
|
||||
<string name="app_profile_template_description">Description</string>
|
||||
<string name="app_profile_template_save">Save</string>
|
||||
<string name="app_profile_template_delete">Delete</string>
|
||||
<string name="app_profile_template_view">View Template</string>
|
||||
<string name="app_profile_template_readonly">readonly</string>
|
||||
<string name="app_profile_template_id_exist">template id already exists!</string>
|
||||
</resources>
|
||||
|
||||
@@ -148,20 +148,26 @@ enum Profile {
|
||||
policy: String,
|
||||
},
|
||||
|
||||
/// get template of <package-name>
|
||||
/// get template of <id>
|
||||
GetTemplate {
|
||||
/// package name
|
||||
package: String,
|
||||
/// template id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// set template of <package-name> to <template>
|
||||
/// set template of <id> to <template string>
|
||||
SetTemplate {
|
||||
/// package name
|
||||
package: String,
|
||||
/// template
|
||||
/// template id
|
||||
id: String,
|
||||
/// template string
|
||||
template: String,
|
||||
},
|
||||
|
||||
/// delete template of <id>
|
||||
DeleteTemplate {
|
||||
/// template id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// list all templates
|
||||
ListTemplates,
|
||||
}
|
||||
@@ -217,10 +223,11 @@ pub fn run() -> Result<()> {
|
||||
Profile::SetSepolicy { package, policy } => {
|
||||
crate::profile::set_sepolicy(package, policy)
|
||||
}
|
||||
Profile::GetTemplate { package } => crate::profile::get_template(package),
|
||||
Profile::SetTemplate { package, template } => {
|
||||
crate::profile::set_template(package, template)
|
||||
}
|
||||
Profile::GetTemplate { id } => crate::profile::get_template(id),
|
||||
Profile::SetTemplate { id, template } => {
|
||||
crate::profile::set_template(id, template)
|
||||
},
|
||||
Profile::DeleteTemplate { id } => crate::profile::delete_template(id),
|
||||
Profile::ListTemplates => crate::profile::list_templates(),
|
||||
},
|
||||
|
||||
|
||||
@@ -18,22 +18,32 @@ pub fn get_sepolicy(pkg: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_template(name: String, template: String) -> Result<()> {
|
||||
// ksud doesn't guarteen the correctness of template, it just save
|
||||
pub fn set_template(id: String, template: String) -> Result<()> {
|
||||
ensure_dir_exists(defs::PROFILE_TEMPLATE_DIR)?;
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(name);
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
|
||||
std::fs::write(template_file, template)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_template(name: String) -> Result<()> {
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(name);
|
||||
pub fn get_template(id: String) -> Result<()> {
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
|
||||
let template = std::fs::read_to_string(template_file)?;
|
||||
println!("{template}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_template(id: String) -> Result<()> {
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
|
||||
std::fs::remove_file(template_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_templates() -> Result<()> {
|
||||
let templates = std::fs::read_dir(defs::PROFILE_TEMPLATE_DIR)?;
|
||||
let templates = std::fs::read_dir(defs::PROFILE_TEMPLATE_DIR);
|
||||
let Ok(templates) = templates else {
|
||||
return Ok(())
|
||||
};
|
||||
for template in templates {
|
||||
let template = template?;
|
||||
let template = template.file_name();
|
||||
|
||||
Reference in New Issue
Block a user