ViewModel refactoring: Permissions part

Add MyDbHelper and MyRepository, use database to store dhizuku clients,
fix #168
This commit is contained in:
BinTianqi
2025-09-23 20:41:22 +08:00
parent 26c956a2cf
commit 2c72912ea6
11 changed files with 588 additions and 480 deletions

View File

@@ -7,7 +7,6 @@ import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ui.AppInstaller import com.bintianqi.owndroid.ui.AppInstaller
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
@@ -15,11 +14,10 @@ class AppInstallerActivity:FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val myVm by viewModels<MyViewModel>()
val vm by viewModels<AppInstallerViewModel>() val vm by viewModels<AppInstallerViewModel>()
vm.initialize(intent) vm.initialize(intent)
val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)
setContent { setContent {
val theme by myVm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) { OwnDroidTheme(theme) {
val uiState by vm.uiState.collectAsState() val uiState by vm.uiState.collectAsState()
AppInstaller( AppInstaller(

View File

@@ -9,7 +9,6 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
@@ -24,7 +23,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.rosan.dhizuku.aidl.IDhizukuClient import com.rosan.dhizuku.aidl.IDhizukuClient
@@ -34,12 +32,9 @@ import com.rosan.dhizuku.server_api.DhizukuService
import com.rosan.dhizuku.shared.DhizukuVariables import com.rosan.dhizuku.shared.DhizukuVariables
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
private const val TAG = "DhizukuServer" private const val TAG = "DhizukuServer"
const val DHIZUKU_CLIENTS_FILE = "dhizuku_clients.json"
class MyDhizukuProvider(): DhizukuProvider() { class MyDhizukuProvider(): DhizukuProvider() {
override fun onCreateService(client: IDhizukuClient): DhizukuService? { override fun onCreateService(client: IDhizukuClient): DhizukuService? {
Log.d(TAG, "Creating MyDhizukuService") Log.d(TAG, "Creating MyDhizukuService")
@@ -56,8 +51,6 @@ class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuC
pm.getNameForUid(callingUid) ?: return false, pm.getNameForUid(callingUid) ?: return false,
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES
) )
val file = mContext.filesDir.resolve(DHIZUKU_CLIENTS_FILE)
val clients = Json.decodeFromString<List<DhizukuClientInfo>>(file.readText())
val signature = getPackageSignature(packageInfo) val signature = getPackageSignature(packageInfo)
val requiredPermission = when (func) { val requiredPermission = when (func) {
"remote_transact", "remote_process" -> func "remote_transact", "remote_process" -> func
@@ -65,9 +58,10 @@ class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuC
"get_delegated_scopes", "set_delegated_scopes" -> "delegated_scopes" "get_delegated_scopes", "set_delegated_scopes" -> "delegated_scopes"
else -> "other" else -> "other"
} }
val hasPermission = clients.find { val hasPermission = (mContext.applicationContext as MyApplication).myRepo
callingUid == it.uid && signature == it.signature && requiredPermission in it.permissions .checkDhizukuClientPermission(
} != null callingUid, signature, requiredPermission
)
Log.d(TAG, "UID $callingUid, PID $callingPid, required permission: $requiredPermission, has permission: $hasPermission") Log.d(TAG, "UID $callingUid, PID $callingPid, required permission: $requiredPermission, has permission: $hasPermission")
return hasPermission return hasPermission
} }
@@ -97,26 +91,19 @@ class DhizukuActivity : ComponentActivity() {
val icon = appInfo.loadIcon(packageManager) val icon = appInfo.loadIcon(packageManager)
val label = appInfo.loadLabel(packageManager).toString() val label = appInfo.loadLabel(packageManager).toString()
fun close(grantPermission: Boolean) { fun close(grantPermission: Boolean) {
val file = filesDir.resolve(DHIZUKU_CLIENTS_FILE)
val json = Json { ignoreUnknownKeys = true }
val clients = json.decodeFromString<MutableList<DhizukuClientInfo>>(file.readText())
val index = clients.indexOfFirst { it.uid == uid }
val clientInfo = DhizukuClientInfo( val clientInfo = DhizukuClientInfo(
uid, getPackageSignature(packageInfo), if (grantPermission) DhizukuPermissions else emptyList() uid, getPackageSignature(packageInfo), if (grantPermission) DhizukuPermissions else emptyList()
) )
if (index == -1) clients += clientInfo (application as MyApplication).myRepo.setDhizukuClient(clientInfo)
else clients[index] = clientInfo
file.writeText(Json.encodeToString(clients))
finish() finish()
listener.onRequestPermission( listener.onRequestPermission(
if (grantPermission) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED if (grantPermission) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED
) )
} }
val vm by viewModels<MyViewModel>()
enableEdgeToEdge() enableEdgeToEdge()
val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)
setContent { setContent {
var appLockDialog by remember { mutableStateOf(false) } var appLockDialog by remember { mutableStateOf(false) }
val theme by vm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) { OwnDroidTheme(theme) {
if (!appLockDialog) AlertDialog( if (!appLockDialog) AlertDialog(
icon = { icon = {

View File

@@ -293,23 +293,40 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
) { ) {
composable<Home> { HomeScreen(::navigate) } composable<Home> { HomeScreen(::navigate) }
composable<WorkModes> { composable<WorkModes> {
WorkModesScreen(it.toRoute(), ::navigateUp, { WorkModesScreen(vm, it.toRoute(), ::navigateUp, {
navController.navigate(Home) { navController.navigate(Home) {
popUpTo<WorkModes> { inclusive = true } popUpTo<WorkModes> { inclusive = true }
} }
}, {
navController.navigate(WorkModes(false)) {
popUpTo(Home) { inclusive = true }
}
}, ::navigate) }, ::navigate)
} }
composable<DhizukuServerSettings> { DhizukuServerSettingsScreen(::navigateUp) } composable<DhizukuServerSettings> {
DhizukuServerSettingsScreen(vm.dhizukuClients, vm::getDhizukuClients,
composable<DelegatedAdmins> { DelegatedAdminsScreen(::navigateUp, ::navigate) } vm::updateDhizukuClient, vm::getDhizukuServerEnabled, vm::setDhizukuServerEnabled,
composable<AddDelegatedAdmin>{ ::navigateUp)
AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(), ::navigateUp) }
composable<DelegatedAdmins> {
DelegatedAdminsScreen(vm.delegatedAdmins, vm::getDelegatedAdmins, ::navigateUp, ::navigate)
}
composable<AddDelegatedAdmin>{
AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(),
vm::setDelegatedAdmin, ::navigateUp)
}
composable<DeviceInfo> { DeviceInfoScreen(vm, ::navigateUp) }
composable<LockScreenInfo> {
LockScreenInfoScreen(vm::getLockScreenInfo, vm::setLockScreenInfo, ::navigateUp)
}
composable<SupportMessage> {
SupportMessageScreen(vm::getShortSupportMessage, vm::getLongSupportMessage,
vm::setShortSupportMessage, vm::setLongSupportMessage, ::navigateUp)
} }
composable<DeviceInfo> { DeviceInfoScreen(::navigateUp) }
composable<LockScreenInfo> { LockScreenInfoScreen(::navigateUp) }
composable<SupportMessage> { SupportMessageScreen(::navigateUp) }
composable<TransferOwnership> { composable<TransferOwnership> {
TransferOwnershipScreen(::navigateUp) { TransferOwnershipScreen(vm.deviceAdminReceivers, vm::getDeviceAdminReceivers,
vm::transferOwnership, ::navigateUp) {
navController.navigate(WorkModes(false)) { navController.navigate(WorkModes(false)) {
popUpTo(Home) { inclusive = true } popUpTo(Home) { inclusive = true }
} }

View File

@@ -5,12 +5,14 @@ import android.os.Build.VERSION
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
class MyApplication : Application() { class MyApplication : Application() {
lateinit var myRepo: MyRepository
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (VERSION.SDK_INT >= 28) HiddenApiBypass.setHiddenApiExemptions("") if (VERSION.SDK_INT >= 28) HiddenApiBypass.setHiddenApiExemptions("")
SP = SharedPrefs(applicationContext) SP = SharedPrefs(applicationContext)
val dbHelper = MyDbHelper(this)
myRepo = MyRepository(dbHelper)
Privilege.initialize(applicationContext) Privilege.initialize(applicationContext)
Privilege.updateStatus()
} }
} }

View File

@@ -0,0 +1,15 @@
package com.bintianqi.owndroid
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 1) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," +
"signature TEXT, permissions TEXT)")
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
}

View File

@@ -0,0 +1,46 @@
package com.bintianqi.owndroid
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
class MyRepository(val dbHelper: MyDbHelper) {
fun getDhizukuClients(): List<DhizukuClientInfo> {
val list = mutableListOf<DhizukuClientInfo>()
dbHelper.readableDatabase.rawQuery("SELECT * FROM dhizuku_clients", null).use { cursor ->
while (cursor.moveToNext()) {
list += DhizukuClientInfo(
cursor.getInt(0), cursor.getString(1),
cursor.getString(2).split(",").filter { it.isNotEmpty() }
)
}
}
return list
}
fun checkDhizukuClientPermission(uid: Int, signature: String?, permission: String): Boolean {
val cursor = if (signature == null) {
dbHelper.readableDatabase.rawQuery(
"SELECT permissions FROM dhizuku_clients WHERE uid = $uid AND signature IS NULL",
null
)
} else {
dbHelper.readableDatabase.rawQuery(
"SELECT permissions FROM dhizuku_clients WHERE uid = $uid AND signature = ?",
arrayOf(signature)
)
}
return cursor.use {
it.moveToNext() && permission in it.getString(0).split(",")
}
}
fun setDhizukuClient(info: DhizukuClientInfo) {
val cv = ContentValues()
cv.put("uid", info.uid)
cv.put("signature", info.signature)
cv.put("permissions", info.permissions.joinToString(","))
dbHelper.writableDatabase.insertWithOnConflict("dhizuku_clients", null, cv,
SQLiteDatabase.CONFLICT_REPLACE)
}
fun deleteDhizukuClient(info: DhizukuClientInfo) {
dbHelper.writableDatabase.delete("dhizuku_clients", "uid = ${info.uid}", null)
}
}

View File

@@ -3,6 +3,8 @@ package com.bintianqi.owndroid
import android.app.ActivityOptions import android.app.ActivityOptions
import android.app.Application import android.app.Application
import android.app.PendingIntent import android.app.PendingIntent
import android.app.admin.DeviceAdminInfo
import android.app.admin.DeviceAdminReceiver
import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager
import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback
import android.app.admin.FactoryResetProtectionPolicy import android.app.admin.FactoryResetProtectionPolicy
@@ -30,18 +32,25 @@ import androidx.lifecycle.application
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bintianqi.owndroid.Privilege.DAR import com.bintianqi.owndroid.Privilege.DAR
import com.bintianqi.owndroid.Privilege.DPM import com.bintianqi.owndroid.Privilege.DPM
import com.bintianqi.owndroid.dpm.ACTIVATE_DEVICE_OWNER_COMMAND
import com.bintianqi.owndroid.dpm.AppStatus import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.CaCertInfo import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.DelegatedAdmin
import com.bintianqi.owndroid.dpm.DeviceAdmin
import com.bintianqi.owndroid.dpm.FrpPolicyInfo import com.bintianqi.owndroid.dpm.FrpPolicyInfo
import com.bintianqi.owndroid.dpm.HardwareProperties import com.bintianqi.owndroid.dpm.HardwareProperties
import com.bintianqi.owndroid.dpm.PendingSystemUpdateInfo import com.bintianqi.owndroid.dpm.PendingSystemUpdateInfo
import com.bintianqi.owndroid.dpm.SystemOptionsStatus import com.bintianqi.owndroid.dpm.SystemOptionsStatus
import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo
import com.bintianqi.owndroid.dpm.delegatedScopesList
import com.bintianqi.owndroid.dpm.getPackageInstaller import com.bintianqi.owndroid.dpm.getPackageInstaller
import com.bintianqi.owndroid.dpm.isValidPackageName import com.bintianqi.owndroid.dpm.isValidPackageName
import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage
import com.bintianqi.owndroid.dpm.permissionList import com.bintianqi.owndroid.dpm.permissionList
import com.bintianqi.owndroid.dpm.temperatureTypes import com.bintianqi.owndroid.dpm.temperatureTypes
import com.rosan.dhizuku.api.Dhizuku
import com.rosan.dhizuku.api.DhizukuRequestPermissionListener
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -56,6 +65,7 @@ import java.security.cert.X509Certificate
import java.util.concurrent.Executors import java.util.concurrent.Executors
class MyViewModel(application: Application): AndroidViewModel(application) { class MyViewModel(application: Application): AndroidViewModel(application) {
val myRepo = getApplication<MyApplication>().myRepo
val PM = application.packageManager val PM = application.packageManager
val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)) val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme))
fun changeTheme(newTheme: ThemeSettings) { fun changeTheme(newTheme: ThemeSettings) {
@@ -787,6 +797,205 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
} }
DPM.installSystemUpdate(DAR, uri, application.mainExecutor, callback) DPM.installSystemUpdate(DAR, uri, application.mainExecutor, callback)
} }
@RequiresApi(24)
fun isCreatingWorkProfileAllowed(): Boolean {
return DPM.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
}
fun activateDoByShizuku(callback: (Boolean, String?) -> Unit) {
viewModelScope.launch {
useShizuku(application) { service ->
try {
val result = IUserService.Stub.asInterface(service)
.execute(ACTIVATE_DEVICE_OWNER_COMMAND)
if (result == null || result.getInt("code", -1) != 0) {
callback(false, null)
} else {
Privilege.updateStatus()
callback(
true, result.getString("output") + "\n" + result.getString("error")
)
}
} catch (e: Exception) {
e.printStackTrace()
callback(false, null)
}
}
}
}
fun activateDoByRoot(callback: (Boolean, String?) -> Unit) {
Shell.getShell { shell ->
if(shell.isRoot) {
val result = Shell.cmd(ACTIVATE_DEVICE_OWNER_COMMAND).exec()
val output = result.out.joinToString("\n") + "\n" + result.err.joinToString("\n")
Privilege.updateStatus()
callback(result.isSuccess, output)
} else {
callback(false, application.getString(R.string.permission_denied))
}
}
}
@RequiresApi(28)
fun activateDoByDhizuku(callback: (Boolean, String?) -> Unit) {
DPM.transferOwnership(DAR, MyAdminComponent, null)
SP.dhizuku = false
Privilege.initialize(application)
callback(true, null)
}
fun activateDhizukuMode(callback: (Boolean, String?) -> Unit) {
fun onSucceed() {
SP.dhizuku = true
Privilege.initialize(application)
callback(true, null)
}
if (Dhizuku.init(application)) {
if (Dhizuku.isPermissionGranted()) {
onSucceed()
} else {
Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() {
override fun onRequestPermission(grantResult: Int) {
if(grantResult == PackageManager.PERMISSION_GRANTED) onSucceed()
}
})
}
} else {
callback(false, application.getString(R.string.failed_to_init_dhizuku))
}
}
fun clearDeviceOwner() {
DPM.clearDeviceOwnerApp(application.packageName)
}
@RequiresApi(24)
fun clearProfileOwner() {
DPM.clearProfileOwner(MyAdminComponent)
}
fun deactivateDhizukuMode() {
SP.dhizuku = false
Privilege.initialize(application)
}
val dhizukuClients = MutableStateFlow(emptyList<Pair<DhizukuClientInfo, AppInfo>>())
fun getDhizukuClients() {
viewModelScope.launch {
dhizukuClients.value = myRepo.getDhizukuClients().mapNotNull {
val packageName = PM.getNameForUid(it.uid)
if (packageName == null) {
myRepo.deleteDhizukuClient(it)
null
} else {
it to getAppInfo(packageName)
}
}
}
}
fun getDhizukuServerEnabled(): Boolean {
return SP.dhizukuServer
}
fun setDhizukuServerEnabled(status: Boolean) {
SP.dhizukuServer = status
}
fun updateDhizukuClient(info: DhizukuClientInfo) {
myRepo.setDhizukuClient(info)
dhizukuClients.update { list ->
val ml = list.toMutableList()
val index = ml.indexOfFirst { it.first.uid == info.uid }
ml[index] = info to ml[index].second
ml
}
}
@RequiresApi(24)
fun getLockScreenInfo(): String {
return DPM.deviceOwnerLockScreenInfo?.toString() ?: ""
}
@RequiresApi(24)
fun setLockScreenInfo(text: String) {
DPM.setDeviceOwnerLockScreenInfo(DAR, text)
}
val delegatedAdmins = MutableStateFlow(emptyList<DelegatedAdmin>())
@RequiresApi(26)
fun getDelegatedAdmins() {
val list = mutableListOf<DelegatedAdmin>()
delegatedScopesList.forEach { scope ->
DPM.getDelegatePackages(DAR, scope.id)?.forEach { pkg ->
val index = list.indexOfFirst { it.app.name == pkg }
if (index == -1) {
list += DelegatedAdmin(getAppInfo(pkg), listOf(scope.id))
} else {
list[index] = DelegatedAdmin(list[index].app, list[index].scopes + scope.id)
}
}
}
delegatedAdmins.value = list
}
@RequiresApi(26)
fun setDelegatedAdmin(name: String, scopes: List<String>) {
DPM.setDelegatedScopes(DAR, name, scopes)
getDelegatedAdmins()
}
@RequiresApi(34)
fun getDeviceFinanced(): Boolean {
return DPM.isDeviceFinanced
}
@RequiresApi(33)
fun getDpmRh(): String? {
return DPM.devicePolicyManagementRoleHolderPackage
}
fun getStorageEncryptionStatus(): Int {
return DPM.storageEncryptionStatus
}
@RequiresApi(28)
fun getDeviceIdAttestationSupported(): Boolean {
return DPM.isDeviceIdAttestationSupported
}
@RequiresApi(30)
fun getUniqueDeviceAttestationSupported(): Boolean {
return DPM.isUniqueDeviceAttestationSupported
}
fun getActiveAdmins(): String {
return DPM.activeAdmins?.joinToString("\n") {
it.flattenToShortString()
} ?: application.getString(R.string.none)
}
@RequiresApi(24)
fun getShortSupportMessage(): String {
return DPM.getShortSupportMessage(DAR)?.toString() ?: ""
}
@RequiresApi(24)
fun getLongSupportMessage(): String {
return DPM.getLongSupportMessage(DAR)?.toString() ?: ""
}
@RequiresApi(24)
fun setShortSupportMessage(text: String?) {
DPM.setShortSupportMessage(DAR, text)
}
@RequiresApi(24)
fun setLongSupportMessage(text: String?) {
DPM.setLongSupportMessage(DAR, text)
}
val deviceAdminReceivers = MutableStateFlow(emptyList<DeviceAdmin>())
fun getDeviceAdminReceivers() {
viewModelScope.launch {
deviceAdminReceivers.value = PM.queryBroadcastReceivers(
Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED),
PackageManager.GET_META_DATA
).mapNotNull {
try {
DeviceAdminInfo(application, it)
} catch(_: Exception) {
null
}
}.filter {
it.isVisible && it.packageName != "com.bintianqi.owndroid" &&
it.activityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0
}.map {
DeviceAdmin(getAppInfo(it.packageName), it.component)
}
}
}
@RequiresApi(28)
fun transferOwnership(component: ComponentName) {
DPM.transferOwnership(DAR, component, null)
Privilege.updateStatus()
}
} }
data class ThemeSettings( data class ThemeSettings(

View File

@@ -31,6 +31,7 @@ object Privilege {
} }
DPM = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager DPM = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
DAR = MyAdminComponent DAR = MyAdminComponent
updateStatus()
} }
lateinit var DPM: DevicePolicyManager lateinit var DPM: DevicePolicyManager
private set private set

View File

@@ -2,13 +2,8 @@ package com.bintianqi.owndroid.dpm
import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.PersistableBundle
import androidx.annotation.Keep
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@@ -24,9 +19,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -57,6 +54,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -67,10 +65,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -85,15 +82,13 @@ import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.DHIZUKU_CLIENTS_FILE import com.bintianqi.owndroid.AppInfo
import com.bintianqi.owndroid.DhizukuClientInfo import com.bintianqi.owndroid.DhizukuClientInfo
import com.bintianqi.owndroid.DhizukuPermissions import com.bintianqi.owndroid.DhizukuPermissions
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.MyAdminComponent
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.Settings import com.bintianqi.owndroid.Settings
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog import com.bintianqi.owndroid.ui.CircularProgressDialog
@@ -104,36 +99,32 @@ import com.bintianqi.owndroid.ui.MySmallTitleScaffold
import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.useShizuku
import com.bintianqi.owndroid.yesOrNo import com.bintianqi.owndroid.yesOrNo
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.rosan.dhizuku.api.Dhizuku
import com.rosan.dhizuku.api.DhizukuRequestPermissionListener
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable data class WorkModes(val canNavigateUp: Boolean) @Serializable data class WorkModes(val canNavigateUp: Boolean)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun WorkModesScreen( fun WorkModesScreen(
params: WorkModes, onNavigateUp: () -> Unit, onActivate: () -> Unit, onNavigate: (Any) -> Unit vm: MyViewModel, params: WorkModes, onNavigateUp: () -> Unit, onActivate: () -> Unit,
onDeactivate: () -> Unit, onNavigate: (Any) -> Unit
) { ) {
val context = LocalContext.current
val coroutine = rememberCoroutineScope()
val privilege by Privilege.status.collectAsStateWithLifecycle() val privilege by Privilege.status.collectAsStateWithLifecycle()
/** 0: none, 1: device owner, 2: circular progress indicator, 3: result, 4: deactivate, 5: command */ /** 0: none, 1: device owner, 2: circular progress indicator, 3: result, 4: deactivate, 5: command */
var dialog by remember { mutableIntStateOf(0) } var dialog by remember { mutableIntStateOf(0) }
var operationSucceed by remember { mutableStateOf(false) } var operationSucceed by remember { mutableStateOf(false) }
var resultText by remember { mutableStateOf("") }
LaunchedEffect(privilege) { LaunchedEffect(privilege) {
if (!params.canNavigateUp && privilege.device) { if (!params.canNavigateUp && privilege.device) {
delay(1000) delay(1000)
if (dialog != 3) { // Activated by ADB command if (dialog != 3) { // Activated by ADB command
operationSucceed = true operationSucceed = true
resultText = ""
dialog = 3 dialog = 3
} }
} }
@@ -193,90 +184,36 @@ fun WorkModesScreen(
}, },
contentWindowInsets = WindowInsets.ime contentWindowInsets = WindowInsets.ime
) { paddingValues -> ) { paddingValues ->
var navigateUpOnSucceed by remember { mutableStateOf(true) } fun handleResult(succeeded: Boolean, output: String?) {
var resultText by remember { mutableStateOf("") } operationSucceed = succeeded
fun handleResult(succeeded: Boolean, activateSucceeded: Boolean, output: String?) {
if(succeeded) {
operationSucceed = activateSucceeded
resultText = output ?: "" resultText = output ?: ""
dialog = 3 dialog = 3
Privilege.updateStatus() }
} else { Column(Modifier.fillMaxSize().padding(paddingValues)) {
dialog = 0 if (!privilege.profile) {
context.showOperationResultToast(false) WorkingModeItem(R.string.device_owner, privilege.device) {
if (!privilege.device || (VERSION.SDK_INT >= 28 && privilege.dhizuku)) {
dialog = 1
} }
} }
Column(Modifier
.fillMaxSize()
.padding(paddingValues)) {
if(!privilege.profile && (VERSION.SDK_INT >= 28 || !privilege.dhizuku)) Row(
Modifier
.fillMaxWidth()
.clickable(!privilege.device || privilege.dhizuku) { dialog = 1 }
.background(if (privilege.device) colorScheme.primaryContainer else Color.Transparent)
.padding(HorizontalPadding, 10.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Column {
Text(stringResource(R.string.device_owner), style = typography.titleLarge)
if(!privilege.device || privilege.dhizuku) Text(
stringResource(R.string.recommended), color = colorScheme.primary, style = typography.labelLarge
)
} }
Icon( if (privilege.profile) WorkingModeItem(R.string.profile_owner, true) { }
if(privilege.device) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null, if (privilege.dhizuku || !privilege.activated) {
tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground WorkingModeItem(R.string.dhizuku, privilege.dhizuku) {
) if (!privilege.dhizuku) {
}
if(privilege.profile) Row(
Modifier
.fillMaxWidth()
.background(colorScheme.primaryContainer)
.padding(HorizontalPadding, 10.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Column {
Text(stringResource(R.string.profile_owner), style = typography.titleLarge)
}
Icon(Icons.Default.Check, null, tint = colorScheme.primary)
}
if(privilege.dhizuku || !(privilege.device || privilege.profile)) Row(
Modifier
.fillMaxWidth()
.clickable(!privilege.dhizuku) {
dialog = 2 dialog = 2
activateDhizukuMode(context, ::handleResult) vm.activateDhizukuMode(::handleResult)
}
} }
.background(if (privilege.dhizuku) colorScheme.primaryContainer else Color.Transparent)
.padding(HorizontalPadding, 10.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Text(stringResource(R.string.dhizuku), style = typography.titleLarge)
Icon(
if(privilege.dhizuku) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
tint = if(privilege.dhizuku) colorScheme.primary else colorScheme.onBackground
)
} }
if( if(
privilege.work || (VERSION.SDK_INT < 24 || privilege.work || (VERSION.SDK_INT < 24 || vm.isCreatingWorkProfileAllowed())
Privilege.DPM.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE))
) Row(
Modifier
.fillMaxWidth()
.clickable(!privilege.work) { onNavigate(CreateWorkProfile) }
.background(if (privilege.device) colorScheme.primaryContainer else Color.Transparent)
.padding(HorizontalPadding, 10.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) { ) {
Column { WorkingModeItem(R.string.work_profile, privilege.work) {
Text(stringResource(R.string.work_profile), style = typography.titleLarge) if (!privilege.work) onNavigate(CreateWorkProfile)
} }
Icon(
if(privilege.work) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground
)
} }
if ((privilege.device || privilege.profile) && !privilege.dhizuku) Row( if (privilege.activated && !privilege.dhizuku) Row(
Modifier Modifier
.padding(top = 20.dp) .padding(top = 20.dp)
.fillMaxWidth() .fillMaxWidth()
@@ -302,27 +239,29 @@ fun WorkModesScreen(
title = { Text(stringResource(R.string.activate_method)) }, title = { Text(stringResource(R.string.activate_method)) },
text = { text = {
FlowRow(Modifier.fillMaxWidth()) { FlowRow(Modifier.fillMaxWidth()) {
if(!privilege.dhizuku) Button({ if (!privilege.dhizuku) {
dialog = 2 Button({ dialog = 5 }, Modifier.padding(end = 8.dp)) {
coroutine.launch { Text(stringResource(R.string.adb_command))
activateUsingShizuku(context, ::handleResult)
} }
Button({
dialog = 2
vm.activateDoByShizuku(::handleResult)
}, Modifier.padding(end = 8.dp)) { }, Modifier.padding(end = 8.dp)) {
Text(stringResource(R.string.shizuku)) Text(stringResource(R.string.shizuku))
} }
if(!privilege.dhizuku) Button({ Button({
dialog = 2 dialog = 2
activateUsingRoot(context, ::handleResult) vm.activateDoByRoot(::handleResult)
}, Modifier.padding(end = 8.dp)) { }, Modifier.padding(end = 8.dp)) {
Text("Root") Text("Root")
} }
if(VERSION.SDK_INT >= 28) Button({ }
if (VERSION.SDK_INT >= 28 && privilege.dhizuku) Button({
dialog = 2 dialog = 2
activateUsingDhizuku(context, ::handleResult) vm.activateDoByDhizuku(::handleResult)
}, Modifier.padding(end = 8.dp)) { }, Modifier.padding(end = 8.dp)) {
Text(stringResource(R.string.dhizuku)) Text(stringResource(R.string.dhizuku))
} }
if (!privilege.dhizuku) Button({ dialog = 5 }) { Text(stringResource(R.string.adb_command)) }
} }
}, },
confirmButton = { confirmButton = {
@@ -334,16 +273,14 @@ fun WorkModesScreen(
if(dialog == 3) AlertDialog( if(dialog == 3) AlertDialog(
title = { Text(stringResource(if(operationSucceed) R.string.succeeded else R.string.failed)) }, title = { Text(stringResource(if(operationSucceed) R.string.succeeded else R.string.failed)) },
text = { text = {
Column(Modifier Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
.fillMaxWidth()
.verticalScroll(rememberScrollState())) {
Text(resultText) Text(resultText)
} }
}, },
confirmButton = { confirmButton = {
TextButton({ TextButton({
dialog = 0 dialog = 0
if(navigateUpOnSucceed && operationSucceed && !params.canNavigateUp) onActivate() if (operationSucceed && !params.canNavigateUp) onActivate()
}) { }) {
Text(stringResource(R.string.confirm)) Text(stringResource(R.string.confirm))
} }
@@ -365,18 +302,17 @@ fun WorkModesScreen(
TextButton( TextButton(
{ {
if(privilege.dhizuku) { if(privilege.dhizuku) {
SP.dhizuku = false vm.deactivateDhizukuMode()
Privilege.initialize(context)
Privilege.updateStatus()
} else { } else {
if(privilege.device) { if(privilege.device) {
Privilege.DPM.clearDeviceOwnerApp(context.packageName) vm.clearDeviceOwner()
} else if(VERSION.SDK_INT >= 24) { } else if(VERSION.SDK_INT >= 24) {
Privilege.DPM.clearProfileOwner(MyAdminComponent) vm.clearProfileOwner()
} }
// Status updated in Receiver.onDisabled() // Status updated in Receiver.onDisabled()
} }
dialog = 0 dialog = 0
onDeactivate()
}, },
enabled = time == 0, enabled = time == 0,
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
@@ -403,95 +339,22 @@ fun WorkModesScreen(
} }
} }
fun activateUsingShizuku(context: Context, callback: (Boolean, Boolean, String?) -> Unit) { @Composable
useShizuku(context) { service -> fun WorkingModeItem(text: Int, active: Boolean, onClick: () -> Unit) {
try { Row(
val result = IUserService.Stub.asInterface(service).execute(ACTIVATE_DEVICE_OWNER_COMMAND) Modifier
if (result == null) { .fillMaxWidth()
callback(false, false, null) .clickable(onClick = onClick)
} else { .background(if (active) colorScheme.primaryContainer else Color.Transparent)
callback( .padding(HorizontalPadding, 10.dp),
true, result.getInt("code", -1) == 0, Arrangement.SpaceBetween, Alignment.CenterVertically
result.getString("output") + "\n" + result.getString("error") ) {
Text(stringResource(text), style = typography.titleLarge)
Icon(
if(active) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
tint = if(active) colorScheme.primary else colorScheme.onBackground
) )
} }
} catch (e: Exception) {
callback(false, false, null)
e.printStackTrace()
}
}
}
fun activateUsingRoot(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
Shell.getShell { shell ->
if(shell.isRoot) {
val result = Shell.cmd(ACTIVATE_DEVICE_OWNER_COMMAND).exec()
val output = result.out.joinToString("\n") + "\n" + result.err.joinToString("\n")
callback(true, result.isSuccess, output)
} else {
callback(true, false, context.getString(R.string.permission_denied))
}
}
}
@RequiresApi(28)
fun activateUsingDhizuku(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
fun doTransfer() {
try {
if (SP.dhizuku) {
Privilege.DPM.transferOwnership(Privilege.DAR, MyAdminComponent, PersistableBundle())
SP.dhizuku = false
Privilege.initialize(context)
} else {
val dpm = binderWrapperDevicePolicyManager(context)
if (dpm == null) {
callback(false, false, null)
return
} else {
dpm.transferOwnership(Dhizuku.getOwnerComponent(), MyAdminComponent, PersistableBundle())
}
}
callback(true, true, null)
} catch (e: Exception) {
e.printStackTrace()
callback(false, false, null)
}
}
if(Dhizuku.init(context)) {
if(Dhizuku.isPermissionGranted()) {
doTransfer()
} else {
Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() {
override fun onRequestPermission(grantResult: Int) {
if(grantResult == PackageManager.PERMISSION_GRANTED) doTransfer()
else callback(false, false, null)
}
})
}
} else {
callback(true, false, context.getString(R.string.failed_to_init_dhizuku))
}
}
fun activateDhizukuMode(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
fun onSucceed() {
SP.dhizuku = true
Privilege.initialize(context)
callback(true, true, null)
}
if(Dhizuku.init(context)) {
if(Dhizuku.isPermissionGranted()) {
onSucceed()
} else {
Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() {
override fun onRequestPermission(grantResult: Int) {
if(grantResult == PackageManager.PERMISSION_GRANTED) onSucceed()
}
})
}
} else {
callback(true, false, context.getString(R.string.failed_to_init_dhizuku))
}
} }
const val ACTIVATE_DEVICE_OWNER_COMMAND = "dpm set-device-owner com.bintianqi.owndroid/.Receiver" const val ACTIVATE_DEVICE_OWNER_COMMAND = "dpm set-device-owner com.bintianqi.owndroid/.Receiver"
@@ -499,41 +362,23 @@ const val ACTIVATE_DEVICE_OWNER_COMMAND = "dpm set-device-owner com.bintianqi.ow
@Serializable object DhizukuServerSettings @Serializable object DhizukuServerSettings
@Composable @Composable
fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) { fun DhizukuServerSettingsScreen(
val context = LocalContext.current dhizukuClients: StateFlow<List<Pair<DhizukuClientInfo, AppInfo>>>,
val pm = context.packageManager getDhizukuClients: () -> Unit, updateDhizukuClient: (DhizukuClientInfo) -> Unit,
val file = context.filesDir.resolve(DHIZUKU_CLIENTS_FILE) getServerEnabled: () -> Boolean, setServerEnabled: (Boolean) -> Unit, onNavigateUp: () -> Unit
var enabled by remember { mutableStateOf(SP.dhizukuServer) } ) {
val clients = remember { mutableStateListOf<DhizukuClientInfo>() } var enabled by remember { mutableStateOf(getServerEnabled()) }
fun changeEnableState(status: Boolean) { val clients by dhizukuClients.collectAsStateWithLifecycle()
enabled = status LaunchedEffect(Unit) { getDhizukuClients() }
SP.dhizukuServer = status
}
fun writeList() {
file.writeText(Json.encodeToString(clients.toList()))
}
LaunchedEffect(Unit) {
if (!file.exists()) file.writeText("[]")
}
LaunchedEffect(enabled) {
if (enabled) {
clients.clear()
val json = Json { ignoreUnknownKeys = true }
clients.addAll(json.decodeFromString<List<DhizukuClientInfo>>(file.readText()))
}
}
MyLazyScaffold(R.string.dhizuku_server, onNavigateUp) { MyLazyScaffold(R.string.dhizuku_server, onNavigateUp) {
item { item {
SwitchItem(R.string.enable, getState = { SP.dhizukuServer }, onCheckedChange = ::changeEnableState) SwitchItem(R.string.enable, enabled, {
setServerEnabled(it)
enabled = it
})
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
} }
if (enabled) itemsIndexed(clients) { index, client -> if (enabled) items(clients) { (client, app) ->
val name = pm.getNameForUid(client.uid)
if (name == null) {
clients.dropWhile { it.uid == client.uid }
writeList()
} else {
val info = pm.getApplicationInfo(name, 0)
var expand by remember { mutableStateOf(false) } var expand by remember { mutableStateOf(false) }
Card( Card(
Modifier Modifier
@@ -548,14 +393,12 @@ fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) {
) { ) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
Image( Image(
rememberDrawablePainter(info.loadIcon(pm)), null, rememberDrawablePainter(app.icon), null,
Modifier Modifier.padding(end = 16.dp).size(45.dp)
.padding(end = 16.dp)
.size(45.dp)
) )
Column { Column {
Text(info.loadLabel(pm).toString(), style = typography.titleMedium) Text(app.label, style = typography.titleMedium)
Text(name, Modifier.alpha(0.7F), style = typography.bodyMedium) Text(app.name, Modifier.alpha(0.7F), style = typography.bodyMedium)
} }
} }
val ts = when (DhizukuPermissions.filter { it !in client.permissions }.size) { val ts = when (DhizukuPermissions.filter { it !in client.permissions }.size) {
@@ -565,9 +408,10 @@ fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) {
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
TriStateCheckbox(ts, { TriStateCheckbox(ts, {
clients[index] = when (ts) { if (ts == ToggleableState.Off) {
ToggleableState.On, ToggleableState.Indeterminate -> client.copy(permissions = emptyList()) updateDhizukuClient(client.copy(permissions = DhizukuPermissions))
ToggleableState.Off -> client.copy(permissions = DhizukuPermissions) } else {
updateDhizukuClient(client.copy(permissions = emptyList()))
} }
}) })
val degrees by animateFloatAsState(if(expand) 180F else 0F) val degrees by animateFloatAsState(if(expand) 180F else 0F)
@@ -581,14 +425,17 @@ fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) {
mapOf( mapOf(
"remote_transact" to "Remote transact", "remote_process" to "Remote process", "remote_transact" to "Remote transact", "remote_process" to "Remote process",
"user_service" to "User service", "delegated_scopes" to "Delegated scopes", "user_service" to "User service", "delegated_scopes" to "Delegated scopes",
"other" to context.getString(R.string.other) "other" to "Other"
).forEach { (k, v) -> ).forEach { (k, v) ->
Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { Row(
Modifier.fillMaxWidth(), Arrangement.SpaceBetween,
Alignment.CenterVertically
) {
Text(v) Text(v)
Checkbox(k in client.permissions, { Checkbox(k in client.permissions, {
val newPermissions = if (it) client.permissions.plus(k) else client.permissions.minus(k) updateDhizukuClient(client.copy(
clients[index] = client.copy(permissions = newPermissions) permissions = client.permissions.run { if (it) plus(k) else minus(k) }
writeList() ))
}) })
} }
} }
@@ -597,17 +444,18 @@ fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) {
} }
} }
} }
}
} }
@Serializable object LockScreenInfo @Serializable object LockScreenInfo
@RequiresApi(24) @RequiresApi(24)
@Composable @Composable
fun LockScreenInfoScreen(onNavigateUp: () -> Unit) { fun LockScreenInfoScreen(
getText: () -> String, setText: (String) -> Unit, onNavigateUp: () -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var infoText by remember { mutableStateOf(Privilege.DPM.deviceOwnerLockScreenInfo?.toString() ?: "") } var infoText by remember { mutableStateOf(getText()) }
MyScaffold(R.string.lock_screen_info, onNavigateUp) { MyScaffold(R.string.lock_screen_info, onNavigateUp) {
OutlinedTextField( OutlinedTextField(
value = infoText, value = infoText,
@@ -622,7 +470,7 @@ fun LockScreenInfoScreen(onNavigateUp: () -> Unit) {
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
Privilege.DPM.setDeviceOwnerLockScreenInfo(Privilege.DAR, infoText) setText(infoText)
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -632,7 +480,7 @@ fun LockScreenInfoScreen(onNavigateUp: () -> Unit) {
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
Privilege.DPM.setDeviceOwnerLockScreenInfo(Privilege.DAR, null) setText("")
infoText = "" infoText = ""
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
@@ -645,73 +493,56 @@ fun LockScreenInfoScreen(onNavigateUp: () -> Unit) {
} }
} }
@Keep data class DelegatedScope(val id: String, val string: Int, val requiresApi: Int = 26)
@Suppress("InlinedApi") @Suppress("InlinedApi")
enum class DelegatedScope(val id: String, @StringRes val string: Int, val requiresApi: Int = 0) { val delegatedScopesList = listOf(
AppRestrictions(DevicePolicyManager.DELEGATION_APP_RESTRICTIONS, R.string.manage_application_restrictions), DelegatedScope(DevicePolicyManager.DELEGATION_APP_RESTRICTIONS, R.string.manage_application_restrictions),
BlockUninstall(DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL, R.string.block_uninstall), DelegatedScope(DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL, R.string.block_uninstall),
CertInstall(DevicePolicyManager.DELEGATION_CERT_INSTALL, R.string.manage_certificates), DelegatedScope(DevicePolicyManager.DELEGATION_CERT_INSTALL, R.string.manage_certificates),
CertSelection(DevicePolicyManager.DELEGATION_CERT_SELECTION, R.string.select_keychain_certificates, 29), DelegatedScope(DevicePolicyManager.DELEGATION_CERT_SELECTION, R.string.select_keychain_certificates, 29),
EnableSystemApp(DevicePolicyManager.DELEGATION_ENABLE_SYSTEM_APP, R.string.enable_system_app), DelegatedScope(DevicePolicyManager.DELEGATION_ENABLE_SYSTEM_APP, R.string.enable_system_app),
InstallExistingPackage(DevicePolicyManager.DELEGATION_INSTALL_EXISTING_PACKAGE, R.string.install_existing_packages, 28), DelegatedScope(DevicePolicyManager.DELEGATION_INSTALL_EXISTING_PACKAGE, R.string.install_existing_packages, 28),
KeepUninstalledPackages(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES, R.string.manage_uninstalled_packages, 28), DelegatedScope(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES, R.string.manage_uninstalled_packages, 28),
NetworkLogging(DevicePolicyManager.DELEGATION_NETWORK_LOGGING, R.string.network_logging, 29), DelegatedScope(DevicePolicyManager.DELEGATION_NETWORK_LOGGING, R.string.network_logging, 29),
PackageAccess(DevicePolicyManager.DELEGATION_PACKAGE_ACCESS, R.string.change_package_state), DelegatedScope(DevicePolicyManager.DELEGATION_PACKAGE_ACCESS, R.string.change_package_state),
PermissionGrant(DevicePolicyManager.DELEGATION_PERMISSION_GRANT, R.string.grant_permissions), DelegatedScope(DevicePolicyManager.DELEGATION_PERMISSION_GRANT, R.string.grant_permissions),
SecurityLogging(DevicePolicyManager.DELEGATION_SECURITY_LOGGING, R.string.security_logging, 31) DelegatedScope(DevicePolicyManager.DELEGATION_SECURITY_LOGGING, R.string.security_logging, 31)
} ).filter { VERSION.SDK_INT >= it.requiresApi }
data class DelegatedAdmin(val app: AppInfo, val scopes: List<String>)
@Serializable object DelegatedAdmins @Serializable object DelegatedAdmins
@RequiresApi(26) @RequiresApi(26)
@Composable @Composable
fun DelegatedAdminsScreen(onNavigateUp: () -> Unit, onNavigate: (AddDelegatedAdmin) -> Unit) { fun DelegatedAdminsScreen(
val packages = remember { mutableStateMapOf<String, MutableList<DelegatedScope>>() } delegatedAdmins: StateFlow<List<DelegatedAdmin>>, getDelegatedAdmins: () -> Unit,
fun refresh() { onNavigateUp: () -> Unit, onNavigate: (AddDelegatedAdmin) -> Unit
val list = mutableMapOf<String, MutableList<DelegatedScope>>() ) {
DelegatedScope.entries.forEach { ds -> val admins by delegatedAdmins.collectAsStateWithLifecycle()
if(VERSION.SDK_INT >= ds.requiresApi) { LaunchedEffect(Unit) { getDelegatedAdmins() }
Privilege.DPM.getDelegatePackages(Privilege.DAR, ds.id)?.forEach { pkg -> MyLazyScaffold(R.string.delegated_admins, onNavigateUp) {
if(list[pkg] != null) { items(admins, { it.app.name }) { (app, scopes) ->
list[pkg]!!.add(ds)
} else {
list[pkg] = mutableListOf(ds)
}
}
}
}
packages.clear()
packages.putAll(list)
}
LaunchedEffect(Unit) { refresh() }
MyScaffold(R.string.delegated_admins, onNavigateUp, 0.dp) {
packages.forEach { (pkg, scopes) ->
Row( Row(
Modifier Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(),
.fillMaxWidth() Arrangement.SpaceBetween, Alignment.CenterVertically
.padding(vertical = 8.dp)
.padding(start = 14.dp, end = 8.dp),
Arrangement.SpaceBetween
) { ) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberDrawablePainter(app.icon), contentDescription = null,
modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(40.dp)
)
Column { Column {
Text(pkg, style = typography.titleMedium) Text(app.label)
Text( Text(app.name, Modifier.alpha(0.8F), style = typography.bodyMedium)
scopes.size.toString() + " " + stringResource(R.string.delegated_scope),
color = colorScheme.onSurfaceVariant, style = typography.bodyMedium
)
} }
IconButton({ onNavigate(AddDelegatedAdmin(pkg, scopes)) }) { }
Icon(Icons.Outlined.Edit, stringResource(R.string.edit)) IconButton({ onNavigate(AddDelegatedAdmin(app.name, scopes)) }) {
Icon(Icons.Outlined.Edit, null)
} }
} }
} }
if(packages.isEmpty()) Text( item {
stringResource(R.string.none),
color = colorScheme.onSurfaceVariant,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(vertical = 4.dp)
)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -723,35 +554,42 @@ fun DelegatedAdminsScreen(onNavigateUp: () -> Unit, onNavigate: (AddDelegatedAdm
Text(stringResource(R.string.add_delegated_admin), style = typography.titleMedium) Text(stringResource(R.string.add_delegated_admin), style = typography.titleMedium)
} }
} }
}
} }
@Serializable data class AddDelegatedAdmin(val pkg: String = "", val scopes: List<DelegatedScope> = emptyList()) @Serializable data class AddDelegatedAdmin(val pkg: String = "", val scopes: List<String> = emptyList())
@RequiresApi(26) @RequiresApi(26)
@Composable @Composable
fun AddDelegatedAdminScreen( fun AddDelegatedAdminScreen(
chosenPackage: Channel<String>, onChoosePackage: () -> Unit, chosenPackage: Channel<String>, onChoosePackage: () -> Unit, data: AddDelegatedAdmin,
data: AddDelegatedAdmin, onNavigateUp: () -> Unit setDelegatedAdmin: (String, List<String>) -> Unit, onNavigateUp: () -> Unit
) { ) {
val updateMode = data.pkg.isNotEmpty() val updateMode = data.pkg.isNotEmpty()
var input by remember { mutableStateOf(data.pkg) } var input by remember { mutableStateOf(data.pkg) }
val scopes = remember { mutableStateListOf(*data.scopes.toTypedArray()) } val scopes = rememberSaveable { mutableStateListOf(*data.scopes.toTypedArray()) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
input = chosenPackage.receive() input = chosenPackage.receive()
} }
MySmallTitleScaffold(if(updateMode) R.string.place_holder else R.string.add_delegated_admin, onNavigateUp, 0.dp) { MySmallTitleScaffold(if(updateMode) R.string.place_holder else R.string.add_delegated_admin, onNavigateUp, 0.dp) {
if (updateMode) {
OutlinedTextField(input, {}, Modifier.fillMaxWidth().padding(HorizontalPadding, 8.dp),
enabled = false, label = { Text(stringResource(R.string.package_name)) })
} else {
PackageNameTextField(input, onChoosePackage, PackageNameTextField(input, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { input = it } Modifier.padding(HorizontalPadding, 8.dp)) { input = it }
DelegatedScope.entries.filter { VERSION.SDK_INT >= it.requiresApi }.forEach { scope -> }
val checked = scope in scopes delegatedScopesList.forEach { scope ->
val checked = scope.id in scopes
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { if (!checked) scopes += scope else scopes -= scope } .clickable { if (!checked) scopes += scope.id else scopes -= scope.id }
.padding(vertical = 4.dp), .padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Checkbox(checked, { if(it) scopes += scope else scopes -= scope }, modifier = Modifier.padding(horizontal = 4.dp)) Checkbox(checked, { if(it) scopes += scope.id else scopes -= scope.id },
modifier = Modifier.padding(horizontal = 4.dp))
Column { Column {
Text(stringResource(scope.string)) Text(stringResource(scope.string))
Text(scope.id, style = typography.bodyMedium, color = colorScheme.onSurfaceVariant) Text(scope.id, style = typography.bodyMedium, color = colorScheme.onSurfaceVariant)
@@ -760,63 +598,57 @@ fun AddDelegatedAdminScreen(
} }
Button( Button(
onClick = { onClick = {
Privilege.DPM.setDelegatedScopes(Privilege.DAR, input, scopes.map { it.id }) setDelegatedAdmin(input, scopes)
onNavigateUp() onNavigateUp()
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, vertical = 4.dp),
.fillMaxWidth()
.padding(HorizontalPadding, vertical = 4.dp),
enabled = input.isNotBlank() && (!updateMode || scopes.toList() != data.scopes) enabled = input.isNotBlank() && (!updateMode || scopes.toList() != data.scopes)
) { ) {
Text(stringResource(if(updateMode) R.string.update else R.string.add)) Text(stringResource(if(updateMode) R.string.update else R.string.add))
} }
if(updateMode) Button( if(updateMode) Button(
onClick = { onClick = {
Privilege.DPM.setDelegatedScopes(Privilege.DAR, input, emptyList()) setDelegatedAdmin(input, emptyList())
onNavigateUp() onNavigateUp()
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
.fillMaxWidth()
.padding(HorizontalPadding),
colors = ButtonDefaults.buttonColors(colorScheme.error, colorScheme.onError) colors = ButtonDefaults.buttonColors(colorScheme.error, colorScheme.onError)
) { ) {
Text(stringResource(R.string.delete)) Text(stringResource(R.string.delete))
} }
Spacer(Modifier.height(40.dp))
} }
} }
@Serializable object DeviceInfo @Serializable object DeviceInfo
@Composable @Composable
fun DeviceInfoScreen(onNavigateUp: () -> Unit) { fun DeviceInfoScreen(vm: MyViewModel, onNavigateUp: () -> Unit) {
val privilege by Privilege.status.collectAsStateWithLifecycle() val privilege by Privilege.status.collectAsStateWithLifecycle()
var dialog by remember { mutableIntStateOf(0) } var dialog by remember { mutableIntStateOf(0) }
MyScaffold(R.string.device_info, onNavigateUp, 0.dp) { MyScaffold(R.string.device_info, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT>=34 && (privilege.device || privilege.org)) { if (VERSION.SDK_INT >= 34 && (privilege.device || privilege.org)) {
InfoItem(R.string.financed_device, Privilege.DPM.isDeviceFinanced.yesOrNo) InfoItem(R.string.financed_device, vm.getDeviceFinanced().yesOrNo)
} }
if(VERSION.SDK_INT >= 33) { if (VERSION.SDK_INT >= 33) {
val dpmRole = Privilege.DPM.devicePolicyManagementRoleHolderPackage InfoItem(R.string.dpmrh, vm.getDpmRh() ?: stringResource(R.string.none))
InfoItem(R.string.dpmrh, dpmRole ?: stringResource(R.string.none))
} }
val encryptionStatus = mutableMapOf( val encryptionStatus = when (vm.getStorageEncryptionStatus()) {
DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE to R.string.es_inactive, DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE -> R.string.es_inactive
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE to R.string.es_active, DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE -> R.string.es_active
DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED to R.string.es_unsupported DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED -> R.string.es_unsupported
) DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY -> R.string.es_active_default_key
if(VERSION.SDK_INT >= 23) { encryptionStatus[DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY] = R.string.es_active_default_key } DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER -> R.string.es_active_per_user
if(VERSION.SDK_INT >= 24) { encryptionStatus[DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER] = R.string.es_active_per_user } else -> R.string.unknown
InfoItem(R.string.encryption_status, encryptionStatus[Privilege.DPM.storageEncryptionStatus] ?: R.string.unknown) }
if(VERSION.SDK_INT >= 28) { InfoItem(R.string.encryption_status, encryptionStatus)
InfoItem(R.string.support_device_id_attestation, Privilege.DPM.isDeviceIdAttestationSupported.yesOrNo, true) { dialog = 1 } if (VERSION.SDK_INT >= 28) {
InfoItem(R.string.support_device_id_attestation, vm.getDeviceIdAttestationSupported().yesOrNo, true) { dialog = 1 }
} }
if (VERSION.SDK_INT >= 30) { if (VERSION.SDK_INT >= 30) {
InfoItem(R.string.support_unique_device_attestation, Privilege.DPM.isUniqueDeviceAttestationSupported.yesOrNo, true) { dialog = 2 } InfoItem(R.string.support_unique_device_attestation, vm.getUniqueDeviceAttestationSupported().yesOrNo, true) { dialog = 2 }
}
val adminList = Privilege.DPM.activeAdmins
if(adminList != null) {
InfoItem(R.string.activated_device_admin, adminList.joinToString("\n") { it.flattenToShortString() })
} }
InfoItem(R.string.activated_device_admin, vm.getActiveAdmins())
} }
if(dialog != 0) AlertDialog( if(dialog != 0) AlertDialog(
text = { Text(stringResource(if(dialog == 1) R.string.info_device_id_attestation else R.string.info_unique_device_attestation)) }, text = { Text(stringResource(if(dialog == 1) R.string.info_device_id_attestation else R.string.info_unique_device_attestation)) },
@@ -829,15 +661,17 @@ fun DeviceInfoScreen(onNavigateUp: () -> Unit) {
@RequiresApi(24) @RequiresApi(24)
@Composable @Composable
fun SupportMessageScreen(onNavigateUp: () -> Unit) { fun SupportMessageScreen(
getShortMessage: () -> String, getLongMessage: () -> String, setShortMessage: (String?) -> Unit,
setLongMessage: (String?) -> Unit, onNavigateUp: () -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
var shortMsg by remember { mutableStateOf("") } var shortMsg by remember { mutableStateOf("") }
var longMsg by remember { mutableStateOf("") } var longMsg by remember { mutableStateOf("") }
val refreshMsg = { LaunchedEffect(Unit) {
shortMsg = Privilege.DPM.getShortSupportMessage(Privilege.DAR)?.toString() ?: "" shortMsg = getShortMessage()
longMsg = Privilege.DPM.getLongSupportMessage(Privilege.DAR)?.toString() ?: "" longMsg = getLongMessage()
} }
LaunchedEffect(Unit) { refreshMsg() }
MyScaffold(R.string.support_messages, onNavigateUp) { MyScaffold(R.string.support_messages, onNavigateUp) {
OutlinedTextField( OutlinedTextField(
value = shortMsg, value = shortMsg,
@@ -851,8 +685,7 @@ fun SupportMessageScreen(onNavigateUp: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button( Button(
onClick = { onClick = {
Privilege.DPM.setShortSupportMessage(Privilege.DAR, shortMsg) setShortMessage(shortMsg)
refreshMsg()
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.49F) modifier = Modifier.fillMaxWidth(0.49F)
@@ -861,8 +694,8 @@ fun SupportMessageScreen(onNavigateUp: () -> Unit) {
} }
Button( Button(
onClick = { onClick = {
Privilege.DPM.setShortSupportMessage(Privilege.DAR, null) setShortMessage(null)
refreshMsg() shortMsg = ""
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.96F) modifier = Modifier.fillMaxWidth(0.96F)
@@ -884,8 +717,7 @@ fun SupportMessageScreen(onNavigateUp: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button( Button(
onClick = { onClick = {
Privilege.DPM.setLongSupportMessage(Privilege.DAR, longMsg) setLongMessage(longMsg)
refreshMsg()
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.49F) modifier = Modifier.fillMaxWidth(0.49F)
@@ -894,8 +726,8 @@ fun SupportMessageScreen(onNavigateUp: () -> Unit) {
} }
Button( Button(
onClick = { onClick = {
Privilege.DPM.setLongSupportMessage(Privilege.DAR, null) setLongMessage(null)
refreshMsg() longMsg = ""
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.96F) modifier = Modifier.fillMaxWidth(0.96F)
@@ -907,57 +739,60 @@ fun SupportMessageScreen(onNavigateUp: () -> Unit) {
} }
} }
data class DeviceAdmin(val app: AppInfo, val admin: ComponentName)
@Serializable object TransferOwnership @Serializable object TransferOwnership
@RequiresApi(28) @RequiresApi(28)
@Composable @Composable
fun TransferOwnershipScreen(onNavigateUp: () -> Unit, onTransferred: () -> Unit) { fun TransferOwnershipScreen(
val context = LocalContext.current deviceAdmins: StateFlow<List<DeviceAdmin>>, getDeviceAdmins: () -> Unit,
transferOwnership: (ComponentName) -> Unit, onNavigateUp: () -> Unit, onTransferred: () -> Unit
) {
val privilege by Privilege.status.collectAsStateWithLifecycle() val privilege by Privilege.status.collectAsStateWithLifecycle()
val focusMgr = LocalFocusManager.current var selectedIndex by remember { mutableIntStateOf(-1) }
var input by remember { mutableStateOf("") }
val componentName = ComponentName.unflattenFromString(input)
var dialog by remember { mutableStateOf(false) } var dialog by remember { mutableStateOf(false) }
MyScaffold(R.string.transfer_ownership, onNavigateUp) { val receivers by deviceAdmins.collectAsStateWithLifecycle()
OutlinedTextField( LaunchedEffect(Unit) { getDeviceAdmins() }
value = input, onValueChange = { input = it }, label = { Text(stringResource(R.string.target_component_name)) }, MyLazyScaffold(R.string.transfer_ownership, onNavigateUp) {
modifier = Modifier.fillMaxWidth(), itemsIndexed(receivers) { index, admin ->
isError = input != "" && componentName == null, Row(
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), Modifier.fillMaxWidth().clickable { selectedIndex = index }.padding(8.dp),
keyboardActions = KeyboardActions(onNext = { focusMgr.clearFocus() }) verticalAlignment = Alignment.CenterVertically
) ) {
Spacer(Modifier.padding(vertical = 5.dp)) RadioButton(selectedIndex == index, { selectedIndex = index })
Image(rememberDrawablePainter(admin.app.icon), null, Modifier.size(40.dp))
Column(Modifier.padding(start = 8.dp)) {
Text(admin.app.label)
Text(admin.app.name, Modifier.alpha(0.7F), style = typography.bodyMedium)
}
}
}
item {
Button( Button(
onClick = { dialog = true }, onClick = { dialog = true },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, 10.dp),
enabled = componentName != null enabled = receivers.getOrNull(selectedIndex) != null
) { ) {
Text(stringResource(R.string.transfer)) Text(stringResource(R.string.transfer))
} }
Spacer(Modifier.padding(vertical = 10.dp)) Notes(R.string.info_transfer_ownership, HorizontalPadding)
Notes(R.string.info_transfer_ownership)
} }
if(dialog) AlertDialog( }
if (dialog) AlertDialog(
text = { text = {
Text(stringResource( Text(stringResource(
R.string.transfer_ownership_warning, R.string.transfer_ownership_warning,
stringResource(if(privilege.device) R.string.device_owner else R.string.profile_owner), stringResource(if(privilege.device) R.string.device_owner else R.string.profile_owner),
ComponentName.unflattenFromString(input)!!.packageName receivers[selectedIndex].app.name
)) ))
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
try { transferOwnership(receivers[selectedIndex].admin)
Privilege.DPM.transferOwnership(Privilege.DAR, componentName!!, null)
Privilege.updateStatus()
context.showOperationResultToast(true)
dialog = false dialog = false
onTransferred() onTransferred()
} catch(e: Exception) {
e.printStackTrace()
context.showOperationResultToast(false)
}
}, },
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) { ) {

View File

@@ -16,7 +16,6 @@ import android.app.admin.DevicePolicyManager.WIPE_EUICC
import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE
import android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA import android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA
import android.app.admin.DevicePolicyManager.WIPE_SILENTLY import android.app.admin.DevicePolicyManager.WIPE_SILENTLY
import android.app.admin.SystemUpdateInfo
import android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC import android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC
import android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_WINDOWED import android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_WINDOWED
import android.app.admin.SystemUpdatePolicy.TYPE_POSTPONE import android.app.admin.SystemUpdatePolicy.TYPE_POSTPONE
@@ -135,7 +134,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.Date
import java.util.TimeZone import java.util.TimeZone
import kotlin.math.roundToLong import kotlin.math.roundToLong

View File

@@ -259,7 +259,7 @@ fun InfoItem(title: Int, text: String, withInfo: Boolean = false, onClick: () ->
Modifier.fillMaxWidth().padding(vertical = 6.dp).padding(start = HorizontalPadding, end = 8.dp), Modifier.fillMaxWidth().padding(vertical = 6.dp).padding(start = HorizontalPadding, end = 8.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically Arrangement.SpaceBetween, Alignment.CenterVertically
) { ) {
Column { Column(Modifier.weight(1F)) {
Text(stringResource(title), style = typography.titleLarge) Text(stringResource(title), style = typography.titleLarge)
Text(text, Modifier.alpha(0.8F)) Text(text, Modifier.alpha(0.8F))
} }