manager: several updates (#510)

+ update deps
+ update app profile page
+ don't show su and module page if no root
This commit is contained in:
Nullptr
2023-05-16 22:32:48 +08:00
committed by GitHub
parent 9cf8ac9c51
commit 76612b9cf7
19 changed files with 503 additions and 342 deletions

View File

@@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.kotlin) alias(libs.plugins.kotlin)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.lsplugin.apksign) alias(libs.plugins.lsplugin.apksign)
id("kotlin-parcelize")
} }
val managerVersionCode: Int by rootProject.extra val managerVersionCode: Int by rootProject.extra
@@ -96,8 +97,6 @@ dependencies {
implementation(libs.compose.destinations.animations.core) implementation(libs.compose.destinations.animations.core)
ksp(libs.compose.destinations.ksp) 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.core)
implementation(libs.com.github.topjohnwu.libsu.service) implementation(libs.com.github.topjohnwu.libsu.service)

View File

@@ -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

View File

@@ -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<String> = emptyList(),
val context: String = "u:r:su:s0",
) : Parcelable {
enum class Namespace {
Inherited,
Global,
Individual,
}
}

View File

@@ -5,7 +5,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem 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.DestinationsNavHost
import com.ramcosta.composedestinations.navigation.popBackStack import com.ramcosta.composedestinations.navigation.popBackStack
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState 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.component.rememberDialogHostState
import me.weishu.kernelsu.ui.screen.BottomBarDestination import me.weishu.kernelsu.ui.screen.BottomBarDestination
import me.weishu.kernelsu.ui.screen.NavGraphs import me.weishu.kernelsu.ui.screen.NavGraphs
@@ -34,7 +35,7 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -64,8 +65,10 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
private fun BottomBar(navController: NavHostController) { private fun BottomBar(navController: NavHostController) {
val isManager = Natives.becomeManager(ksuApp.packageName)
NavigationBar(tonalElevation = 8.dp) { NavigationBar(tonalElevation = 8.dp) {
BottomBarDestination.values().forEach { destination -> BottomBarDestination.values().forEach { destination ->
if (!isManager && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem( NavigationBarItem(
selected = isCurrentDestOnBackStack, selected = isCurrentDestOnBackStack,

View File

@@ -10,10 +10,22 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons 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.material.icons.outlined.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.* 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.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalSoftwareKeyboardController 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.ImeAction
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.weishu.kernelsu.R
private const val TAG = "SearchBar" private const val TAG = "SearchBar"

View File

@@ -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)
},
)
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -1,32 +1,24 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package me.weishu.kernelsu.ui.screen package me.weishu.kernelsu.ui.screen
import android.content.pm.PackageInfo import android.os.Parcelable
import androidx.compose.foundation.clickable import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons 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.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.material.icons.filled.Security
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -37,9 +29,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
@@ -47,9 +39,17 @@ import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import me.weishu.kernelsu.Natives import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.R 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.util.LocalSnackbarHost
import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
/** /**
* @author weishu * @author weishu
@@ -59,282 +59,171 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost
@Composable @Composable
fun AppProfileScreen( fun AppProfileScreen(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
packageName: String, appInfo: SuperUserViewModel.AppInfo,
grantRoot: Boolean,
label: String,
icon: PackageInfo
) { ) {
val context = LocalContext.current
val snackbarHost = LocalSnackbarHost.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( Scaffold(
topBar = { topBar = { TopBar { navigator.popBackStack() } }
TopBar(onBack = {
navigator.popBackStack()
})
}
) { paddingValues -> ) { paddingValues ->
AppProfileInner(
Column(modifier = Modifier.padding(paddingValues)) { modifier = Modifier.padding(paddingValues),
packageName = appInfo.packageName,
val scope = rememberCoroutineScope() appLabel = appInfo.label,
appIcon = {
val uid = icon.applicationInfo.uid AsyncImage(
val isAllowlistModeInit = Natives.isAllowlistMode() model = ImageRequest.Builder(context)
.data(appInfo.packageInfo)
val isInAllowDenyListInit = if (isAllowlistModeInit) { .crossfade(true)
Natives.isUidInAllowlist(uid) .build(),
} else { contentDescription = appInfo.label,
Natives.isUidInDenylist(uid) modifier = Modifier
} .padding(4.dp)
.width(48.dp)
GroupTitle(stringResource(id = R.string.app_profile_title0)) .height(48.dp)
)
var allowlistMode by rememberSaveable { },
mutableStateOf(isAllowlistModeInit) isRootGranted = isRootGranted,
} onSwitchRootPermission = { grant ->
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 ->
scope.launch { scope.launch {
val success = Natives.allowRoot(uid, checked) val success = Natives.allowRoot(appInfo.uid, grant)
if (success) { if (success) {
isGrantRoot = checked isRootGranted = grant
} else { } 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 @Composable
private fun WorkingMode(allowlistMode: Boolean, onCheckedChange: (Boolean) -> Unit) { private fun AppProfileInner(
var showDropdown by remember { mutableStateOf(false) } modifier: Modifier = Modifier,
val mode = if (allowlistMode) { packageName: String,
stringResource(id = R.string.app_profile_allowlist) appLabel: String,
} else { appIcon: @Composable () -> Unit,
stringResource(id = R.string.app_profile_denylist) isRootGranted: Boolean,
} onSwitchRootPermission: (Boolean) -> Unit,
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
) { ) {
ListItem( Column(modifier = modifier) {
headlineText = { Text(title) }, ListItem(
leadingContent = { headlineContent = { Text(appLabel) },
Icon( supportingContent = { Text(packageName) },
icon, leadingContent = appIcon,
contentDescription = title )
)
}, SwitchItem(
trailingContent = { icon = Icons.Filled.Security,
Switch(checked = checked, onCheckedChange = onCheckChange) title = stringResource(id = R.string.superuser),
checked = isRootGranted,
onCheckedChange = onSwitchRootPermission,
)
Divider(thickness = Dp.Hairline)
Crossfade(targetState = isRootGranted, label = "") { current ->
if (current) {
var mode: Mode<RootProfile> 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<AppProfile> 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 @Parcelize
private fun Uid() { private sealed class Mode<P : Parcelable> : Parcelable {
ListItem(
headlineText = {
Text("Uid: 0")
},
leadingContent = {
Icon(
Icons.Filled.PermIdentity,
contentDescription = "Uid"
)
},
)
}
@Composable class Default<P : Parcelable> : Mode<P>()
private fun Gid() {
ListItem(
headlineText = { Text("Gid: 0") },
leadingContent = {
Icon(
Icons.Filled.Group,
contentDescription = "Gid"
)
},
)
}
@Composable class Template<P : Parcelable>(val template: String) : Mode<P>()
private fun Groups() {
ListItem(
headlineText = { Text("Groups: 0") },
leadingContent = {
Icon(
Icons.Filled.Groups3,
contentDescription = "Groups"
)
},
)
}
@Composable class Custom<P : Parcelable>(val profile: P) : Mode<P>()
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"
)
},
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TopBar(onBack: () -> Unit = {}) { private fun TopBar(onBack: () -> Unit) {
TopAppBar( TopAppBar(
title = { title = {
Text(stringResource(R.string.app_profile)) Text(stringResource(R.string.profile))
}, },
navigationIcon = { navigationIcon = {
IconButton( IconButton(
@@ -342,4 +231,17 @@ private fun TopBar(onBack: () -> Unit = {}) {
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
}, },
) )
} }
@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 },
)
}

View File

@@ -15,9 +15,10 @@ enum class BottomBarDestination(
val direction: DirectionDestinationSpec, val direction: DirectionDestinationSpec,
@StringRes val label: Int, @StringRes val label: Int,
val iconSelected: ImageVector, val iconSelected: ImageVector,
val iconNotSelected: ImageVector val iconNotSelected: ImageVector,
val rootRequired: Boolean,
) { ) {
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home), Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security), SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps) Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true)
} }

View File

@@ -33,7 +33,6 @@ import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
@OptIn(ExperimentalMaterial3Api::class)
@RootNavGraph(start = true) @RootNavGraph(start = true)
@Destination @Destination
@Composable @Composable

View File

@@ -35,7 +35,6 @@ import java.util.*
* @author weishu * @author weishu
* @date 2023/1/1. * @date 2023/1/1.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@Destination @Destination
fun InstallScreen(navigator: DestinationsNavigator, uri: Uri) { fun InstallScreen(navigator: DestinationsNavigator, uri: Uri) {

View File

@@ -39,7 +39,6 @@ import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Destination @Destination
@Composable @Composable
fun ModuleScreen(navigator: DestinationsNavigator) { fun ModuleScreen(navigator: DestinationsNavigator) {

View File

@@ -2,6 +2,7 @@ package me.weishu.kernelsu.ui.screen
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.alorma.compose.settings.ui.*
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -28,7 +28,6 @@ import me.weishu.kernelsu.ui.util.getBugreportFile
* @author weishu * @author weishu
* @date 2023/1/1. * @date 2023/1/1.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Destination @Destination
@Composable @Composable
fun SettingScreen(navigator: DestinationsNavigator) { fun SettingScreen(navigator: DestinationsNavigator) {
@@ -50,11 +49,9 @@ fun SettingScreen(navigator: DestinationsNavigator) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val dialogHost = LocalDialogHost.current val dialogHost = LocalDialogHost.current
SettingsMenuLink( ListItem(
title = { headlineContent = { Text(stringResource(id = R.string.send_log)) },
Text(stringResource(id = R.string.send_log)) modifier = Modifier.clickable {
},
onClick = {
scope.launch { scope.launch {
val bugreport = dialogHost.withLoading { val bugreport = dialogHost.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -85,11 +82,9 @@ fun SettingScreen(navigator: DestinationsNavigator) {
) )
val about = stringResource(id = R.string.about) val about = stringResource(id = R.string.about)
SettingsMenuLink( ListItem(
title = { headlineContent = { Text(about) },
Text(about) modifier = Modifier.clickable {
},
onClick = {
showAboutDialog.value = true showAboutDialog.value = true
} }
) )
@@ -108,4 +103,4 @@ private fun TopBar(onBack: () -> Unit = {}) {
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
}, },
) )
} }

View File

@@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -24,18 +23,13 @@ import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.ConfirmDialog 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.component.SearchAppBar
import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination 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 me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
import java.util.*
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Destination @Destination
@Composable @Composable
fun SuperUserScreen(navigator: DestinationsNavigator) { fun SuperUserScreen(navigator: DestinationsNavigator) {
@@ -107,18 +101,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
.padding(innerPadding) .padding(innerPadding)
.pullRefresh(refreshState) .pullRefresh(refreshState)
) { ) {
val failMessage = stringResource(R.string.superuser_failed_to_grant_root)
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(viewModel.appList, key = { it.packageName + it.uid }) { app -> items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
AppItem(app) { AppItem(app) {
navigator.navigate( navigator.navigate(AppProfileScreenDestination(app))
AppProfileScreenDestination(
packageName = app.packageName,
grantRoot = app.onAllowList,
label = app.label, icon = app.icon
)
)
} }
} }
@@ -133,7 +119,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun AppItem( private fun AppItem(
app: SuperUserViewModel.AppInfo, app: SuperUserViewModel.AppInfo,
@@ -141,12 +126,12 @@ private fun AppItem(
) { ) {
ListItem( ListItem(
modifier = Modifier.clickable(onClick = onClickListener), modifier = Modifier.clickable(onClick = onClickListener),
headlineText = { Text(app.label) }, headlineContent = { Text(app.label) },
supportingText = { Text(app.packageName) }, supportingContent = { Text(app.packageName) },
leadingContent = { leadingContent = {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current)
.data(app.icon) .data(app.packageInfo)
.crossfade(true) .crossfade(true)
.build(), .build(),
contentDescription = app.label, contentDescription = app.label,

View File

@@ -6,6 +6,7 @@ import android.content.ServiceConnection
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.IBinder import android.os.IBinder
import android.os.Parcelable
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -16,6 +17,7 @@ import androidx.lifecycle.ViewModel
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import me.weishu.kernelsu.IKsuInterface import me.weishu.kernelsu.IKsuInterface
import me.weishu.kernelsu.Natives import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ksuApp
@@ -34,14 +36,18 @@ class SuperUserViewModel : ViewModel() {
private var apps by mutableStateOf<List<AppInfo>>(emptyList()) private var apps by mutableStateOf<List<AppInfo>>(emptyList())
} }
class AppInfo( @Parcelize
data class AppInfo(
val label: String, val label: String,
val packageName: String, val packageInfo: PackageInfo,
val icon: PackageInfo,
val uid: Int,
val onAllowList: Boolean, 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 search by mutableStateOf("")
var showSystemApps by mutableStateOf(false) var showSystemApps by mutableStateOf(false)
@@ -67,7 +73,7 @@ class SuperUserViewModel : ViewModel() {
.toPinyinString(it.label).contains(search) .toPinyinString(it.label).contains(search)
}.filter { }.filter {
it.uid == 2000 // Always show shell 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 { val result = connectKsuService {
Log.w(TAG, "KsuService disconnected") Log.w(TAG, "KsuService disconnected")
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val allowList = Natives.getAllowList().toSet() val allowList = Natives.getAllowList().toSet()
@@ -130,9 +136,7 @@ class SuperUserViewModel : ViewModel() {
val uid = appInfo.uid val uid = appInfo.uid
AppInfo( AppInfo(
label = appInfo.loadLabel(pm).toString(), label = appInfo.loadLabel(pm).toString(),
packageName = it.packageName, packageInfo = it,
icon = it,
uid = uid,
onAllowList = uid in allowList, onAllowList = uid in allowList,
onDenyList = uid in denyList onDenyList = uid in denyList
) )

View File

@@ -65,15 +65,15 @@
<string name="home_support_title">Support Us</string> <string name="home_support_title">Support Us</string>
<string name="home_support_content">KernelSU is, and always will be, free, and open source. You can however show us that you care by making a donation.</string> <string name="home_support_content">KernelSU is, and always will be, free, and open source. You can however show us that you care by making a donation.</string>
<string name="about_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string> <string name="about_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
<string name="app_profile" translatable="false">App Profile</string> <string name="profile">App profile</string>
<string name="app_profile_title1">Application</string> <string name="profile_default">Use default profile</string>
<string name="app_profile_title2" translatable="false">Root Profile</string> <string name="profile_template">Use template profile</string>
<string name="app_profile_allowlist">Allowlist</string> <string name="profile_custom">Use custom profile</string>
<string name="app_profile_denylist">Denylist</string> <string name="profile_name">Profile name</string>
<string name="app_profile_title0">Global</string> <string name="profile_namespace">Mount namespace</string>
<string name="app_profile_mode">Working Mode</string> <string name="profile_namespace_inherited">Inherited</string>
<string name="failed_to_set_allowlist_mode">Failed to switch to allowlist mode</string> <string name="profile_namespace_global">Global</string>
<string name="failed_to_add_to_allowlist">Failed to add %s to allowlist</string> <string name="profile_namespace_individual">Individual</string>
<string name="failed_to_add_to_denylist">Failed to add %s to denylist</string> <string name="profile_unmount_modules">Unmount modules</string>
<string name="failed_to_set_denylist_mode">Failed to switch to denylist mode</string> <string name="profile_allow_root_request">Allow root request</string>
</resources> </resources>

View File

@@ -1,12 +1,12 @@
[versions] [versions]
agp = "8.0.0" agp = "8.0.1"
kotlin = "1.8.10" kotlin = "1.8.10"
ksp = "1.8.10-1.0.9" ksp = "1.8.10-1.0.9"
compose-bom = "2023.04.01" compose-bom = "2023.05.01"
lifecycle = "2.6.1" lifecycle = "2.6.1"
accompanist = "0.30.0" accompanist = "0.30.0"
navigation = "2.5.3" navigation = "2.5.3"
compose-destination = "1.9.40-beta" compose-destination = "1.9.42-beta"
libsu = "5.0.5" libsu = "5.0.5"
[plugins] [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-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-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-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" } 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" } 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" } me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version = "1.5.0" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME 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 distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME