manager: Add app profile UI

This commit is contained in:
tiann
2023-05-16 15:06:31 +08:00
parent f81caf75a9
commit eccce7b31f
4 changed files with 303 additions and 39 deletions

View File

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

View File

@@ -1,5 +1,6 @@
package me.weishu.kernelsu.ui.screen package me.weishu.kernelsu.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -21,12 +22,14 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.weishu.kernelsu.Natives 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.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.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalDialogHost
import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.LocalSnackbarHost
import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
@@ -35,9 +38,8 @@ import java.util.*
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Destination @Destination
@Composable @Composable
fun SuperUserScreen() { fun SuperUserScreen(navigator: DestinationsNavigator) {
val viewModel = viewModel<SuperUserViewModel>() val viewModel = viewModel<SuperUserViewModel>()
val snackbarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -109,35 +111,16 @@ fun SuperUserScreen() {
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(viewModel.appList, key = { it.packageName + it.uid }) { app -> items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
var isChecked by rememberSaveable(app) { mutableStateOf(app.onAllowList) } AppItem(app) {
val dialogHost = LocalDialogHost.current navigator.navigate(
val content = AppProfileScreenDestination(
stringResource(id = R.string.superuser_allow_root_confirm, app.label) packageName = app.packageName,
val confirm = stringResource(id = android.R.string.ok) grantRoot = app.onAllowList,
val cancel = stringResource(id = android.R.string.cancel) label = app.label, icon = app.icon
)
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))
}
}
} }
} }
} }
@@ -154,10 +137,10 @@ fun SuperUserScreen() {
@Composable @Composable
private fun AppItem( private fun AppItem(
app: SuperUserViewModel.AppInfo, app: SuperUserViewModel.AppInfo,
isChecked: Boolean, onClickListener: () -> Unit,
onCheckedChange: (Boolean) -> Unit
) { ) {
ListItem( ListItem(
modifier = Modifier.clickable(onClick = onClickListener),
headlineText = { Text(app.label) }, headlineText = { Text(app.label) },
supportingText = { Text(app.packageName) }, supportingText = { Text(app.packageName) },
leadingContent = { leadingContent = {
@@ -173,12 +156,5 @@ private fun AppItem(
.height(48.dp) .height(48.dp)
) )
}, },
trailingContent = {
Switch(
checked = isChecked,
onCheckedChange = onCheckedChange,
modifier = Modifier.padding(4.dp)
)
}
) )
} }

View File

@@ -61,4 +61,6 @@
<string name="home_support_title">支持开发</string> <string name="home_support_title">支持开发</string>
<string name="home_support_content">KernelSU 将保持免费和开源,向开发者捐赠以表示支持。</string> <string name="home_support_content">KernelSU 将保持免费和开源,向开发者捐赠以表示支持。</string>
<string name="about_source_code"><![CDATA[在 %1$s 查看源码<br/>加入我们的 %2$s 频道<br/>加入我们的 <b><a href="https://pd.qq.com/s/8lipl1brp">QQ 频道</a></b>]]></string> <string name="about_source_code"><![CDATA[在 %1$s 查看源码<br/>加入我们的 %2$s 频道<br/>加入我们的 <b><a href="https://pd.qq.com/s/8lipl1brp">QQ 频道</a></b>]]></string>
<string name="app_profile_title1">应用</string>
<string name="app_profile_root_switch">Root 权限</string>
</resources> </resources>

View File

@@ -65,4 +65,9 @@
<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="app_profile_title1">Application</string>
<string name="app_profile_title2" translatable="false">Root Profile</string>
<string name="app_profile_root_switch">Grant Root</string>
<string name="app_profile_allowlist">Allowlist</string>
</resources> </resources>