From 68d639e32539b30f40867b4e9a333f0729cfadc8 Mon Sep 17 00:00:00 2001 From: weishu Date: Sat, 3 Jun 2023 15:00:42 +0800 Subject: [PATCH] manager: Add groups and caps for profile --- manager/app/build.gradle.kts | 3 + manager/app/src/main/cpp/jni.cc | 21 +- .../weishu/kernelsu/profile/Capabilities.kt | 49 +++++ .../java/me/weishu/kernelsu/profile/Groups.kt | 87 ++++++++ .../ui/component/profile/RootProfileConfig.kt | 203 ++++++++++++++++-- manager/gradle/libs.versions.toml | 4 + 6 files changed, 341 insertions(+), 26 deletions(-) create mode 100644 manager/app/src/main/java/me/weishu/kernelsu/profile/Capabilities.kt create mode 100644 manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index 22904a0c..7ebb399c 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -107,4 +107,7 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.me.zhanghai.android.appiconloader.coil) + + implementation(libs.sheet.compose.dialogs.core) + implementation(libs.sheet.compose.dialogs.list) } diff --git a/manager/app/src/main/cpp/jni.cc b/manager/app/src/main/cpp/jni.cc index db1d5b9b..c66edd33 100644 --- a/manager/app/src/main/cpp/jni.cc +++ b/manager/app/src/main/cpp/jni.cc @@ -84,6 +84,17 @@ static int getListSize(JNIEnv *env, jobject list) { return env->CallIntMethod(list, size); } +static void fillArrayWithList(JNIEnv* env, jobject list, int *data, int count) { + auto cls = env->GetObjectClass(list); + auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;"); + auto integerCls = env->FindClass("java/lang/Integer"); + auto intValue = env->GetMethodID(integerCls, "intValue", "()I"); + for (int i = 0; i < count; ++i) { + auto integer = env->CallObjectMethod(list, get, i); + data[i] = env->CallIntMethod(integer, intValue); + } +} + extern "C" JNIEXPORT jobject JNICALL Java_me_weishu_kernelsu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) { @@ -140,9 +151,6 @@ Java_me_weishu_kernelsu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, env->SetIntField(obj, gidField, profile.root_profile.gid); jobject groupList = env->GetObjectField(obj, groupsField); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - } fillIntArray(env, groupList, profile.root_profile.groups, profile.root_profile.groups_count); jobject capList = env->GetObjectField(obj, capabilitiesField); @@ -223,7 +231,12 @@ Java_me_weishu_kernelsu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobjec p.root_profile.uid = uid; p.root_profile.gid = gid; - p.root_profile.groups_count = getListSize(env, groups); + + int groups_count = getListSize(env, groups); + p.root_profile.groups_count = groups_count; + fillArrayWithList(env, groups, p.root_profile.groups, groups_count); + + fillArrayWithList(env, capabilities, p.root_profile.capabilities, 2); auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr); strcpy(p.root_profile.selinux_domain, cdomain); diff --git a/manager/app/src/main/java/me/weishu/kernelsu/profile/Capabilities.kt b/manager/app/src/main/java/me/weishu/kernelsu/profile/Capabilities.kt new file mode 100644 index 00000000..d52518c2 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/profile/Capabilities.kt @@ -0,0 +1,49 @@ +package me.weishu.kernelsu.profile + +/** + * @author weishu + * @date 2023/6/3. + */ +enum class Capabilities(val cap: Int, val display: String, val desc: String) { + CAP_CHOWN(0, "CHOWN", "Make arbitrary changes to file UIDs and GIDs (see chown(2))"), + CAP_DAC_OVERRIDE(1, "DAC_OVERRIDE", "Bypass file read, write, and execute permission checks"), + CAP_DAC_READ_SEARCH(2, "DAC_READ_SEARCH", "Bypass file read permission checks and directory read and execute permission checks"), + CAP_FOWNER(3, "FOWNER", "Bypass permission checks on operations that normally require the filesystem UID of the process to match the UID of the file (e.g., chmod(2), utime(2)), excluding those operations covered by CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH"), + CAP_FSETID(4, "FSETID", "Don’t clear set-user-ID and set-group-ID permission bits when a file is modified; set the set-group-ID bit for a file whose GID does not match the filesystem or any of the supplementary GIDs of the calling process"), + CAP_KILL(5, "KILL", "Bypass permission checks for sending signals (see kill(2))."), + CAP_SETGID(6, "SETGID", "Make arbitrary manipulations of process GIDs and supplementary GID list; allow setgid(2) manipulation of the caller’s effective and real group IDs"), + CAP_SETUID(7, "SETUID", "Make arbitrary manipulations of process UIDs (setuid(2), setreuid(2), setresuid(2), setfsuid(2)); allow changing the current process user IDs; allow changing of the current process group ID to any value in the system’s range of legal group IDs"), + CAP_SETPCAP(8, "SETPCAP", "If file capabilities are supported: grant or remove any capability in the caller’s permitted capability set to or from any other process. (This property supersedes the obsolete notion of giving a process all capabilities by granting all capabilities in its permitted set, and of removing all capabilities from a process by granting no capabilities in its permitted set. It does not permit any actions that were not permitted before.)"), + CAP_LINUX_IMMUTABLE(9, "LINUX_IMMUTABLE", "Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags (see chattr(1))."), + CAP_NET_BIND_SERVICE(10, "NET_BIND_SERVICE", "Bind a socket to Internet domain"), + CAP_NET_BROADCAST(11, "NET_BROADCAST", "Make socket broadcasts, and listen to multicasts"), + CAP_NET_ADMIN(12, "NET_ADMIN", "Perform various network-related operations: interface configuration, administration of IP firewall, masquerading, and accounting, modify routing tables, bind to any address for transparent proxying, set type-of-service (TOS), clear driver statistics, set promiscuous mode, enabling multicasting, use setsockopt(2) to set the following socket options: SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE"), + CAP_NET_RAW(13, "NET_RAW", "Use RAW and PACKET sockets"), + CAP_IPC_LOCK(14, "IPC_LOCK", "Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2))"), + CAP_IPC_OWNER(15, "IPC_OWNER", "Bypass permission checks for operations on System V IPC objects"), + CAP_SYS_MODULE(16, "SYS_MODULE", "Load and unload kernel modules (see init_module(2) and delete_module(2)); in kernels before 2.6.25, this also granted rights for various other operations related to kernel modules"), + CAP_SYS_RAWIO(17, "SYS_RAWIO", "Perform I/O port operations (iopl(2) and ioperm(2)); access /proc/kcore"), + CAP_SYS_CHROOT(18, "SYS_CHROOT", "Use chroot(2)"), + CAP_SYS_PTRACE(19, "SYS_PTRACE", "Trace arbitrary processes using ptrace(2)"), + CAP_SYS_PACCT(20, "SYS_PACCT", "Use acct(2)"), + CAP_SYS_ADMIN(21, "SYS_ADMIN", "Perform a range of system administration operations including: quotactl(2), mount(2), umount(2), swapon(2), swapoff(2), sethostname(2), and setdomainname(2); set and modify process resource limits (setrlimit(2)); perform various network-related operations (e.g., setting privileged socket options, enabling multicasting, interface configuration); perform various IPC operations (e.g., SysV semaphores, POSIX message queues, System V shared memory); allow reboot and kexec_load(2); override /proc/sys kernel tunables; perform ptrace(2) PTRACE_SECCOMP_GET_FILTER operation; perform some tracing and debugging operations (see ptrace(2)); administer the lifetime of kernel tracepoints (tracefs(5)); perform the KEYCTL_CHOWN and KEYCTL_SETPERM keyctl(2) operations; perform the following keyctl(2) operations: KEYCTL_CAPABILITIES, KEYCTL_CAPSQUASH, and KEYCTL_PKEY_ OPERATIONS; set state for the Extensible Authentication Protocol (EAP) kernel module; and override the RLIMIT_NPROC resource limit; allow ioperm/iopl access to I/O ports"), + CAP_SYS_BOOT(22, "SYS_BOOT", "Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution"), + CAP_SYS_NICE(23, "SYS_NICE", "Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes; set real-time scheduling policies for calling process, and set scheduling policies and priorities for arbitrary processes (sched_setscheduler(2), sched_setparam(2)"), + CAP_SYS_RESOURCE(24, "SYS_RESOURCE", "Override resource Limits. Set resource limits (setrlimit(2), prlimit(2)), override quota limits (quota(2), quotactl(2)), override reserved space on ext2 filesystem (ext2_ioctl(2)), override size restrictions on IPC message queues (msg(2)) and system V shared memory segments (shmget(2)), and override the /proc/sys/fs/pipe-size-max limit"), + CAP_SYS_TIME(25, "SYS_TIME", "Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock"), + CAP_SYS_TTY_CONFIG(26, "SYS_TTY_CONFIG", "Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals"), + CAP_MKNOD(27, "MKNOD", "Create special files using mknod(2)"), + CAP_LEASE(28, "LEASE", "Establish leases on arbitrary files (see fcntl(2))"), + CAP_AUDIT_WRITE(29, "AUDIT_WRITE", "Write records to kernel auditing log"), + CAP_AUDIT_CONTROL(30, "AUDIT_CONTROL", "Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules"), + CAP_SETFCAP(31, "SETFCAP", "If file capabilities are supported: grant or remove any capability in any capability set to any file"), + CAP_MAC_OVERRIDE(32, "MAC_OVERRIDE", "Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM)"), + CAP_MAC_ADMIN(33, "MAC_ADMIN", "Allow MAC configuration or state changes. Implemented for the Smack LSM"), + CAP_SYSLOG(34, "SYSLOG", "Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege"), + CAP_WAKE_ALARM(35, "WAKE_ALARM", "Trigger something that will wake up the system"), + CAP_BLOCK_SUSPEND(36, "BLOCK_SUSPEND", "Employ features that can block system suspend"), + CAP_AUDIT_READ(37, "AUDIT_READ", "Allow reading the audit log via a multicast netlink socket"), + CAP_PERFMON(38, "PERFMON", "Allow performance monitoring via perf_event_open(2)"), + CAP_BPF(39, "BPF", "Allow BPF operations via bpf(2)"), + CAP_CHECKPOINT_RESTORE(40, "CHECKPOINT_RESTORE", "Allow processes to be checkpointed via checkpoint/restore in user namespace(2)"), +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt b/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt new file mode 100644 index 00000000..8dd5e9f2 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt @@ -0,0 +1,87 @@ +package me.weishu.kernelsu.profile + +/** + * @author weishu + * @date 2023/6/3. + */ +enum class Groups(val gid: Int, val display: String, val desc: String) { + ROOT(0, "root", "traditional unix root user"), + DAEMON(1, "daemon", "Traditional unix daemon owner."), + BIN(2, "bin", "Traditional unix binaries owner."), + SYS(3, "sys", "A group with the same gid on Linux/macOS/Android."), + SYSTEM(1000, "system", "system server"), + RADIO(1001, "radio", "telephony subsystem, RIL"), + BLUETOOTH(1002, "bluetooth", "bluetooth subsystem"), + GRAPHICS(1003, "graphics", "graphics devices"), + INPUT(1004, "input", "input devices"), + AUDIO(1005, "audio", "audio devices"), + CAMERA(1006, "camera", "camera devices"), + LOG(1007, "log", "log devices"), + COMPASS(1008, "compass", "compass device"), + MOUNT(1009, "mount", "mountd socket"), + WIFI(1010, "wifi", "wifi subsystem"), + ADB(1011, "adb", "android debug bridge (adbd)"), + INSTALL(1012, "install", "group for installing packages"), + MEDIA(1013, "media", "mediaserver process"), + DHCP(1014, "dhcp", "dhcp client"), + SDCARD_RW(1015, "sdcard_rw", "external storage write access"), + VPN(1016, "vpn", "vpn system"), + KEYSTORE(1017, "keystore", "keystore subsystem"), + USB(1018, "usb", "USB devices"), + DRM(1019, "drm", "DRM server"), + MDNSR(1020, "mdnsr", "MulticastDNSResponder (service discovery)"), + GPS(1021, "gps", "GPS daemon"), + UNUSED1(1022, "unused1", "deprecated, DO NOT USE"), + MEDIA_RW(1023, "media_rw", "internal media storage write access"), + MTP(1024, "mtp", "MTP USB driver access"), + UNUSED2(1025, "unused2", "deprecated, DO NOT USE"), + DRMRPC(1026, "drmrpc", "group for drm rpc"), + NFC(1027, "nfc", "nfc subsystem"), + SDCARD_R(1028, "sdcard_r", "external storage read access"), + CLAT(1029, "clat", "clat part of nat464"), + LOOP_RADIO(1030, "loop_radio", "loop radio devices"), + MEDIA_DRM(1031, "media_drm", "MediaDrm plugins"), + PACKAGE_INFO(1032, "package_info", "access to installed package details"), + SDCARD_PICS(1033, "sdcard_pics", "external storage photos access"), + SDCARD_AV(1034, "sdcard_av", "external storage audio/video access"), + SDCARD_ALL(1035, "sdcard_all", "access all users external storage"), + LOGD(1036, "logd", "log daemon"), + SHARED_RELRO(1037, "shared_relro", "creator of shared GNU RELRO files"), + DBUS(1038, "dbus", "dbus-daemon IPC broker process"), + TLSDATE(1039, "tlsdate", "tlsdate unprivileged user"), + MEDIA_EX(1040, "media_ex", "mediaextractor process"), + AUDIOSERVER(1041, "audioserver", "audioserver process"), + METRICS_COLL(1042, "metrics_coll", "metrics_collector process"), + METRICSD(1043, "metricsd", "metricsd process"), + WEBSERV(1044, "webserv", "webservd process"), + DEBUGGERD(1045, "debuggerd", "debuggerd unprivileged user"), + MEDIA_CODEC(1046, "media_codec", "media_codec process"), + CAMERASERVER(1047, "cameraserver", "cameraserver process"), + FIREWALL(1048, "firewall", "firewall process"), + TRUNKS(1049, "trunks", "trunksd process"), + NVRAM(1050, "nvram", "nvram daemon"), + DNS_TETHER(1051, "dns_tether", "dns_tether device"), + DNS_TETHER_RESERVED(1052, "dns_tether_reserved", "Reserved range for dns_tether"), + WEBVIEW_ZYGOTE(1053, "webview_zygote", "zygote process"), + WEBVIEW_USER(1054, "webview_user", "webview chromium user"), + ETHERNET(1055, "ethernet", "Ethernet"), + TOMBSTONED(1056, "tombstoned", "tombstoned process"), + GRAPHICS_RW(1057, "graphics_rw", "graphics devices"), + + SHELL(2000, "shell", "adb and debug shell user"), + CACHE(2001, "cache", "cache access"), + DIAG(2002, "diag", "diagnostics"), + NET_BT_ADMIN(3001, "net_bt_admin", "bluetooth: create any socket"), + NET_BT(3002, "net_bt", "bluetooth: create sco, rfcomm or l2cap sockets"), + INET(3003, "inet", "can create AF_INET and AF_INET6 sockets"), + NET_RAW(3004, "net_raw", "can create raw INET sockets"), + NET_ADMIN(3005, "net_admin", "can configure interfaces and routing tables."), + NET_BW_STATS(3006, "net_bw_stats", "read bandwidth statistics"), + NET_BW_ACCT(3007, "net_bw_acct", "change bandwidth statistics accounting"), + NET_BT_STACK(3008, "net_bt_stack", "access to various bluetooth management functions"), + QCOM_DIAG(3009, "qcom_diag", "allow msm specific diag commands"), + EVERYBODY(9997, "everybody", "Shared external storage read/write"), + MISC(9998, "misc", "Access to misc storage"), + NOBODY(9999, "nobody", "Reserved"), + APP(10000, "app", "Access to app data"), +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt index b01263ee..0f99ae1d 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt @@ -1,15 +1,27 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package me.weishu.kernelsu.ui.component.profile +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material3.AssistChip import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,8 +33,15 @@ 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 androidx.compose.ui.unit.dp +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.list.ListDialog +import com.maxkeppeler.sheets.list.models.ListOption +import com.maxkeppeler.sheets.list.models.ListSelection import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R +import me.weishu.kernelsu.profile.Capabilities +import me.weishu.kernelsu.profile.Groups @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,7 +72,9 @@ fun RootProfileConfig( onExpandedChange = { expanded = !expanded } ) { OutlinedTextField( - modifier = Modifier.menuAnchor(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), readOnly = true, label = { Text(stringResource(R.string.profile_namespace)) }, value = currentNamespace, @@ -94,6 +115,7 @@ fun RootProfileConfig( ListItem(headlineContent = { OutlinedTextField( + modifier = Modifier.fillMaxWidth(), label = { Text("uid") }, value = profile.uid.toString(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -104,7 +126,12 @@ fun RootProfileConfig( }.let { filtered -> filtered.ifEmpty { "0" } }.let { value -> - onProfileChange(profile.copy(uid = value.toInt(), rootUseDefault = false)) + onProfileChange( + profile.copy( + uid = value.toInt(), + rootUseDefault = false + ) + ) } } } @@ -114,6 +141,7 @@ fun RootProfileConfig( ListItem(headlineContent = { OutlinedTextField( + modifier = Modifier.fillMaxWidth(), label = { Text("gid") }, value = profile.gid.toString(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -124,42 +152,173 @@ fun RootProfileConfig( }.let { filtered -> filtered.ifEmpty { "0" } }.let { value -> - onProfileChange(profile.copy(gid = value.toInt(), rootUseDefault = false)) + onProfileChange( + profile.copy( + gid = value.toInt(), + rootUseDefault = false + ) + ) } } } ) }) - ListItem(headlineContent = { - OutlinedTextField( - label = { Text("groups") }, - value = profile.groups.joinToString(","), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - 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)) - } - } - } + val selectedGroups = profile.groups.mapNotNull { id -> + Groups.values().find { it.gid == id } + } + GroupsPanel(selectedGroups) { + onProfileChange( + profile.copy( + groups = it.map { group -> group.gid }.ifEmpty { listOf(0) }, + rootUseDefault = false + ) ) - }) + } + + val selectedCaps = profile.capabilities.mapNotNull { id -> + Capabilities.values().find { it.cap == id } + } + CapsPanel(selectedCaps) { + onProfileChange( + profile.copy( + capabilities = it.map { cap -> cap.cap }, + rootUseDefault = false + ) + ) + } ListItem(headlineContent = { OutlinedTextField( - label = { Text("context") }, + modifier = Modifier.fillMaxWidth(), + label = { Text("SELinux context") }, value = profile.context, onValueChange = { onProfileChange(profile.copy(context = it, rootUseDefault = false)) } ) }) + + + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun GroupsPanel(selected: List, closeSelection: (selection: List) -> Unit) { + + var showDialog by remember { mutableStateOf(false) } + + if (showDialog) { + val groups = Groups.values() + val options = groups.map { value -> + ListOption( + titleText = value.display, + selected = selected.contains(value), + ) + } + + val selection = mutableListOf() + ListDialog( + state = rememberUseCaseState(visible = true, onFinishedRequest = { + Log.i("mylog", "onFinishedRequest") + closeSelection(selection) + }, onCloseRequest = { + showDialog = false + Log.i("mylog", "onCloseRequest") + }), + selection = ListSelection.Multiple( + showCheckBoxes = true, + options = options + ) { indecies, _ -> + // Handle selection + indecies.forEach { index -> + val group = groups[index] + selection.add(group) + } + } + ) + } + + OutlinedCard(modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .clickable { + showDialog = true + }) { + + Column(modifier = Modifier.padding(16.dp)) { + Text("groups") + FlowRow { + selected.forEach { group -> + AssistChip( + modifier = Modifier.padding(3.dp), + onClick = { /*TODO*/ }, + label = { Text(group.display) }) + } + } + } + + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun CapsPanel( + selected: List, + closeSelection: (selection: List) -> Unit +) { + + var showDialog by remember { mutableStateOf(false) } + + if (showDialog) { + val caps = Capabilities.values() + val options = caps.map { value -> + ListOption( + titleText = value.display, + selected = selected.contains(value), + ) + } + + val selection = mutableListOf() + ListDialog( + state = rememberUseCaseState(visible = true, onFinishedRequest = { + closeSelection(selection) + }, onCloseRequest = { + showDialog = false + }), + selection = ListSelection.Multiple( + showCheckBoxes = true, + options = options + ) { indecies, _ -> + // Handle selection + indecies.forEach { index -> + val group = caps[index] + selection.add(group) + } + } + ) + } + + OutlinedCard(modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .clickable { + showDialog = true + }) { + + Column(modifier = Modifier.padding(16.dp)) { + Text("Capabilities") + FlowRow { + selected.forEach { group -> + AssistChip( + modifier = Modifier.padding(3.dp), + onClick = { /*TODO*/ }, + label = { Text(group.display) }) + } + } + } + } } diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index f019f340..c8e6e4e2 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -8,6 +8,7 @@ accompanist = "0.30.0" navigation = "2.5.3" compose-destination = "1.9.42-beta" libsu = "5.0.5" +sheets-compose-dialogs = "1.1.1" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -51,3 +52,6 @@ me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconl compose-destinations-animations-core = { group = "io.github.raamcosta.compose-destinations", name = "animations-core", version.ref = "compose-destination" } compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "compose-destination" } + +sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs"} +sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs"}