mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
User operation shortcuts
Update dependencies
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
fun switchUser(id: Int, isUserId: Boolean): Boolean {
|
||||
val uh = getUserHandle(id, isUserId)
|
||||
if (uh == null) return false
|
||||
DPM.switchUser(DAR, uh)
|
||||
return true
|
||||
} ?: emptyList()
|
||||
}
|
||||
@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 doUserOperation(type: UserOperationType, id: Int, isUserId: Boolean): Boolean {
|
||||
return doUserOperationWithContext(application, type, id, isUserId)
|
||||
}
|
||||
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() ?: "")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
ExposedDropdownMenuBox(menu, { menu = it }) {
|
||||
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))
|
||||
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)
|
||||
}
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
{
|
||||
focusMgr.clearFocus()
|
||||
if (switchUser(input.toInt(), useUserId)) context.popToast(R.string.user_not_exist)
|
||||
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))
|
||||
}
|
||||
if(VERSION.SDK_INT >= 28) {
|
||||
CreateShortcutIcon(UserOperationType.Switch)
|
||||
}
|
||||
if (VERSION.SDK_INT >= 28) Row {
|
||||
Button(
|
||||
onClick = {
|
||||
{
|
||||
focusMgr.clearFocus()
|
||||
context.popToast(stopUser(input.toInt(), useUserId))
|
||||
val result = doOperation(UserOperationType.Stop, input.toInt(), useUserId)
|
||||
context.showOperationResultToast(result)
|
||||
},
|
||||
enabled = legalInput,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user