From 22d084f89b6a2569ef6b479a49c0906b49ed7d0d Mon Sep 17 00:00:00 2001 From: weishu Date: Mon, 19 Jun 2023 22:16:46 +0800 Subject: [PATCH] manager: Add selinux rules UI --- manager/app/build.gradle.kts | 1 + .../ui/component/profile/RootProfileConfig.kt | 125 ++++++++++++++---- .../java/me/weishu/kernelsu/ui/util/KsuCli.kt | 10 ++ manager/app/src/main/res/values/strings.xml | 2 + manager/gradle/libs.versions.toml | 3 +- 5 files changed, 113 insertions(+), 28 deletions(-) diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index 7ebb399c..d0056182 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -110,4 +110,5 @@ dependencies { implementation(libs.sheet.compose.dialogs.core) implementation(libs.sheet.compose.dialogs.list) + implementation(libs.sheet.compose.dialogs.input) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt index fa9764ac..eaf0aa2a 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt @@ -8,13 +8,11 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions 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.Group import androidx.compose.material3.AssistChip import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -25,29 +23,29 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.text.isDigitsOnly import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.IconSource import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeker.sheets.core.utils.TestTags -import com.maxkeppeker.sheets.core.views.IconComponent +import com.maxkeppeler.sheets.input.InputDialog +import com.maxkeppeler.sheets.input.models.InputHeader +import com.maxkeppeler.sheets.input.models.InputSelection +import com.maxkeppeler.sheets.input.models.InputTextField +import com.maxkeppeler.sheets.input.models.InputTextFieldType +import com.maxkeppeler.sheets.input.models.ValidationResult import com.maxkeppeler.sheets.list.ListDialog import com.maxkeppeler.sheets.list.models.ListOption import com.maxkeppeler.sheets.list.models.ListSelection @@ -55,8 +53,9 @@ 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.util.isSepolicyValid -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RootProfileConfig( modifier: Modifier = Modifier, @@ -172,26 +171,15 @@ fun RootProfileConfig( ) } - ListItem(headlineContent = { - val keyboardController = LocalSoftwareKeyboardController.current - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(text = stringResource(R.string.profile_selinux_context)) }, - value = profile.context, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - onValueChange = { - onProfileChange(profile.copy(context = it, rootUseDefault = false)) - } + SELinuxPanel(profile = profile, onSELinuxChange = { domain, rules -> + onProfileChange( + profile.copy( + context = domain, + rootUseDefault = false + ) ) }) - } } @@ -368,6 +356,89 @@ private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { }) } +@Composable +private fun SELinuxPanel(profile: Natives.Profile, onSELinuxChange: (domain: String, rules: String) -> Unit) { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) { + var domain by remember { mutableStateOf(profile.context) } + var rules by remember { mutableStateOf("") } + + val inputOptions = listOf( + InputTextField( + text = domain, + header = InputHeader( + title = stringResource(id = R.string.profile_selinux_domain), + ), + type = InputTextFieldType.OUTLINED, + required = true, + resultListener = { + domain = it ?: "" + }, + validationListener = { value -> + // value can be a-zA-Z0-9_ + val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") + if (value?.matches(regex) == true) ValidationResult.Valid + else ValidationResult.Invalid("Domain must be valid sepolicy") + } + ), + InputTextField( + text = rules, + header = InputHeader( + title = stringResource(id = R.string.profile_selinux_rules), + ), + type = InputTextFieldType.OUTLINED, + singleLine = false, + resultListener = { + rules = it ?: "" + }, + validationListener = { value -> + if (isSepolicyValid(value)) ValidationResult.Valid + else ValidationResult.Invalid("Rules must be valid sepolicy") + } + ) + ) + + InputDialog( + state = rememberUseCaseState(visible = true, + onFinishedRequest = { + onSELinuxChange(domain, rules) + }, + onCloseRequest = { + showDialog = false + }), + header = Header.Default( + title = stringResource(R.string.profile_selinux_context), + ), + selection = InputSelection( + input = inputOptions, + onPositiveClick = { result -> + // Handle selection + }, + ) + ) + } + + ListItem(headlineContent = { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .clickable { + showDialog = true + }, + enabled = false, + colors = TextFieldDefaults.outlinedTextFieldColors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledBorderColor = MaterialTheme.colorScheme.outline, + disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + label = { Text(text = stringResource(R.string.profile_selinux_context)) }, + value = profile.context, + onValueChange = { }, + ) + }) +} + @Preview @Composable private fun RootProfileConfigPreview() { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt index 5805e460..1a204fd0 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt @@ -140,4 +140,14 @@ fun hasMagisk(): Boolean { val result = shell.newJob().add("nsenter --mount=/proc/1/ns/mnt which magisk").exec() Log.i(TAG, "has magisk: ${result.isSuccess}") return result.isSuccess +} + +fun isSepolicyValid(rules: String?): Boolean { + if (rules == null) { + return true + } + val shell = getRootShell() + val result = + shell.newJob().add("ksud sepolicy check '$rules'").to(ArrayList(), null).exec() + return result.isSuccess } \ No newline at end of file diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 3031f323..28258ccd 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -74,4 +74,6 @@ Umount modules by default The global default value for \"Umount modules\" in App Profiles. If enabled, it will remove all module modifications to the system for applications that do not have a Profile set. Enabling this option will allow KernelSU to restore any modified files by the modules for this application. + Domain + Rules diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index c8e6e4e2..b995ccff 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -8,7 +8,7 @@ accompanist = "0.30.0" navigation = "2.5.3" compose-destination = "1.9.42-beta" libsu = "5.0.5" -sheets-compose-dialogs = "1.1.1" +sheets-compose-dialogs = "1.2.0" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -55,3 +55,4 @@ compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs"} sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs"} +sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "input", version.ref = "sheets-compose-dialogs"}