manager: Add app profile UI
This commit is contained in:
@@ -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) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user