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
|
||||
|
||||
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<SuperUserViewModel>()
|
||||
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
|
||||
AppItem(app) {
|
||||
navigator.navigate(
|
||||
AppProfileScreenDestination(
|
||||
packageName = app.packageName,
|
||||
grantRoot = app.onAllowList,
|
||||
label = app.label, icon = app.icon
|
||||
)
|
||||
)
|
||||
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
|
||||
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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,4 +61,6 @@
|
||||
<string name="home_support_title">支持开发</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="app_profile_title1">应用</string>
|
||||
<string name="app_profile_root_switch">Root 权限</string>
|
||||
</resources>
|
||||
|
||||
@@ -65,4 +65,9 @@
|
||||
<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_root_switch">Grant Root</string>
|
||||
<string name="app_profile_allowlist">Allowlist</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user