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" }