diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index e9f781f0..22904a0c 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.kotlin) alias(libs.plugins.ksp) alias(libs.plugins.lsplugin.apksign) + id("kotlin-parcelize") } val managerVersionCode: Int by rootProject.extra @@ -96,8 +97,6 @@ dependencies { implementation(libs.compose.destinations.animations.core) ksp(libs.compose.destinations.ksp) - implementation(libs.com.github.alorma.compose.settings.ui.m3) - implementation(libs.com.github.topjohnwu.libsu.core) implementation(libs.com.github.topjohnwu.libsu.service) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/profile/AppProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/profile/AppProfile.kt new file mode 100644 index 00000000..7511df81 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/profile/AppProfile.kt @@ -0,0 +1,13 @@ +package me.weishu.kernelsu.profile + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import kotlinx.parcelize.Parcelize + +@Immutable +@Parcelize +data class AppProfile( + val profileName: String, + val allowRootRequest: Boolean = false, + val unmountModules: Boolean = false, +) : Parcelable diff --git a/manager/app/src/main/java/me/weishu/kernelsu/profile/RootProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/profile/RootProfile.kt new file mode 100644 index 00000000..dca64b28 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/profile/RootProfile.kt @@ -0,0 +1,23 @@ +package me.weishu.kernelsu.profile + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import kotlinx.parcelize.Parcelize + +@Immutable +@Parcelize +data class RootProfile( + val profileName: String, + val namespace: Namespace = Namespace.Inherited, + val uid: Int = 0, + val gid: Int = 0, + val groups: Int = 0, + val capabilities: List = emptyList(), + val context: String = "u:r:su:s0", +) : Parcelable { + enum class Namespace { + Inherited, + Global, + Individual, + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt index c86a3da8..abb86b7e 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt @@ -5,7 +5,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -25,6 +24,8 @@ import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.navigation.popBackStack import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState +import me.weishu.kernelsu.Natives +import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.component.rememberDialogHostState import me.weishu.kernelsu.ui.screen.BottomBarDestination import me.weishu.kernelsu.ui.screen.NavGraphs @@ -34,7 +35,7 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost class MainActivity : ComponentActivity() { - @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) + @OptIn(ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,8 +65,10 @@ class MainActivity : ComponentActivity() { @Composable private fun BottomBar(navController: NavHostController) { + val isManager = Natives.becomeManager(ksuApp.packageName) NavigationBar(tonalElevation = 8.dp) { BottomBarDestination.values().forEach { destination -> + if (!isManager && destination.rootRequired) return@forEach val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) NavigationBarItem( selected = isCurrentDestOnBackStack, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt index 1d9a822e..af47ee64 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt @@ -10,10 +10,22 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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 @@ -21,11 +33,9 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import me.weishu.kernelsu.R private const val TAG = "SearchBar" diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt new file mode 100644 index 00000000..ab62136e --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt @@ -0,0 +1,45 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +fun SwitchItem( + icon: ImageVector? = null, + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + ListItem( + headlineContent = { + Text(title) + }, + leadingContent = icon?.let { + { Icon(icon, title) } + }, + trailingContent = { + Switch(checked = checked, onCheckedChange = onCheckedChange) + }, + ) +} + +@Composable +fun RadioItem( + title: String, + selected: Boolean, + onClick: () -> Unit, +) { + ListItem( + headlineContent = { + Text(title) + }, + leadingContent = { + RadioButton(selected = selected, onClick = onClick) + }, + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt new file mode 100644 index 00000000..2e5ccd35 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt @@ -0,0 +1,55 @@ +package me.weishu.kernelsu.ui.component.profile + +import androidx.compose.foundation.layout.Column +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import me.weishu.kernelsu.R +import me.weishu.kernelsu.profile.AppProfile +import me.weishu.kernelsu.ui.component.SwitchItem + +@Composable +fun AppProfileConfig( + modifier: Modifier = Modifier, + fixedName: Boolean, + profile: AppProfile, + onProfileChange: (AppProfile) -> Unit, +) { + Column(modifier = modifier) { + if (!fixedName) { + OutlinedTextField( + label = { Text(stringResource(R.string.profile_name)) }, + value = profile.profileName, + onValueChange = { onProfileChange(profile.copy(profileName = it)) } + ) + } + + SwitchItem( + title = stringResource(R.string.profile_allow_root_request), + checked = profile.allowRootRequest, + onCheckedChange = { onProfileChange(profile.copy(allowRootRequest = it)) } + ) + + SwitchItem( + title = stringResource(R.string.profile_unmount_modules), + checked = profile.unmountModules, + onCheckedChange = { onProfileChange(profile.copy(unmountModules = it)) } + ) + } +} + +@Preview +@Composable +private fun AppProfileConfigPreview() { + var profile by remember { mutableStateOf(AppProfile("")) } + AppProfileConfig(fixedName = true, profile = profile) { + profile = it + } +} 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 new file mode 100644 index 00000000..9a2fbe74 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt @@ -0,0 +1,131 @@ +package me.weishu.kernelsu.ui.component.profile + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import me.weishu.kernelsu.R +import me.weishu.kernelsu.profile.RootProfile + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RootProfileConfig( + modifier: Modifier = Modifier, + fixedName: Boolean, + profile: RootProfile, + onProfileChange: (RootProfile) -> Unit, +) { + Column(modifier = modifier) { + if (!fixedName) { + OutlinedTextField( + label = { Text(stringResource(R.string.profile_name)) }, + value = profile.profileName, + onValueChange = { onProfileChange(profile.copy(profileName = it)) } + ) + } + + var namespaceBoxExpanded by remember { mutableStateOf(false) } + val currentNamespace = when (profile.namespace) { + RootProfile.Namespace.Inherited -> stringResource(R.string.profile_namespace_inherited) + RootProfile.Namespace.Global -> stringResource(R.string.profile_namespace_global) + RootProfile.Namespace.Individual -> stringResource(R.string.profile_namespace_individual) + } + ListItem(headlineContent = { + ExposedDropdownMenuBox( + expanded = namespaceBoxExpanded, + onExpandedChange = { namespaceBoxExpanded = it } + ) { + OutlinedTextField( + modifier = Modifier.menuAnchor(), + readOnly = true, + label = { Text(stringResource(R.string.profile_namespace)) }, + value = currentNamespace, + onValueChange = {}, + ) + ExposedDropdownMenu( + expanded = namespaceBoxExpanded, + onDismissRequest = { namespaceBoxExpanded = false } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.profile_namespace_inherited)) }, + onClick = { + onProfileChange(profile.copy(namespace = RootProfile.Namespace.Inherited)) + namespaceBoxExpanded = false + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.profile_namespace_global)) }, + onClick = { + onProfileChange(profile.copy(namespace = RootProfile.Namespace.Global)) + namespaceBoxExpanded = false + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.profile_namespace_individual)) }, + onClick = { + onProfileChange(profile.copy(namespace = RootProfile.Namespace.Individual)) + namespaceBoxExpanded = false + }, + ) + } + } + }) + + ListItem(headlineContent = { + OutlinedTextField( + label = { Text("uid") }, + value = profile.uid.toString(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { onProfileChange(profile.copy(uid = it.toInt())) } + ) + }) + + ListItem(headlineContent = { + OutlinedTextField( + label = { Text("gid") }, + value = profile.gid.toString(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { onProfileChange(profile.copy(gid = it.toInt())) } + ) + }) + + ListItem(headlineContent = { + OutlinedTextField( + label = { Text("groups") }, + value = profile.groups.toString(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { onProfileChange(profile.copy(groups = it.toInt())) } + ) + }) + + ListItem(headlineContent = { + OutlinedTextField( + label = { Text("context") }, + value = profile.context, + onValueChange = { onProfileChange(profile.copy(context = it)) } + ) + }) + } +} + +@Preview +@Composable +private fun RootProfileConfigPreview() { + var profile by remember { mutableStateOf(RootProfile("")) } + RootProfileConfig(fixedName = true, profile = profile) { + profile = it + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt index 933dedfa..e07f3228 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt @@ -1,32 +1,24 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - package me.weishu.kernelsu.ui.screen -import android.content.pm.PackageInfo -import androidx.compose.foundation.clickable +import android.os.Parcelable +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Android import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Group -import androidx.compose.material.icons.filled.Groups3 -import androidx.compose.material.icons.filled.List -import androidx.compose.material.icons.filled.PermIdentity -import androidx.compose.material.icons.filled.Rule -import androidx.compose.material.icons.filled.SafetyDivider import androidx.compose.material.icons.filled.Security import androidx.compose.material3.Divider 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.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -37,9 +29,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -47,9 +39,17 @@ import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R +import me.weishu.kernelsu.profile.AppProfile +import me.weishu.kernelsu.profile.RootProfile +import me.weishu.kernelsu.ui.component.RadioItem +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.util.LocalSnackbarHost +import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel /** * @author weishu @@ -59,282 +59,171 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost @Composable fun AppProfileScreen( navigator: DestinationsNavigator, - packageName: String, - grantRoot: Boolean, - label: String, - icon: PackageInfo + appInfo: SuperUserViewModel.AppInfo, ) { - + val context = LocalContext.current val snackbarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() + val failToGrantRoot = stringResource(R.string.superuser_failed_to_grant_root) + var isRootGranted by rememberSaveable { mutableStateOf(appInfo.onAllowList) } Scaffold( - topBar = { - TopBar(onBack = { - navigator.popBackStack() - }) - } + topBar = { TopBar { navigator.popBackStack() } } ) { paddingValues -> - - Column(modifier = Modifier.padding(paddingValues)) { - - val scope = rememberCoroutineScope() - - val uid = icon.applicationInfo.uid - val isAllowlistModeInit = Natives.isAllowlistMode() - - val isInAllowDenyListInit = if (isAllowlistModeInit) { - Natives.isUidInAllowlist(uid) - } else { - Natives.isUidInDenylist(uid) - } - - GroupTitle(stringResource(id = R.string.app_profile_title0)) - - var allowlistMode by rememberSaveable { - mutableStateOf(isAllowlistModeInit) - } - - var isInAllowDenyList by rememberSaveable { - mutableStateOf(isInAllowDenyListInit) - } - - val setAllowlistFailedMsg = if (allowlistMode) { - stringResource(R.string.failed_to_set_denylist_mode) - } else { - stringResource(R.string.failed_to_set_allowlist_mode) - } - WorkingMode(allowlistMode) { checked -> - if (Natives.setAllowlistMode(checked)) { - allowlistMode = !allowlistMode - } else scope.launch { - snackbarHost.showSnackbar(setAllowlistFailedMsg) - } - } - - Divider(thickness = Dp.Hairline) - - GroupTitle(stringResource(id = R.string.app_profile_title1)) - - ListItem( - headlineText = { Text(label) }, - supportingText = { Text(packageName) }, - leadingContent = { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(icon) - .crossfade(true) - .build(), - contentDescription = label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) - ) - }, - ) - - var isGrantRoot by rememberSaveable { - mutableStateOf(grantRoot) - } - - val failToGrantRoot = stringResource(R.string.superuser_failed_to_grant_root) - AppSwitch( - Icons.Filled.Security, - stringResource(id = R.string.superuser), - checked = isGrantRoot - ) { checked -> - + AppProfileInner( + modifier = Modifier.padding(paddingValues), + packageName = appInfo.packageName, + appLabel = appInfo.label, + appIcon = { + AsyncImage( + model = ImageRequest.Builder(context) + .data(appInfo.packageInfo) + .crossfade(true) + .build(), + contentDescription = appInfo.label, + modifier = Modifier + .padding(4.dp) + .width(48.dp) + .height(48.dp) + ) + }, + isRootGranted = isRootGranted, + onSwitchRootPermission = { grant -> scope.launch { - val success = Natives.allowRoot(uid, checked) + val success = Natives.allowRoot(appInfo.uid, grant) if (success) { - isGrantRoot = checked + isRootGranted = grant } else { - snackbarHost.showSnackbar(failToGrantRoot.format(uid)) + snackbarHost.showSnackbar(failToGrantRoot.format(appInfo.uid)) } } - } - - val failedToAddAllowListMsg = if (allowlistMode) { - stringResource(R.string.failed_to_add_to_allowlist) - } else { - stringResource(R.string.failed_to_add_to_denylist) - } - AppSwitch( - icon = Icons.Filled.List, - title = if (allowlistMode) { - stringResource(id = R.string.app_profile_allowlist) - } else { - stringResource(id = R.string.app_profile_denylist) - }, - checked = isInAllowDenyList - ) { checked -> - val success = if (allowlistMode) { - Natives.addUidToAllowlist(uid) - } else { - Natives.addUidToDenylist(uid) - } - if (success) { - isInAllowDenyList = checked - } else scope.launch { - snackbarHost.showSnackbar(failedToAddAllowListMsg.format(label)) - } - } - - Divider(thickness = Dp.Hairline) - - GroupTitle(title = stringResource(id = R.string.app_profile_title2)) - - Uid() - - Gid() - - Groups() - - Capabilities() - - SELinuxDomain() - } - } -} - -@Composable -private fun GroupTitle(title: String) { - Row(modifier = Modifier.padding(12.dp)) { - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = title, - color = MaterialTheme.colorScheme.primary, - fontStyle = MaterialTheme.typography.titleSmall.fontStyle, - fontSize = MaterialTheme.typography.titleSmall.fontSize, - fontWeight = MaterialTheme.typography.titleSmall.fontWeight, + }, ) } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun WorkingMode(allowlistMode: Boolean, onCheckedChange: (Boolean) -> Unit) { - var showDropdown by remember { mutableStateOf(false) } - val mode = if (allowlistMode) { - stringResource(id = R.string.app_profile_allowlist) - } else { - stringResource(id = R.string.app_profile_denylist) - } - ListItem( - modifier = Modifier.clickable(onClick = { - showDropdown = true - }), - headlineText = { - Text(stringResource(id = R.string.app_profile_mode)) - }, - supportingText = { - Text(mode) - }, - leadingContent = { - Icon( - Icons.Filled.List, - contentDescription = stringResource(id = R.string.app_profile_mode) - ) - }, - trailingContent = { - Switch(checked = allowlistMode, onCheckedChange = onCheckedChange) - } - ) -} - -@Composable -private fun AppSwitch( - icon: ImageVector, - title: String, - checked: Boolean, - onCheckChange: (Boolean) -> Unit +private fun AppProfileInner( + modifier: Modifier = Modifier, + packageName: String, + appLabel: String, + appIcon: @Composable () -> Unit, + isRootGranted: Boolean, + onSwitchRootPermission: (Boolean) -> Unit, ) { - ListItem( - headlineText = { Text(title) }, - leadingContent = { - Icon( - icon, - contentDescription = title - ) - }, - trailingContent = { - Switch(checked = checked, onCheckedChange = onCheckChange) + Column(modifier = modifier) { + ListItem( + headlineContent = { Text(appLabel) }, + supportingContent = { Text(packageName) }, + leadingContent = appIcon, + ) + + SwitchItem( + icon = Icons.Filled.Security, + title = stringResource(id = R.string.superuser), + checked = isRootGranted, + onCheckedChange = onSwitchRootPermission, + ) + + Divider(thickness = Dp.Hairline) + + Crossfade(targetState = isRootGranted, label = "") { current -> + if (current) { + var mode: Mode by rememberSaveable { mutableStateOf(Mode.Default()) } + var template by rememberSaveable { mutableStateOf("None") } + var profile by rememberSaveable { mutableStateOf(RootProfile("@$packageName")) } + + Column { + RadioItem( + title = stringResource(R.string.profile_default), + selected = mode is Mode.Default, + onClick = { mode = Mode.Default() } + ) + + RadioItem( + title = stringResource(R.string.profile_template), + selected = mode is Mode.Template, + onClick = { mode = Mode.Template("") } + ) + AnimatedVisibility(mode is Mode.Template) { + var expanded by remember { mutableStateOf(false) } + ListItem(headlineContent = { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + OutlinedTextField( + modifier = Modifier.menuAnchor(), + readOnly = true, + label = { Text(stringResource(R.string.profile_template)) }, + value = template, + onValueChange = {} + ) + // TODO: Template + } + }) + } + + RadioItem( + title = stringResource(R.string.profile_custom), + selected = mode is Mode.Custom, + onClick = { mode = Mode.Custom(profile) } + ) + AnimatedVisibility(mode is Mode.Custom) { + RootProfileConfig( + fixedName = true, + profile = profile, + onProfileChange = { profile = it } + ) + } + } + } else { + var mode: Mode by rememberSaveable { mutableStateOf(Mode.Default()) } + var profile by rememberSaveable { mutableStateOf(AppProfile("@$packageName")) } + + Column { + RadioItem( + title = stringResource(R.string.profile_default), + selected = mode is Mode.Default, + onClick = { mode = Mode.Default() } + ) + + RadioItem( + title = stringResource(R.string.profile_custom), + selected = mode is Mode.Custom, + onClick = { mode = Mode.Custom(profile) } + ) + AnimatedVisibility(mode is Mode.Custom) { + AppProfileConfig( + fixedName = true, + profile = profile, + onProfileChange = { profile = it } + ) + } + } + + } } - ) + } } -@Composable -private fun Uid() { - ListItem( - headlineText = { - Text("Uid: 0") - }, - leadingContent = { - Icon( - Icons.Filled.PermIdentity, - contentDescription = "Uid" - ) - }, - ) -} +@Parcelize +private sealed class Mode

: Parcelable { -@Composable -private fun Gid() { - ListItem( - headlineText = { Text("Gid: 0") }, - leadingContent = { - Icon( - Icons.Filled.Group, - contentDescription = "Gid" - ) - }, - ) -} + class Default

: Mode

() -@Composable -private fun Groups() { - ListItem( - headlineText = { Text("Groups: 0") }, - leadingContent = { - Icon( - Icons.Filled.Groups3, - contentDescription = "Groups" - ) - }, - ) -} + class Template

(val template: String) : Mode

() -@Composable -private fun Capabilities() { - ListItem( - headlineText = { Text("Capabilities") }, - leadingContent = { - Icon( - Icons.Filled.SafetyDivider, - contentDescription = "Capabilities" - ) - }, - ) -} - -@Composable -private fun SELinuxDomain() { - ListItem( - headlineText = { Text("u:r:su:s0") }, - leadingContent = { - Icon( - Icons.Filled.Rule, - contentDescription = "SELinuxDomain" - ) - }, - ) + class Custom

(val profile: P) : Mode

() } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onBack: () -> Unit = {}) { +private fun TopBar(onBack: () -> Unit) { TopAppBar( title = { - Text(stringResource(R.string.app_profile)) + Text(stringResource(R.string.profile)) }, navigationIcon = { IconButton( @@ -342,4 +231,17 @@ private fun TopBar(onBack: () -> Unit = {}) { ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } }, ) -} \ No newline at end of file +} + +@Preview +@Composable +private fun AppProfilePreview() { + var isRootGranted by remember { mutableStateOf(false) } + AppProfileInner( + packageName = "icu.nullptr.test", + appLabel = "Test", + appIcon = { Icon(Icons.Filled.Android, null) }, + isRootGranted = isRootGranted, + onSwitchRootPermission = { isRootGranted = it }, + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt index fd8fb6f5..9345ced5 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt @@ -15,9 +15,10 @@ enum class BottomBarDestination( val direction: DirectionDestinationSpec, @StringRes val label: Int, val iconSelected: ImageVector, - val iconNotSelected: ImageVector + val iconNotSelected: ImageVector, + val rootRequired: Boolean, ) { - Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home), - SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security), - Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps) + Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false), + SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true), + Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt index 11681ada..77fc1e6a 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt @@ -33,7 +33,6 @@ import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination import me.weishu.kernelsu.ui.util.* -@OptIn(ExperimentalMaterial3Api::class) @RootNavGraph(start = true) @Destination @Composable diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt index 2f8a05df..72a8f7d9 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt @@ -35,7 +35,6 @@ import java.util.* * @author weishu * @date 2023/1/1. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable @Destination fun InstallScreen(navigator: DestinationsNavigator, uri: Uri) { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index 8b36f4d0..b0c7f06f 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -39,7 +39,6 @@ import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel -@OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun ModuleScreen(navigator: DestinationsNavigator) { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt index d2159c95..88477319 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt @@ -2,6 +2,7 @@ package me.weishu.kernelsu.ui.screen import android.content.Intent import android.net.Uri +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack @@ -11,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.content.FileProvider -import com.alorma.compose.settings.ui.* import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.Dispatchers @@ -28,7 +28,6 @@ import me.weishu.kernelsu.ui.util.getBugreportFile * @author weishu * @date 2023/1/1. */ -@OptIn(ExperimentalMaterial3Api::class) @Destination @Composable fun SettingScreen(navigator: DestinationsNavigator) { @@ -50,11 +49,9 @@ fun SettingScreen(navigator: DestinationsNavigator) { val context = LocalContext.current val scope = rememberCoroutineScope() val dialogHost = LocalDialogHost.current - SettingsMenuLink( - title = { - Text(stringResource(id = R.string.send_log)) - }, - onClick = { + ListItem( + headlineContent = { Text(stringResource(id = R.string.send_log)) }, + modifier = Modifier.clickable { scope.launch { val bugreport = dialogHost.withLoading { withContext(Dispatchers.IO) { @@ -85,11 +82,9 @@ fun SettingScreen(navigator: DestinationsNavigator) { ) val about = stringResource(id = R.string.about) - SettingsMenuLink( - title = { - Text(about) - }, - onClick = { + ListItem( + headlineContent = { Text(about) }, + modifier = Modifier.clickable { showAboutDialog.value = true } ) @@ -108,4 +103,4 @@ private fun TopBar(onBack: () -> Unit = {}) { ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } }, ) -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt index 2d6f40f2..a5180beb 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt @@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -24,18 +23,13 @@ import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch -import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.ConfirmDialog -import me.weishu.kernelsu.ui.component.ConfirmResult import me.weishu.kernelsu.ui.component.SearchAppBar import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination -import me.weishu.kernelsu.ui.util.LocalDialogHost -import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel -import java.util.* -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class) @Destination @Composable fun SuperUserScreen(navigator: DestinationsNavigator) { @@ -107,18 +101,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { .padding(innerPadding) .pullRefresh(refreshState) ) { - val failMessage = stringResource(R.string.superuser_failed_to_grant_root) - LazyColumn(Modifier.fillMaxSize()) { items(viewModel.appList, key = { it.packageName + it.uid }) { app -> AppItem(app) { - navigator.navigate( - AppProfileScreenDestination( - packageName = app.packageName, - grantRoot = app.onAllowList, - label = app.label, icon = app.icon - ) - ) + navigator.navigate(AppProfileScreenDestination(app)) } } @@ -133,7 +119,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun AppItem( app: SuperUserViewModel.AppInfo, @@ -141,12 +126,12 @@ private fun AppItem( ) { ListItem( modifier = Modifier.clickable(onClick = onClickListener), - headlineText = { Text(app.label) }, - supportingText = { Text(app.packageName) }, + headlineContent = { Text(app.label) }, + supportingContent = { Text(app.packageName) }, leadingContent = { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(app.icon) + .data(app.packageInfo) .crossfade(true) .build(), contentDescription = app.label, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt index 4096b094..3748e993 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt @@ -6,6 +6,7 @@ import android.content.ServiceConnection import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.os.IBinder +import android.os.Parcelable import android.os.SystemClock import android.util.Log import androidx.compose.runtime.derivedStateOf @@ -16,6 +17,7 @@ import androidx.lifecycle.ViewModel import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.IKsuInterface import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp @@ -34,14 +36,18 @@ class SuperUserViewModel : ViewModel() { private var apps by mutableStateOf>(emptyList()) } - class AppInfo( + @Parcelize + data class AppInfo( val label: String, - val packageName: String, - val icon: PackageInfo, - val uid: Int, + val packageInfo: PackageInfo, val onAllowList: Boolean, - val onDenyList: Boolean - ) + val onDenyList: Boolean, + ) : Parcelable { + val packageName: String + get() = packageInfo.packageName + val uid: Int + get() = packageInfo.applicationInfo.uid + } var search by mutableStateOf("") var showSystemApps by mutableStateOf(false) @@ -67,7 +73,7 @@ class SuperUserViewModel : ViewModel() { .toPinyinString(it.label).contains(search) }.filter { it.uid == 2000 // Always show shell - || showSystemApps || it.icon.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 + || showSystemApps || it.packageInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 } } @@ -107,7 +113,7 @@ class SuperUserViewModel : ViewModel() { val result = connectKsuService { Log.w(TAG, "KsuService disconnected") } - + withContext(Dispatchers.IO) { val pm = ksuApp.packageManager val allowList = Natives.getAllowList().toSet() @@ -130,9 +136,7 @@ class SuperUserViewModel : ViewModel() { val uid = appInfo.uid AppInfo( label = appInfo.loadLabel(pm).toString(), - packageName = it.packageName, - icon = it, - uid = uid, + packageInfo = it, onAllowList = uid in allowList, onDenyList = uid in denyList ) diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 657b1178..f7c87028 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -65,15 +65,15 @@ Support Us KernelSU is, and always will be, free, and open source. You can however show us that you care by making a donation. Join our %2$s channel]]> - App Profile - Application - Root Profile - Allowlist - Denylist - Global - Working Mode - Failed to switch to allowlist mode - Failed to add %s to allowlist - Failed to add %s to denylist - Failed to switch to denylist mode + App profile + Use default profile + Use template profile + Use custom profile + Profile name + Mount namespace + Inherited + Global + Individual + Unmount modules + Allow root request diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index b7b6da15..f019f340 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] -agp = "8.0.0" +agp = "8.0.1" kotlin = "1.8.10" ksp = "1.8.10-1.0.9" -compose-bom = "2023.04.01" +compose-bom = "2023.05.01" lifecycle = "2.6.1" accompanist = "0.30.0" navigation = "2.5.3" -compose-destination = "1.9.40-beta" +compose-destination = "1.9.42-beta" libsu = "5.0.5" [plugins] @@ -38,8 +38,6 @@ com-google-accompanist-drawablepainter = { group = "com.google.accompanist", nam com-google-accompanist-navigation-animation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "accompanist" } com-google-accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } -com-github-alorma-compose-settings-ui-m3 = { module = "com.github.alorma:compose-settings-ui-m3", version = "0.22.0" } - com-github-topjohnwu-libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } com-github-topjohnwu-libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } @@ -47,7 +45,7 @@ dev-rikka-rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:pa io-coil-kt-coil-compose = { group = "io.coil-kt", name = "coil-compose", version = "2.3.0" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.6.4" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.7.1" } me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version = "1.5.0" } diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index 33aca6fd..90dc95d2 100644 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ b/manager/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME