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.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)

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.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,

View File

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

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
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 = {
AppProfileInner(
modifier = Modifier.padding(paddingValues),
packageName = appInfo.packageName,
appLabel = appInfo.label,
appIcon = {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(icon)
model = ImageRequest.Builder(context)
.data(appInfo.packageInfo)
.crossfade(true)
.build(),
contentDescription = label,
contentDescription = appInfo.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 ->
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,
)
}
}
@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
) {
ListItem(
headlineText = { Text(title) },
leadingContent = {
Icon(
icon,
contentDescription = title
)
},
trailingContent = {
Switch(checked = checked, onCheckedChange = onCheckChange)
}
)
}
@Composable
private fun Uid() {
ListItem(
headlineText = {
Text("Uid: 0")
},
leadingContent = {
Icon(
Icons.Filled.PermIdentity,
contentDescription = "Uid"
)
},
)
}
@Composable
private fun Gid() {
ListItem(
headlineText = { Text("Gid: 0") },
leadingContent = {
Icon(
Icons.Filled.Group,
contentDescription = "Gid"
)
},
)
}
@Composable
private fun Groups() {
ListItem(
headlineText = { Text("Groups: 0") },
leadingContent = {
Icon(
Icons.Filled.Groups3,
contentDescription = "Groups"
)
},
)
}
@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"
)
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}) {
private fun AppProfileInner(
modifier: Modifier = Modifier,
packageName: String,
appLabel: String,
appIcon: @Composable () -> Unit,
isRootGranted: Boolean,
onSwitchRootPermission: (Boolean) -> Unit,
) {
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<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 }
)
}
}
}
}
}
}
@Parcelize
private sealed class Mode<P : Parcelable> : Parcelable {
class Default<P : Parcelable> : Mode<P>()
class Template<P : Parcelable>(val template: String) : Mode<P>()
class Custom<P : Parcelable>(val profile: P) : Mode<P>()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit) {
TopAppBar(
title = {
Text(stringResource(R.string.app_profile))
Text(stringResource(R.string.profile))
},
navigationIcon = {
IconButton(
@@ -343,3 +232,16 @@ private fun TopBar(onBack: () -> Unit = {}) {
},
)
}
@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,
@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)
}

View File

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

View File

@@ -35,7 +35,6 @@ import java.util.*
* @author weishu
* @date 2023/1/1.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Destination
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.viewmodel.ModuleViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun ModuleScreen(navigator: DestinationsNavigator) {

View File

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

View File

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

View File

@@ -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<List<AppInfo>>(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
}
}
@@ -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
)

View File

@@ -65,15 +65,15 @@
<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="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="app_profile_title1">Application</string>
<string name="app_profile_title2" translatable="false">Root Profile</string>
<string name="app_profile_allowlist">Allowlist</string>
<string name="app_profile_denylist">Denylist</string>
<string name="app_profile_title0">Global</string>
<string name="app_profile_mode">Working Mode</string>
<string name="failed_to_set_allowlist_mode">Failed to switch to allowlist mode</string>
<string name="failed_to_add_to_allowlist">Failed to add %s to allowlist</string>
<string name="failed_to_add_to_denylist">Failed to add %s to denylist</string>
<string name="failed_to_set_denylist_mode">Failed to switch to denylist mode</string>
<string name="profile">App profile</string>
<string name="profile_default">Use default profile</string>
<string name="profile_template">Use template profile</string>
<string name="profile_custom">Use custom profile</string>
<string name="profile_name">Profile name</string>
<string name="profile_namespace">Mount namespace</string>
<string name="profile_namespace_inherited">Inherited</string>
<string name="profile_namespace_global">Global</string>
<string name="profile_namespace_individual">Individual</string>
<string name="profile_unmount_modules">Unmount modules</string>
<string name="profile_allow_root_request">Allow root request</string>
</resources>

View File

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

View File

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