diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f405d2d..88b5533 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,6 +91,7 @@ dependencies { implementation(libs.accompanist.permissions) implementation(libs.androidx.material3) implementation(libs.androidx.navigation.compose) + implementation(libs.material.icons.core) implementation(libs.shizuku.provider) implementation(libs.shizuku.api) implementation(libs.dhizuku.api) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c316c33..506c110 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ - + Unit) { UsersOptionsScreen(vm::getLogoutEnabled, vm::setLogoutEnabled, ::navigateUp) } composable { - UserOperationScreen(vm::startUser, vm::switchUser, vm::stopUser, vm::deleteUser, ::navigateUp) + UserOperationScreen(vm::getUserIdentifiers, vm::doUserOperation, + vm::createUserOperationShortcut, ::navigateUp) } composable { CreateUserScreen(vm::createUser, ::navigateUp) } composable { ChangeUsernameScreen(vm::setProfileName, ::navigateUp) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 4be2f61..20333e4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -87,12 +87,15 @@ import com.bintianqi.owndroid.dpm.SsidPolicy import com.bintianqi.owndroid.dpm.SsidPolicyType import com.bintianqi.owndroid.dpm.SystemOptionsStatus import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo +import com.bintianqi.owndroid.dpm.UserIdentifier import com.bintianqi.owndroid.dpm.UserInformation +import com.bintianqi.owndroid.dpm.UserOperationType import com.bintianqi.owndroid.dpm.WifiInfo import com.bintianqi.owndroid.dpm.WifiSecurity import com.bintianqi.owndroid.dpm.WifiStatus import com.bintianqi.owndroid.dpm.activateOrgProfileCommand import com.bintianqi.owndroid.dpm.delegatedScopesList +import com.bintianqi.owndroid.dpm.doUserOperationWithContext import com.bintianqi.owndroid.dpm.getPackageInstaller import com.bintianqi.owndroid.dpm.handlePrivilegeChange import com.bintianqi.owndroid.dpm.isValidPackageName @@ -1290,35 +1293,28 @@ class MyViewModel(application: Application): AndroidViewModel(application) { UM.getSerialNumberForUser(uh) ) } + @Suppress("PrivateApi") @RequiresApi(28) - fun startUser(id: Int, isUserId: Boolean): Int { - val uh = getUserHandle(id, isUserId) - if (uh == null) return R.string.user_not_exist - return getUserOperationResultText(DPM.startUserInBackground(DAR, uh)) + fun getUserIdentifiers(): List { + return DPM.getSecondaryUsers(DAR)?.mapNotNull { + try { + val field = UserHandle::class.java.getDeclaredField("mHandle") + field.isAccessible = true + UserIdentifier(field.get(it) as Int, UM.getSerialNumberForUser(it)) + } catch (e: Exception) { + e.printStackTrace() + null + } + } ?: emptyList() } - fun switchUser(id: Int, isUserId: Boolean): Boolean { - val uh = getUserHandle(id, isUserId) - if (uh == null) return false - DPM.switchUser(DAR, uh) - return true + fun doUserOperation(type: UserOperationType, id: Int, isUserId: Boolean): Boolean { + return doUserOperationWithContext(application, type, id, isUserId) } - @RequiresApi(28) - fun stopUser(id: Int, isUserId: Boolean): Int { - val uh = getUserHandle(id, isUserId) - if (uh == null) return R.string.user_not_exist - return getUserOperationResultText(DPM.stopUser(DAR, uh)) - } - fun deleteUser(id: Int, isUserId: Boolean): Boolean { - val uh = getUserHandle(id, isUserId) - if (uh == null) return false - return DPM.removeUser(DAR, uh) - } - fun getUserHandle(id: Int, isUserId: Boolean): UserHandle? { - return if (isUserId && VERSION.SDK_INT >= 24) { - UserHandle.getUserHandleForUid(id * 100000) - } else { - UM.getUserForSerialNumber(id.toLong()) - } + fun createUserOperationShortcut(type: UserOperationType, id: Int, isUserId: Boolean): Boolean { + val serial = if (isUserId && VERSION.SDK_INT >= 24) { + UM.getSerialNumberForUser(UserHandle.getUserHandleForUid(id * 100000)) + } else id + return ShortcutUtils.setUserOperationShortcut(application, type, serial.toInt()) } fun getUserOperationResultText(code: Int): Int { return when (code) { @@ -1369,10 +1365,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { DPM.setUserIcon(DAR, bitmap) } @RequiresApi(28) - fun getSecondaryUsers(): List { - return DPM.getSecondaryUsers(DAR).map { UM.getSerialNumberForUser(it) } - } - @RequiresApi(28) fun getUserSessionMessages(): Pair { return (DPM.getStartUserSessionMessage(DAR)?.toString() ?: "") to (DPM.getEndUserSessionMessage(DAR)?.toString() ?: "") diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 652f894..408c569 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -103,6 +103,10 @@ class Receiver : DeviceAdminReceiver() { override fun onUserRemoved(context: Context, intent: Intent, removedUser: UserHandle) { super.onUserRemoved(context, intent, removedUser) sendUserRelatedNotification(context, removedUser, NotificationType.UserRemoved) + val um = context.getSystemService(Context.USER_SERVICE) as UserManager + ShortcutUtils.deleteUserOperationShortcut( + context, um.getSerialNumberForUser(removedUser).toInt() + ) } override fun onBugreportShared(context: Context, intent: Intent, hash: String) { diff --git a/app/src/main/java/com/bintianqi/owndroid/ShortcutUtils.kt b/app/src/main/java/com/bintianqi/owndroid/ShortcutUtils.kt index efff2dd..0d4c2fa 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ShortcutUtils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ShortcutUtils.kt @@ -5,6 +5,7 @@ import android.content.Intent import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import com.bintianqi.owndroid.dpm.UserOperationType object ShortcutUtils { fun setAllShortcuts(context: Context, enabled: Boolean) { @@ -19,7 +20,7 @@ object ShortcutUtils { ) ShortcutManagerCompat.setDynamicShortcuts(context, list) } else { - ShortcutManagerCompat.removeAllDynamicShortcuts(context) + ShortcutManagerCompat.removeDynamicShortcuts(context, MyShortcut.entries.map { it.id }) } } fun setShortcut(context: Context, shortcut: MyShortcut, state: Boolean) { @@ -79,6 +80,46 @@ object ShortcutUtils { val shortcut = createUserRestrictionShortcut(context, id, state) ShortcutManagerCompat.updateShortcuts(context, listOf(shortcut)) } + fun buildUserOperationShortcut( + context: Context, type: UserOperationType, serial: Int + ): ShortcutInfoCompat { + setShortcutKey() + val icon = when (type) { + UserOperationType.Start, UserOperationType.Switch -> R.drawable.person_fill0 + UserOperationType.Stop -> R.drawable.person_off + else -> R.drawable.person_fill0 + } + val text = when (type) { + UserOperationType.Start -> R.string.start_user_n + UserOperationType.Switch -> R.string.switch_to_user_n + UserOperationType.Stop -> R.string.stop_user_n + else -> R.string.place_holder + } + return ShortcutInfoCompat.Builder(context, "USER_OPERATION-${type.name}-$serial") + .setIcon(IconCompat.createWithResource(context, icon)) + .setShortLabel(context.getString(text, serial)) + .setIntent( + Intent(context, ShortcutsReceiverActivity::class.java) + .setAction("com.bintianqi.owndroid.action.USER_OPERATION") + .putExtra("operation", type.name) + .putExtra("serial", serial) + .putExtra("key", SP.shortcutKey) + ) + .build() + } + fun setUserOperationShortcut(context: Context, type: UserOperationType, serial: Int): Boolean { + val shortcut = buildUserOperationShortcut(context, type, serial) + return ShortcutManagerCompat.requestPinShortcut(context, shortcut, null) + } + fun deleteUserOperationShortcut(context: Context, serial: Int) { + val shortcuts = ShortcutManagerCompat.getShortcuts( + context, ShortcutManagerCompat.FLAG_MATCH_PINNED + ) + val matchedShortcuts = shortcuts.filter { + it.id.startsWith("USER_OPERATION-") && it.id.endsWith("-$serial") + }.map { it.id } + ShortcutManagerCompat.removeLongLivedShortcuts(context, matchedShortcuts) + } fun setShortcutKey() { if (SP.shortcutKey.isNullOrEmpty()) { SP.shortcutKey = generateBase64Key(10) diff --git a/app/src/main/java/com/bintianqi/owndroid/ShortcutsReceiverActivity.kt b/app/src/main/java/com/bintianqi/owndroid/ShortcutsReceiverActivity.kt index 7c1c7cd..5d0c1f4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ShortcutsReceiverActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ShortcutsReceiverActivity.kt @@ -3,6 +3,8 @@ package com.bintianqi.owndroid import android.app.Activity import android.os.Bundle import android.util.Log +import com.bintianqi.owndroid.dpm.UserOperationType +import com.bintianqi.owndroid.dpm.doUserOperationWithContext class ShortcutsReceiverActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -11,7 +13,7 @@ class ShortcutsReceiverActivity : Activity() { val action = intent.action?.removePrefix("com.bintianqi.owndroid.action.") val key = SP.shortcutKey val requestKey = intent?.getStringExtra("key") - if (action != null && SP.shortcuts && key != null && requestKey == key) { + if (action != null && key != null && requestKey == key) { when (action) { "LOCK" -> Privilege.DPM.lockNow() "DISABLE_CAMERA" -> { @@ -35,12 +37,21 @@ class ShortcutsReceiverActivity : Activity() { } ShortcutUtils.updateUserRestrictionShortcut(this, id, !state, false) } + "USER_OPERATION" -> { + val typeName = intent.getStringExtra("operation") ?: return + val type = UserOperationType.valueOf(typeName) + val serial = intent.getIntExtra("serial", -1) + if (serial == -1) return + doUserOperationWithContext(this, type, serial, false) + } } Log.d(TAG, "Received intent: $action") showOperationResultToast(true) } else { showOperationResultToast(false) } + } catch(e: Exception) { + e.printStackTrace() } finally { finish() } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index fb64d9b..b0932b6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -12,12 +12,16 @@ import android.content.Intent import android.content.pm.IPackageInstaller import android.content.pm.PackageInstaller import android.os.Build.VERSION +import android.os.UserHandle +import android.os.UserManager import android.util.Log import androidx.annotation.RequiresApi import com.bintianqi.owndroid.MyApplication import com.bintianqi.owndroid.NotificationType import com.bintianqi.owndroid.NotificationUtils import com.bintianqi.owndroid.Privilege +import com.bintianqi.owndroid.Privilege.DAR +import com.bintianqi.owndroid.Privilege.DPM import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SP import com.bintianqi.owndroid.ShortcutUtils @@ -142,7 +146,7 @@ class NetworkLog( @RequiresApi(26) fun retrieveNetworkLogs(app: MyApplication, token: Long) { CoroutineScope(Dispatchers.IO).launch { - val logs = Privilege.DPM.retrieveNetworkLogs(Privilege.DAR, token)?.mapNotNull { + val logs = DPM.retrieveNetworkLogs(DAR, token)?.mapNotNull { when (it) { is DnsEvent -> NetworkLog( if (VERSION.SDK_INT >= 28) it.id else null, it.packageName, it.timestamp, "dns", @@ -486,7 +490,7 @@ fun transformSecurityEventData(tag: Int, payload: Any): SecurityEventData? { @RequiresApi(24) fun retrieveSecurityLogs(app: MyApplication) { CoroutineScope(Dispatchers.IO).launch { - val logs = Privilege.DPM.retrieveSecurityLogs(Privilege.DAR) + val logs = DPM.retrieveSecurityLogs(DAR) if (logs.isNullOrEmpty()) return@launch app.myRepo.writeSecurityLogs(logs) NotificationUtils.sendBasicNotification( @@ -500,7 +504,7 @@ fun setDefaultAffiliationID() { if (VERSION.SDK_INT < 26) return if(!SP.isDefaultAffiliationIdSet) { try { - Privilege.DPM.setAffiliationIds(Privilege.DAR, setOf("OwnDroid_default_affiliation_id")) + DPM.setAffiliationIds(DAR, setOf("OwnDroid_default_affiliation_id")) SP.isDefaultAffiliationIdSet = true Log.d("DPM", "Default affiliation id set") } catch (e: Exception) { @@ -559,3 +563,29 @@ fun handlePrivilegeChange(context: Context) { SP.apiKeyHash = "" } } + +fun doUserOperationWithContext( + context: Context, type: UserOperationType, id: Int, isUserId: Boolean +): Boolean { + val um = context.getSystemService(Context.USER_SERVICE) as UserManager + val handle = if (isUserId && VERSION.SDK_INT >= 24) { + UserHandle.getUserHandleForUid(id * 100000) + } else { + um.getUserForSerialNumber(id.toLong()) + } + if (handle == null) return false + return when (type) { + UserOperationType.Start -> { + if (VERSION.SDK_INT >= 28) + DPM.startUserInBackground(DAR, handle) == UserManager.USER_OPERATION_SUCCESS + else false + } + UserOperationType.Switch -> DPM.switchUser(DAR, handle) + UserOperationType.Stop -> { + if (VERSION.SDK_INT >= 28) + DPM.stopUser(DAR, handle) == UserManager.USER_OPERATION_SUCCESS + else false + } + UserOperationType.Delete -> DPM.removeUser(DAR, handle) + } +} diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 5c53b99..2177563 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -28,8 +28,14 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -40,6 +46,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -81,15 +88,14 @@ import kotlinx.serialization.Serializable fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() - /** 1: secondary users, 2: logout*/ + /** 1: logout */ var dialog by rememberSaveable { mutableIntStateOf(0) } MyScaffold(R.string.users, onNavigateUp, 0.dp) { if(VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated) { - FunctionItem(R.string.logout, icon = R.drawable.logout_fill0) { dialog = 2 } + FunctionItem(R.string.logout, icon = R.drawable.logout_fill0) { dialog = 1 } } FunctionItem(R.string.user_info, icon = R.drawable.person_fill0) { onNavigate(UserInfo) } if(VERSION.SDK_INT >= 28 && privilege.device) { - FunctionItem(R.string.secondary_users, icon = R.drawable.list_fill0) { dialog = 1 } FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(UsersOptions) } } if(privilege.device) { @@ -126,24 +132,6 @@ fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> } } if (VERSION.SDK_INT >= 28 && dialog == 1) AlertDialog( - title = { Text(stringResource(R.string.secondary_users)) }, - text = { - val list = vm.getSecondaryUsers() - val text = if (list.isEmpty()) { - stringResource(R.string.no_secondary_users) - } else { - "(" + stringResource(R.string.serial_number) + ")\n" + list.joinToString("\n") - } - Text(text) - }, - confirmButton = { - TextButton({ dialog = 0 }) { - Text(stringResource(R.string.confirm)) - } - }, - onDismissRequest = { dialog = 0 } - ) - if (VERSION.SDK_INT >= 28 && dialog == 2) AlertDialog( title = { Text(stringResource(R.string.logout)) }, text = { Text(stringResource(R.string.info_logout)) @@ -227,21 +215,43 @@ fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) { ) } +class UserIdentifier(val id: Int, val serial: Long) + +enum class UserOperationType { + Start, Switch, Stop, Delete +} + @Serializable object UserOperation +@OptIn(ExperimentalMaterial3Api::class) @Composable fun UserOperationScreen( - startUser: (Int, Boolean) -> Int, switchUser: (Int, Boolean) -> Boolean, - stopUser: (Int, Boolean) -> Int, deleteUser: (Int, Boolean) -> Boolean, onNavigateUp: () -> Unit + getUsers: () -> List, doOperation: (UserOperationType, Int, Boolean) -> Boolean, + createShortcut: (UserOperationType, Int, Boolean) -> Boolean, onNavigateUp: () -> Unit ) { + val context = LocalContext.current var input by rememberSaveable { mutableStateOf("") } val focusMgr = LocalFocusManager.current var useUserId by rememberSaveable { mutableStateOf(false) } var dialog by rememberSaveable { mutableStateOf(false) } + var menu by remember { mutableStateOf(false) } val legalInput = input.toIntOrNull() != null + val identifiers = remember { mutableStateListOf() } + @Composable + fun CreateShortcutIcon(type: UserOperationType) { + FilledTonalIconButton({ + if (!createShortcut(type, input.toInt(), useUserId)) + context.showOperationResultToast(false) + }, enabled = legalInput) { + Icon(painterResource(R.drawable.open_in_new), null) + } + } + LaunchedEffect(Unit) { + identifiers.addAll(getUsers()) + } MyScaffold(R.string.user_operation, onNavigateUp) { - if(VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + if (VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { SegmentedButton(!useUserId, { useUserId = false }, SegmentedButtonDefaults.itemShape(0, 2)) { Text(stringResource(R.string.serial_number)) } @@ -249,50 +259,84 @@ fun UserOperationScreen( Text(stringResource(R.string.user_id)) } } - OutlinedTextField( - value = input, - onValueChange = { input = it }, - label = { Text(stringResource(if(useUserId) R.string.user_id else R.string.serial_number)) }, - modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 8.dp), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }) - ) - if(VERSION.SDK_INT >= 28) { - Button( - onClick = { - focusMgr.clearFocus() - context.popToast(startUser(input.toInt(), useUserId)) + ExposedDropdownMenuBox(menu, { menu = it }) { + OutlinedTextField( + input, { input = it }, + Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryEditable) + .padding(top = 4.dp, bottom = 8.dp), + label = { + Text(stringResource(if(useUserId) R.string.user_id else R.string.serial_number)) }, - enabled = legalInput, - modifier = Modifier.fillMaxWidth() + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(menu) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done) + ) + ExposedDropdownMenu(menu, { menu = false }) { + if (identifiers.isEmpty()) { + DropdownMenuItem( + { Text(stringResource(R.string.no_secondary_users)) }, {} + ) + } else { + identifiers.forEach { + val text = (if (useUserId) it.id else it.serial).toString() + DropdownMenuItem( + { Text(text) }, + { + input = text + menu = false + } + ) + } + } + } + } + if (VERSION.SDK_INT >= 28) Row { + Button( + { + focusMgr.clearFocus() + val result = doOperation(UserOperationType.Start, input.toInt(), useUserId) + context.showOperationResultToast(result) + }, + Modifier.weight(1F), + legalInput ) { Icon(Icons.Default.PlayArrow, null, Modifier.padding(end = 4.dp)) Text(stringResource(R.string.start_in_background)) } + CreateShortcutIcon(UserOperationType.Start) } - Button( - onClick = { - focusMgr.clearFocus() - if (switchUser(input.toInt(), useUserId)) context.popToast(R.string.user_not_exist) - }, - enabled = legalInput, - modifier = Modifier.fillMaxWidth() - ) { - Icon(painterResource(R.drawable.sync_alt_fill0), null, Modifier.padding(end = 4.dp)) - Text(stringResource(R.string.user_operation_switch)) - } - if(VERSION.SDK_INT >= 28) { + Row { Button( - onClick = { + { focusMgr.clearFocus() - context.popToast(stopUser(input.toInt(), useUserId)) + val result = doOperation(UserOperationType.Switch, input.toInt(), useUserId) + context.showOperationResultToast(result) }, - enabled = legalInput, - modifier = Modifier.fillMaxWidth() + Modifier.weight(1F), + legalInput + ) { + Icon(painterResource(R.drawable.sync_alt_fill0), null, Modifier.padding(end = 4.dp)) + Text(stringResource(R.string.user_operation_switch)) + } + CreateShortcutIcon(UserOperationType.Switch) + } + if (VERSION.SDK_INT >= 28) Row { + Button( + { + focusMgr.clearFocus() + val result = doOperation(UserOperationType.Stop, input.toInt(), useUserId) + context.showOperationResultToast(result) + }, + Modifier.weight(1F), + legalInput ) { Icon(Icons.Default.Close, null, Modifier.padding(end = 4.dp)) Text(stringResource(R.string.stop)) } + CreateShortcutIcon(UserOperationType.Stop) } Button( onClick = { @@ -312,7 +356,8 @@ fun UserOperationScreen( }, confirmButton = { TextButton({ - context.showOperationResultToast(deleteUser(input.toInt(), useUserId)) + val result = doOperation(UserOperationType.Delete, input.toInt(), useUserId) + context.showOperationResultToast(result) dialog = false }) { Text(stringResource(R.string.confirm)) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a33c9b5..ad9d6cc 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -469,6 +469,9 @@ 次要用户 无次要用户 用户操作 + 启动用户 %1$d + 切换到用户 %1$d + 停止用户 %1$d 用户不存在 序列号 登出 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19b6ea4..452520e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -505,6 +505,9 @@ Secondary users No secondary users User operation + Start user %1$d + Switch to user %1$d + Stop user %1$d User does not exist Serial number Logout diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 267a1b5..1b8a2f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] -agp = "8.12.2" -kotlin = "2.2.10" +agp = "8.13.0" +kotlin = "2.2.20" -navigation-compose = "2.9.3" -composeBom = "2025.08.01" +navigation-compose = "2.9.5" +composeBom = "2025.10.00" accompanist-drawablepainter = "0.37.3" accompanist-permissions = "0.37.3" shizuku = "13.1.5" fragment = "1.8.9" -dhizuku = "2.5.3" -dhizuku-server = "0.0.5" +dhizuku = "2.5.4" +dhizuku-server = "0.0.6" hiddenApiBypass = "6.1" libsu = "6.0.0" serialization = "1.9.0" @@ -22,6 +22,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose" } androidx-material3 = { module = "androidx.compose.material3:material3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" } +material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist-permissions" } @@ -39,4 +40,4 @@ serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.10" } +serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.20" }