User operation shortcuts

Update dependencies
This commit is contained in:
BinTianqi
2025-10-20 17:35:58 +08:00
parent fde191adc5
commit a57b3b3a8e
12 changed files with 232 additions and 100 deletions

View File

@@ -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)

View File

@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.shared,rikka.shizuku.aidl"/>
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.shared,rikka.shizuku.aidl,com.rosan.dhizuku.server_api,com.rosan.dhizuku.api"/>
<uses-feature android:name="android.software.device_admin"/>
<application
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -601,7 +601,8 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
UsersOptionsScreen(vm::getLogoutEnabled, vm::setLogoutEnabled, ::navigateUp)
}
composable<UserOperation> {
UserOperationScreen(vm::startUser, vm::switchUser, vm::stopUser, vm::deleteUser, ::navigateUp)
UserOperationScreen(vm::getUserIdentifiers, vm::doUserOperation,
vm::createUserOperationShortcut, ::navigateUp)
}
composable<CreateUser> { CreateUserScreen(vm::createUser, ::navigateUp) }
composable<ChangeUsername> { ChangeUsernameScreen(vm::setProfileName, ::navigateUp) }

View File

@@ -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<UserIdentifier> {
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<Long> {
return DPM.getSecondaryUsers(DAR).map { UM.getSerialNumberForUser(it) }
}
@RequiresApi(28)
fun getUserSessionMessages(): Pair<String, String> {
return (DPM.getStartUserSessionMessage(DAR)?.toString() ?: "") to
(DPM.getEndUserSessionMessage(DAR)?.toString() ?: "")

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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<UserIdentifier>, 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<UserIdentifier>() }
@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))

View File

@@ -469,6 +469,9 @@
<string name="secondary_users">次要用户</string>
<string name="no_secondary_users">无次要用户</string>
<string name="user_operation">用户操作</string>
<string name="start_user_n">启动用户 %1$d</string>
<string name="switch_to_user_n">切换到用户 %1$d</string>
<string name="stop_user_n">停止用户 %1$d</string>
<string name="user_not_exist">用户不存在</string>
<string name="serial_number">序列号</string>
<string name="logout">登出</string>

View File

@@ -505,6 +505,9 @@
<string name="secondary_users">Secondary users</string>
<string name="no_secondary_users">No secondary users</string>
<string name="user_operation">User operation</string>
<string name="start_user_n">Start user %1$d</string>
<string name="switch_to_user_n">Switch to user %1$d</string>
<string name="stop_user_n">Stop user %1$d</string>
<string name="user_not_exist">User does not exist</string>
<string name="serial_number">Serial number</string>
<string name="logout">Logout</string>

View File

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