Fix CA certs manager and User restrictions manager

Add authentication to app installer
Change version to 6.4 (36)
This commit is contained in:
BinTianqi
2025-02-08 19:07:23 +08:00
parent 5e109d74b1
commit 9528d3eb8d
9 changed files with 119 additions and 112 deletions

View File

@@ -10,12 +10,13 @@ import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -49,6 +50,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
@@ -63,7 +65,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLDecoder
class AppInstallerActivity:ComponentActivity() {
class AppInstallerActivity:FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
@@ -82,7 +84,7 @@ class AppInstallerActivity:ComponentActivity() {
installing, sessionMode, { vm.sessionMode.value = it },
packages, { uri -> vm.packages.update { it.minus(uri) } },
{ uris -> vm.packages.update { it.plus(uris) } },
vm::startInstallationProcess, writtenPackages, writingPackage,
{ vm.startInstallationProcess(this) }, writtenPackages, writingPackage,
result, { vm.result.value = null }
)
}
@@ -237,7 +239,19 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
val writtenPackages = MutableStateFlow(setOf<Uri>())
val writingPackage = MutableStateFlow<Uri?>(null)
fun startInstallationProcess() {
fun startInstallationProcess(activity: FragmentActivity) {
startAuth(activity, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
startInstall()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Toast.makeText(activity, R.string.failed_to_authenticate, Toast.LENGTH_SHORT).show()
}
})
}
private fun startInstall() {
if(installing.value) return
installing.value = true
viewModelScope.launch(Dispatchers.IO) {

View File

@@ -1,5 +1,6 @@
package com.bintianqi.owndroid
import android.annotation.SuppressLint
import android.app.admin.DevicePolicyManager
import android.os.Build.VERSION
import android.os.Bundle
@@ -53,6 +54,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -119,7 +121,7 @@ import com.bintianqi.owndroid.dpm.UpdateNetwork
import com.bintianqi.owndroid.dpm.UserOperation
import com.bintianqi.owndroid.dpm.UserOptions
import com.bintianqi.owndroid.dpm.UserRestriction
import com.bintianqi.owndroid.dpm.UserRestrictionItem
import com.bintianqi.owndroid.dpm.UserRestrictionScreen
import com.bintianqi.owndroid.dpm.UserSessionMessage
import com.bintianqi.owndroid.dpm.Users
import com.bintianqi.owndroid.dpm.Wifi
@@ -137,7 +139,6 @@ import com.bintianqi.owndroid.dpm.isDeviceOwner
import com.bintianqi.owndroid.dpm.isProfileOwner
import com.bintianqi.owndroid.dpm.setDefaultAffiliationID
import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import com.rosan.dhizuku.api.Dhizuku
import kotlinx.coroutines.delay
@@ -196,6 +197,17 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
LaunchedEffect(backToHome) {
if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false }
}
val userRestrictions by vm.userRestrictions.collectAsStateWithLifecycle()
fun onUserRestrictionsChange(id: String, status: Boolean) {
try {
if(status) dpm.addUserRestriction(receiver, id)
else dpm.clearUserRestriction(receiver, id)
@SuppressLint("NewApi")
vm.userRestrictions.value = dpm.getUserRestrictions(receiver)
} catch(_: Exception) {
context.showOperationResultToast(false)
}
}
@Suppress("NewApi") NavHost(
navController = navCtrl,
startDestination = "HomePage",
@@ -268,24 +280,36 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
composable(route = "Applications") { ApplicationManage(navCtrl, vm) }
composable(route = "UserRestriction") { UserRestriction(navCtrl) }
composable(route = "UserRestriction") { UserRestriction(navCtrl, vm) }
composable(route = "UR-Internet") {
MyScaffold(R.string.network_and_internet, 0.dp, navCtrl) { RestrictionData.internet.forEach { UserRestrictionItem(it, vm) } }
UserRestrictionScreen(R.string.network_and_internet, RestrictionData.internet, userRestrictions, ::onUserRestrictionsChange) {
navCtrl.navigateUp()
}
}
composable(route = "UR-Connectivity") {
MyScaffold(R.string.connectivity, 0.dp, navCtrl) { RestrictionData.connectivity.forEach { UserRestrictionItem(it, vm) } }
UserRestrictionScreen(R.string.connectivity, RestrictionData.connectivity, userRestrictions, ::onUserRestrictionsChange) {
navCtrl.navigateUp()
}
}
composable(route = "UR-Applications") {
MyScaffold(R.string.applications, 0.dp, navCtrl) { RestrictionData.applications.forEach { UserRestrictionItem(it, vm) } }
UserRestrictionScreen(R.string.applications, RestrictionData.applications, userRestrictions, ::onUserRestrictionsChange) {
navCtrl.navigateUp()
}
}
composable(route = "UR-Users") {
MyScaffold(R.string.users, 0.dp, navCtrl) { RestrictionData.users.forEach { UserRestrictionItem(it, vm) } }
UserRestrictionScreen(R.string.users, RestrictionData.users, userRestrictions, ::onUserRestrictionsChange) {
navCtrl.navigateUp()
}
}
composable(route = "UR-Media") {
MyScaffold(R.string.media, 0.dp, navCtrl) { RestrictionData.media.forEach { UserRestrictionItem(it, vm) } }
UserRestrictionScreen(R.string.media, RestrictionData.media, userRestrictions, ::onUserRestrictionsChange) {
navCtrl.navigateUp()
}
}
composable(route = "UR-Other") {
MyScaffold(R.string.other, 0.dp, navCtrl) { RestrictionData.other.forEach { UserRestrictionItem(it, vm) } }
UserRestrictionScreen(R.string.other, RestrictionData.other, userRestrictions, ::onUserRestrictionsChange) {
navCtrl.navigateUp()
}
}
composable(route = "Users") { Users(navCtrl) }

View File

@@ -137,7 +137,6 @@ import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.RadioButtonItem
import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.uriToStream
import com.bintianqi.owndroid.yesOrNo
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
@@ -1227,63 +1226,24 @@ fun CACert(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var exist by remember { mutableStateOf(false) }
var fileUri by remember { mutableStateOf<Uri?>(null) }
var caCertByteArray by remember { mutableStateOf(ByteArray(100000)) }
val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
result.data?.data?.let { uri ->
uriToStream(context, uri) {
val array = it.readBytes()
caCertByteArray = if(array.size < 10000) {
array
} else {
byteArrayOf()
}
}
var dialog by remember { mutableStateOf(false) }
var caCertByteArray = remember { byteArrayOf() }
val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri ?: return@rememberLauncherForActivityResult
uriToStream(context, uri) {
caCertByteArray = it.readBytes()
}
dialog = true
}
MyScaffold(R.string.ca_cert, 8.dp, navCtrl) {
Text(
text = if(fileUri == null) { stringResource(R.string.please_select_ca_cert) }
else { stringResource(R.string.cert_installed, stringResource(exist.yesOrNo)) },
modifier = Modifier.animateContentSize()
)
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
val caCertIntent = Intent(Intent.ACTION_GET_CONTENT)
caCertIntent.setType("*/*")
caCertIntent.addCategory(Intent.CATEGORY_OPENABLE)
getFileLauncher.launch(caCertIntent)
getFileLauncher.launch("*/*")
},
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
) {
Text(stringResource(R.string.select_ca_cert))
}
AnimatedVisibility(fileUri != null) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button(
onClick = {
context.showOperationResultToast(dpm.installCaCert(receiver, caCertByteArray))
exist = dpm.hasCaCertInstalled(receiver, caCertByteArray)
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.install))
}
Button(
onClick = {
dpm.uninstallCaCert(receiver, caCertByteArray)
exist = dpm.hasCaCertInstalled(receiver, caCertByteArray)
context.showOperationResultToast(true)
},
enabled = exist,
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.uninstall))
}
}
}
Button(
onClick = {
dpm.uninstallAllUserCaCerts(receiver)
@@ -1293,6 +1253,28 @@ fun CACert(navCtrl: NavHostController) {
) {
Text(stringResource(R.string.uninstall_all_user_ca_cert))
}
if(dialog) {
val exist = dpm.hasCaCertInstalled(receiver, caCertByteArray)
AlertDialog(
confirmButton = {
TextButton({
if(exist) {
dpm.uninstallCaCert(receiver, caCertByteArray)
} else {
val result = dpm.installCaCert(receiver, caCertByteArray)
context.showOperationResultToast(result)
}
dialog = false
}) {
Text(stringResource(if(exist) R.string.uninstall else R.string.install))
}
},
dismissButton = {
TextButton({ dialog = false }) { Text(stringResource(R.string.cancel)) }
},
onDismissRequest = { dialog = false }
)
}
}
}

View File

@@ -1,22 +1,20 @@
package com.bintianqi.owndroid.dpm
import android.os.Build.VERSION
import android.os.Build
import android.os.Bundle
import android.os.UserManager
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.R
@@ -31,15 +29,19 @@ data class Restriction(
val requiresApi: Int = 0
)
@RequiresApi(24)
@Composable
fun UserRestriction(navCtrl:NavHostController) {
fun UserRestriction(navCtrl:NavHostController, vm: MyViewModel) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
LaunchedEffect(Unit) {
vm.userRestrictions.value = dpm.getUserRestrictions(receiver)
}
MyScaffold(R.string.user_restriction, 0.dp, navCtrl) {
Text(text = stringResource(R.string.switch_to_disable_feature), modifier = Modifier.padding(start = 16.dp))
if(context.isProfileOwner) { Text(text = stringResource(R.string.profile_owner_is_restricted), modifier = Modifier.padding(start = 16.dp)) }
if(context.isProfileOwner && (VERSION.SDK_INT < 24 || dpm.isManagedProfile(receiver))) {
if(context.isProfileOwner && dpm.isManagedProfile(receiver)) {
Text(text = stringResource(R.string.some_features_invalid_in_work_profile), modifier = Modifier.padding(start = 16.dp))
}
Spacer(Modifier.padding(vertical = 2.dp))
@@ -54,30 +56,19 @@ fun UserRestriction(navCtrl:NavHostController) {
@RequiresApi(24)
@Composable
fun UserRestrictionItem(restriction: Restriction, vm: MyViewModel) {
val context = LocalContext.current
val userRestrictions by vm.userRestrictions.collectAsStateWithLifecycle()
Box(modifier = Modifier.padding(start = 22.dp, end = 16.dp)) {
SwitchItem(
restriction.name, restriction.id, restriction.icon,
userRestrictions.getBoolean(restriction.id),
{
val dpm = context.getDPM()
val receiver = context.getReceiver()
try {
if(it) {
dpm.addUserRestriction(receiver, restriction.id)
} else {
dpm.clearUserRestriction(receiver, restriction.id)
}
vm.userRestrictions.value = dpm.getUserRestrictions(receiver)
} catch(_: SecurityException) {
if(context.isProfileOwner) {
Toast.makeText(context, R.string.require_device_owner, Toast.LENGTH_SHORT).show()
}
}
}, padding = false
)
fun UserRestrictionScreen(
title: Int, items: List<Restriction>, restrictions: Bundle,
onRestrictionChange: (String, Boolean) -> Unit, onNavigateUp: () -> Unit
) {
MyScaffold(title, 0.dp, onNavigateUp, false) {
items.filter { Build.VERSION.SDK_INT >= it.requiresApi }.forEach { restriction ->
SwitchItem(
restriction.name, restriction.id, restriction.icon,
restrictions.getBoolean(restriction.id), { onRestrictionChange(restriction.id, it) }, padding = true
)
/*Box(modifier = Modifier.padding(start = 22.dp, end = 16.dp)) {
}*/
}
}
}