manager: implement app profile api call

This commit is contained in:
weishu
2023-05-17 11:23:46 +08:00
parent f2cb841b8a
commit 41265b0203
16 changed files with 395 additions and 267 deletions

View File

@@ -1,42 +0,0 @@
package me.weishu.kernelsu;
/**
* @author weishu
* @date 2022/12/8.
*/
public final class Natives {
static {
System.loadLibrary("kernelsu");
}
// become root manager, return true if success.
public static native boolean becomeManager(String pkg);
public static native int getVersion();
// get the uid list of allowed su processes.
public static native int[] getAllowList();
public static native int[] getDenyList();
public static native boolean allowRoot(int uid, boolean allow);
public static native boolean isSafeMode();
public static native boolean isAllowlistMode();
public static native boolean setAllowlistMode(boolean isAllowlist);
public static native boolean isUidInAllowlist(int uid);
public static native boolean isUidInDenylist(int uid);
public static native boolean addUidToAllowlist(int uid);
public static native boolean removeUidFromAllowlist(int uid);
public static native boolean addUidToDenylist(int uid);
public static native boolean removeUidFromDenylist(int uid);
}

View File

@@ -0,0 +1,75 @@
package me.weishu.kernelsu
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
/**
* @author weishu
* @date 2022/12/8.
*/
object Natives {
const val DEFAULT_ROOT_PROFILE_KEY = "_root_default_"
const val DEFAULT_NON_ROOT_PROFILE_KEY = "_non_root_default_"
init {
System.loadLibrary("kernelsu")
}
// become root manager, return true if success.
external fun becomeManager(pkg: String?): Boolean
val version: Int
external get
// get the uid list of allowed su processes.
val allowList: IntArray
external get
val denyList: IntArray
external get
external fun allowRoot(uid: Int, allow: Boolean): Boolean
val isSafeMode: Boolean
external get
/**
* Get the profile of the given package.
* @param key usually the package name
* @return return null if failed.
*/
external fun getAppProfile(key: String?, uid: Int): Profile
external fun setAppProfile(profile: Profile?): Boolean
@Immutable
@Parcelize
data class Profile(
// and there is a default profile for root and non-root
val name: String,
// current uid for the package, this is convivent for kernel to check
// if the package name doesn't match uid, then it should be invalidated.
val currentUid: Int = 0,
// if this is true, kernel will grant root permission to this package
val allowSu: Boolean = false,
// these are used for root profile
val rootUseDefault: Boolean = true,
val rootTemplate: String? = null,
val uid: Int = 0,
val gid: Int = 0,
val groups: List<Int> = mutableListOf(),
val capabilities: List<Int> = mutableListOf(),
val context: String = "su",
val namespace: Namespace = Namespace.Inherited,
val nonRootUseDefault: Boolean = true,
val umountModules: Boolean = false,
) : Parcelable {
enum class Namespace {
Inherited,
Global,
Individual,
}
constructor(): this("")
}
}

View File

@@ -1,13 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -11,36 +11,30 @@ 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.Natives
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,
profile: Natives.Profile,
onProfileChange: (Natives.Profile) -> Unit,
) {
Column(modifier = modifier) {
if (!fixedName) {
OutlinedTextField(
label = { Text(stringResource(R.string.profile_name)) },
value = profile.profileName,
onValueChange = { onProfileChange(profile.copy(profileName = it)) }
value = profile.name,
onValueChange = { onProfileChange(profile.copy(name = 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)) }
checked = profile.umountModules,
onCheckedChange = { onProfileChange(profile.copy(umountModules = it)) }
)
}
}
@@ -48,7 +42,7 @@ fun AppProfileConfig(
@Preview
@Composable
private fun AppProfileConfigPreview() {
var profile by remember { mutableStateOf(AppProfile("")) }
var profile by remember { mutableStateOf(Natives.Profile("")) }
AppProfileConfig(fixedName = true, profile = profile) {
profile = it
}

View File

@@ -21,31 +21,31 @@ 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.Natives
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,
profile: Natives.Profile,
onProfileChange: (Natives.Profile) -> Unit,
) {
Column(modifier = modifier) {
if (!fixedName) {
OutlinedTextField(
label = { Text(stringResource(R.string.profile_name)) },
value = profile.profileName,
onValueChange = { onProfileChange(profile.copy(profileName = it)) }
value = profile.name,
onValueChange = { onProfileChange(profile.copy(name = it)) }
)
}
var expanded 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)
Natives.Profile.Namespace.Inherited -> stringResource(R.string.profile_namespace_inherited)
Natives.Profile.Namespace.Global -> stringResource(R.string.profile_namespace_global)
Natives.Profile.Namespace.Individual -> stringResource(R.string.profile_namespace_individual)
}
ListItem(headlineContent = {
ExposedDropdownMenuBox(
@@ -70,21 +70,21 @@ fun RootProfileConfig(
DropdownMenuItem(
text = { Text(stringResource(R.string.profile_namespace_inherited)) },
onClick = {
onProfileChange(profile.copy(namespace = RootProfile.Namespace.Inherited))
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Inherited))
expanded = false
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.profile_namespace_global)) },
onClick = {
onProfileChange(profile.copy(namespace = RootProfile.Namespace.Global))
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Global))
expanded = false
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.profile_namespace_individual)) },
onClick = {
onProfileChange(profile.copy(namespace = RootProfile.Namespace.Individual))
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Individual))
expanded = false
},
)
@@ -97,7 +97,18 @@ fun RootProfileConfig(
label = { Text("uid") },
value = profile.uid.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { onProfileChange(profile.copy(uid = it.toInt())) }
onValueChange = {
if (it.isNotEmpty()) {
it.filter { symbol ->
symbol.isDigit()
}.let { filtered ->
filtered.ifEmpty { "0" }
}.let { value ->
onProfileChange(profile.copy(uid = value.toInt(), rootUseDefault = false))
}
}
}
)
})
@@ -106,16 +117,37 @@ fun RootProfileConfig(
label = { Text("gid") },
value = profile.gid.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { onProfileChange(profile.copy(gid = it.toInt())) }
onValueChange = {
if (it.isNotEmpty()) {
it.filter { symbol ->
symbol.isDigit()
}.let { filtered ->
filtered.ifEmpty { "0" }
}.let { value ->
onProfileChange(profile.copy(gid = value.toInt(), rootUseDefault = false))
}
}
}
)
})
ListItem(headlineContent = {
OutlinedTextField(
label = { Text("groups") },
value = profile.groups.toString(),
value = profile.groups.joinToString(","),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { onProfileChange(profile.copy(groups = it.toInt())) }
onValueChange = { s ->
if (s.isNotEmpty()) {
s.filter { symbol ->
symbol.isDigit() || symbol == ','
}.let { filtered ->
filtered.ifEmpty { "0" }
}.let { value ->
val groups = value.split(',').filter { it.isNotEmpty() }.map { it.toInt() }
onProfileChange(profile.copy(groups = groups, rootUseDefault = false))
}
}
}
)
})
@@ -123,7 +155,9 @@ fun RootProfileConfig(
OutlinedTextField(
label = { Text("context") },
value = profile.context,
onValueChange = { onProfileChange(profile.copy(context = it)) }
onValueChange = {
onProfileChange(profile.copy(context = it, rootUseDefault = false))
}
)
})
}
@@ -132,7 +166,7 @@ fun RootProfileConfig(
@Preview
@Composable
private fun RootProfileConfigPreview() {
var profile by remember { mutableStateOf(RootProfile("")) }
var profile by remember { mutableStateOf(Natives.Profile("")) }
RootProfileConfig(fixedName = true, profile = profile) {
profile = it
}

View File

@@ -1,5 +1,6 @@
package me.weishu.kernelsu.ui.screen
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
@@ -49,8 +50,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
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.SwitchItem
import me.weishu.kernelsu.ui.component.profile.AppProfileConfig
import me.weishu.kernelsu.ui.component.profile.RootProfileConfig
@@ -70,8 +69,15 @@ fun AppProfileScreen(
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) }
val failToUpdateAppProfile =
stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
val packageName = appInfo.packageName
var profile by rememberSaveable {
mutableStateOf(Natives.getAppProfile(packageName, appInfo.uid))
}
Log.i("mylog", "profile: $profile")
Scaffold(
topBar = { TopBar { navigator.popBackStack() } }
@@ -95,14 +101,13 @@ fun AppProfileScreen(
.height(48.dp)
)
},
isRootGranted = isRootGranted,
onSwitchRootPermission = { grant ->
profile = profile,
onProfileChange = {
scope.launch {
val success = Natives.allowRoot(appInfo.uid, grant)
if (success) {
isRootGranted = grant
if (!Natives.setAppProfile(it)) {
snackbarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
} else {
snackbarHost.showSnackbar(failToGrantRoot.format(appInfo.uid))
profile = it
}
}
},
@@ -117,9 +122,11 @@ private fun AppProfileInner(
packageName: String,
appLabel: String,
appIcon: @Composable () -> Unit,
isRootGranted: Boolean,
onSwitchRootPermission: (Boolean) -> Unit,
profile: Natives.Profile,
onProfileChange: (Natives.Profile) -> Unit,
) {
val isRootGranted = profile.allowSu
Column(modifier = modifier) {
ListItem(
headlineContent = { Text(appLabel) },
@@ -131,14 +138,32 @@ private fun AppProfileInner(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.superuser),
checked = isRootGranted,
onCheckedChange = onSwitchRootPermission,
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
)
Crossfade(targetState = isRootGranted, label = "") { current ->
Column {
if (current) {
var mode by rememberSaveable { mutableStateOf(Mode.Default) }
ProfileBox(mode, true) { mode = it }
val mode = if (profile.rootUseDefault) {
Mode.Default
} else if (profile.rootTemplate != null) {
Mode.Template
} else {
Mode.Custom
}
ProfileBox(mode, true) {
when (it) {
Mode.Default -> {
onProfileChange(profile.copy(rootUseDefault = true))
}
Mode.Template -> {
onProfileChange(profile.copy(rootUseDefault = false))
}
else -> {
onProfileChange(profile.copy(rootUseDefault = false))
}
}
}
Crossfade(targetState = mode, label = "") { currentMode ->
if (currentMode == Mode.Template) {
var expanded by remember { mutableStateOf(false) }
@@ -163,11 +188,10 @@ private fun AppProfileInner(
}
})
} else if (mode == Mode.Custom) {
var profile by rememberSaveable { mutableStateOf(RootProfile("@$packageName")) }
RootProfileConfig(
fixedName = true,
profile = profile,
onProfileChange = { profile = it }
onProfileChange = onProfileChange
)
}
}
@@ -176,11 +200,10 @@ private fun AppProfileInner(
ProfileBox(mode, false) { mode = it }
Crossfade(targetState = mode, label = "") { currentMode ->
if (currentMode == Mode.Custom) {
var profile by rememberSaveable { mutableStateOf(AppProfile(packageName)) }
AppProfileConfig(
fixedName = true,
profile = profile,
onProfileChange = { profile = it }
onProfileChange = onProfileChange
)
}
}
@@ -256,12 +279,15 @@ private fun ProfileBox(
@Preview
@Composable
private fun AppProfilePreview() {
var isRootGranted by remember { mutableStateOf(false) }
var profile by remember { mutableStateOf(Natives.Profile("")) }
AppProfileInner(
packageName = "icu.nullptr.test",
appLabel = "Test",
appIcon = { Icon(Icons.Filled.Android, null) },
isRootGranted = isRootGranted,
onSwitchRootPermission = { isRootGranted = it },
profile = profile,
onProfileChange = {
profile = it
},
)
}

View File

@@ -56,7 +56,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
SideEffect {
if (isManager) install()
}
val ksuVersion = if (isManager) Natives.getVersion() else null
val ksuVersion = if (isManager) Natives.version else null
StatusCard(kernelVersion, ksuVersion)
InfoCard()
@@ -141,7 +141,7 @@ private fun StatusCard(kernelVersion: KernelVersion, ksuVersion: Int?) {
) {
when {
ksuVersion != null -> {
val appendText = if (Natives.isSafeMode()) {
val appendText = if (Natives.isSafeMode) {
" [${stringResource(id = R.string.safe_mode)}]"
} else {
""

View File

@@ -50,8 +50,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
}
}
val isSafeMode = Natives.isSafeMode()
val isKSUVersionInvalid = Natives.getVersion() < 0
val isSafeMode = Natives.isSafeMode
val isKSUVersionInvalid = Natives.version < 0
val hasMagisk = hasMagisk()
val hideInstallButton = isSafeMode || isKSUVersionInvalid || hasMagisk

View File

@@ -71,7 +71,7 @@ fun getModuleCount(): Int {
}
fun getSuperuserCount(): Int {
return Natives.getAllowList().size
return Natives.allowList.size
}
fun toggleModule(id: String, enable: Boolean): Boolean {

View File

@@ -72,9 +72,9 @@ fun getBugreportFile(context: Context): File {
pw.println("Nodename: ${uname.nodename}")
pw.println("Sysname: ${uname.sysname}")
val ksuKernel = Natives.getVersion()
val ksuKernel = Natives.version
pw.println("KernelSU: $ksuKernel")
val safeMode = Natives.isSafeMode()
val safeMode = Natives.isSafeMode
pw.println("SafeMode: $safeMode")
}

View File

@@ -116,8 +116,8 @@ class SuperUserViewModel : ViewModel() {
withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager
val allowList = Natives.getAllowList().toSet()
val denyList = Natives.getDenyList().toSet()
val allowList = Natives.allowList.toSet()
val denyList = Natives.denyList.toSet()
Log.i(TAG, "allowList: $allowList")
Log.i(TAG, "denyList: $denyList")
val start = SystemClock.elapsedRealtime()