From eccce7b31fe9862ebf263f676cd74488e479c0eb Mon Sep 17 00:00:00 2001 From: tiann Date: Tue, 16 May 2023 15:06:31 +0800 Subject: [PATCH] manager: Add app profile UI --- .../weishu/kernelsu/ui/screen/AppProfile.kt | 281 ++++++++++++++++++ .../me/weishu/kernelsu/ui/screen/SuperUser.kt | 54 +--- .../src/main/res/values-zh-rCN/strings.xml | 2 + manager/app/src/main/res/values/strings.xml | 5 + 4 files changed, 303 insertions(+), 39 deletions(-) create mode 100644 manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt new file mode 100644 index 00000000..03ed4cc1 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt @@ -0,0 +1,281 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package me.weishu.kernelsu.ui.screen + +import android.content.pm.PackageInfo +import androidx.compose.foundation.clickable +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.ArrowBack +import androidx.compose.material.icons.filled.DeveloperMode +import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Groups2 +import androidx.compose.material.icons.filled.Groups3 +import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.filled.PermDeviceInformation +import androidx.compose.material.icons.filled.PermIdentity +import androidx.compose.material.icons.filled.Rule +import androidx.compose.material.icons.filled.RuleFolder +import androidx.compose.material.icons.filled.SafetyDivider +import androidx.compose.material.icons.filled.Security +import androidx.compose.material3.Divider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +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 +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.alorma.compose.settings.ui.SettingsGroup +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.AboutDialog +import me.weishu.kernelsu.ui.component.ConfirmResult +import me.weishu.kernelsu.ui.component.LoadingDialog +import me.weishu.kernelsu.ui.util.LocalDialogHost +import me.weishu.kernelsu.ui.util.LocalSnackbarHost + +/** + * @author weishu + * @date 2023/5/16. + */ +@Destination +@Composable +fun AppProfileScreen( + navigator: DestinationsNavigator, + packageName: String, + grantRoot: Boolean, + label: String, + icon: PackageInfo +) { + + val snackbarHost = LocalSnackbarHost.current + + Scaffold( + topBar = { + TopBar(onBack = { + navigator.popBackStack() + }) + } + ) { paddingValues -> + LoadingDialog() + + val showAboutDialog = remember { mutableStateOf(false) } + AboutDialog(showAboutDialog) + + Column(modifier = Modifier.padding(paddingValues)) { + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + 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 isChecked by rememberSaveable { + mutableStateOf(grantRoot) + } + + val failMessage = stringResource(R.string.superuser_failed_to_grant_root) + val uid = icon.applicationInfo.uid; + AppSwitch( + Icons.Filled.Security, + stringResource(id = R.string.app_profile_root_switch), + checked = isChecked + ) { checked -> + + scope.launch { + val success = Natives.allowRoot(uid, checked) + if (success) { + isChecked = checked + } else { + snackbarHost.showSnackbar(failMessage.format(uid)) + } + } + } + + AppSwitch( + icon = Icons.Filled.List, + title = stringResource(id = R.string.app_profile_allowlist), + checked = true + ) { + + } + + 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(30.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 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 = {}) { + TopAppBar( + title = { + Text(stringResource(R.string.app_profile)) + }, + navigationIcon = { + IconButton( + onClick = onBack + ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } + }, + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt index 1d0874d1..2d6f40f2 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt @@ -1,5 +1,6 @@ package me.weishu.kernelsu.ui.screen +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -21,12 +22,14 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage 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 @@ -35,9 +38,8 @@ import java.util.* @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Destination @Composable -fun SuperUserScreen() { +fun SuperUserScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() - val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() LaunchedEffect(Unit) { @@ -109,35 +111,16 @@ fun SuperUserScreen() { LazyColumn(Modifier.fillMaxSize()) { items(viewModel.appList, key = { it.packageName + it.uid }) { app -> - var isChecked by rememberSaveable(app) { mutableStateOf(app.onAllowList) } - val dialogHost = LocalDialogHost.current - val content = - stringResource(id = R.string.superuser_allow_root_confirm, app.label) - val confirm = stringResource(id = android.R.string.ok) - val cancel = stringResource(id = android.R.string.cancel) - - AppItem(app, isChecked) { checked -> - scope.launch { - if (checked) { - val confirmResult = dialogHost.showConfirm( - app.label, - content = content, - confirm = confirm, - dismiss = cancel - ) - if (confirmResult != ConfirmResult.Confirmed) { - return@launch - } - } - - val success = Natives.allowRoot(app.uid, checked) - if (success) { - isChecked = checked - } else { - snackbarHost.showSnackbar(failMessage.format(app.uid)) - } - } + AppItem(app) { + navigator.navigate( + AppProfileScreenDestination( + packageName = app.packageName, + grantRoot = app.onAllowList, + label = app.label, icon = app.icon + ) + ) } + } } @@ -154,10 +137,10 @@ fun SuperUserScreen() { @Composable private fun AppItem( app: SuperUserViewModel.AppInfo, - isChecked: Boolean, - onCheckedChange: (Boolean) -> Unit + onClickListener: () -> Unit, ) { ListItem( + modifier = Modifier.clickable(onClick = onClickListener), headlineText = { Text(app.label) }, supportingText = { Text(app.packageName) }, leadingContent = { @@ -173,12 +156,5 @@ private fun AppItem( .height(48.dp) ) }, - trailingContent = { - Switch( - checked = isChecked, - onCheckedChange = onCheckedChange, - modifier = Modifier.padding(4.dp) - ) - } ) } diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index dc4eba7e..7c3791e4 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -61,4 +61,6 @@ 支持开发 KernelSU 将保持免费和开源,向开发者捐赠以表示支持。 加入我们的 %2$s 频道
加入我们的 QQ 频道]]>
+ 应用 + Root 权限 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index d4942bb7..46601de7 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -65,4 +65,9 @@ Support Us KernelSU is, and always will be, free, and open source. You can however show us that you care by making a donation. Join our %2$s channel]]> + App Profile + Application + Root Profile + Grant Root + Allowlist