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

@@ -15,7 +15,7 @@ android {
}
}
namespace = "com.bintianqi.owndroid"
compileSdk = 34
compileSdk = 35
lint.checkReleaseBuilds = false
lint.disable += "All"
@@ -24,9 +24,8 @@ android {
applicationId = "com.bintianqi.owndroid"
minSdk = 21
targetSdk = 35
compileSdk = 35
versionCode = 35
versionName = "6.3"
versionCode = 36
versionName = "6.4"
multiDexEnabled = false
}

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)) {
}*/
}
}
}

View File

@@ -5,6 +5,7 @@
<string name="disable">Отключить</string>
<string name="enable">Включить</string>
<string name="success">Успешно</string>
<string name="failure">Failure</string> <!--TODO-->
<string name="failed">Ошибка</string>
<string name="add">Добавить</string>
<string name="remove">Удалить</string>
@@ -69,7 +70,7 @@
<string name="overview">Обзор</string>
<string name="features">Особенности</string>
<string name="default_str">По умолчанию</string>
<string name="timeout">Timeout</string> <!--TODO-->
<!--Разрешения-->
<string name="click_to_activate">Нажмите для активации</string>
@@ -194,8 +195,6 @@
<string name="ltf_keyguard">Разрешить блокировку экрана</string>
<string name="ltf_block_activity_start_in_task">Блокировать запуск активности в задаче</string>
<string name="ca_cert">CA-сертификат</string>
<string name="please_select_ca_cert">Выберите сертификат</string>
<string name="cert_installed">Сертификат установлен: %1$s</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">Выберите сертификат...</string>
<string name="uninstall_all_user_ca_cert">Удалить все пользовательские CA-сертификаты</string>
<string name="security_logging">Журнал безопасности</string>
@@ -413,6 +412,7 @@
<string name="silent_uninstall">Тихое удаление</string>
<string name="request_uninstall">Запросить удаление</string>
<string name="install_app">Установить приложение</string>
<string name="search">Search</string> <!--TODO-->
<!--Ограничения пользователя-->
<string name="user_restriction">Ограничения пользователя</string>
@@ -423,7 +423,6 @@
<string name="connectivity">Другие подключения</string>
<string name="media">Медиа</string>
<string name="other">Другое</string>
<string name="require_device_owner">Требуется владелец устройства</string>
<string name="config_mobile_network">Настроить мобильную сеть</string>
<string name="config_wifi">Настроить Wi-Fi</string>
<string name="data_roaming">Роуминг данных</string>
@@ -609,7 +608,7 @@
<string name="lock_in_background">Блокировать при переключении в фоновый режим</string>
<string name="clear_storage">Очистить хранилище</string>
<string name="skipped_authentication">Авторизация пропущена, поскольку она недоступна.</string>
<string name="failed_to_authenticate">Failed to authenticate</string> <!--TODO-->
<string name="api_key">API ключ</string>
<string name="api_key_exist">API ключ уже сущетвует, установка нового перезапишет текущий</string>

View File

@@ -6,6 +6,7 @@
<string name="disable">Devre Dışı Bırak</string>
<string name="enable">Etkinleştir</string>
<string name="success">Başarılı</string>
<string name="failure">Failure</string> <!--TODO-->
<string name="failed">Başarısız</string>
<string name="add">Ekle</string>
<string name="remove">Kaldır</string>
@@ -70,6 +71,7 @@
<string name="overview">Overview</string>
<string name="features">Features</string>
<string name="default_str">Default</string>
<string name="timeout">Timeout</string>
<!--Permissions-->
<string name="click_to_activate">Etkinleştirmek İçin Tıklayın</string>
@@ -200,8 +202,6 @@
<string name="ltf_keyguard">Ekran kilidine izin ver</string>
<string name="ltf_block_activity_start_in_task">Görevde etkinlik başlatmayı engelle</string>
<string name="ca_cert">CA sertifikası</string> <!--TODO-->
<string name="please_select_ca_cert">Lütfen bir sertifika seçin</string> <!--TODO-->
<string name="cert_installed">Yüklenen sertifika: %1$s</string> <!--TODO-->
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">Sertifika seç...</string> <!--TODO-->
<string name="uninstall_all_user_ca_cert">Tüm kullanıcı sertifikalarını kaldır</string> <!--TODO-->
<string name="security_logging">Security logging</string> <!--TODO-->
@@ -428,7 +428,6 @@
<string name="connectivity">Diğer bağlantı</string>
<string name="media">Medya</string>
<string name="other">Diğer</string>
<string name="require_device_owner">Cihaz sahibi gerektirir</string>
<string name="config_mobile_network">Mobil ağı yapılandır</string>
<string name="config_wifi">Wi-Fi\'yi yapılandır</string>
<string name="data_roaming">Veri dolaşımı</string>
@@ -610,6 +609,7 @@
<string name="lock_in_background">Arka plana geçince kilitle</string>
<string name="clear_storage">Depolamayı temizle</string>
<string name="skipped_authentication">Skipped authentication because it is unavailable.</string> <!--TODO-->
<string name="failed_to_authenticate">Failed to authenticate</string> <!--TODO-->
<!--TODO-->
<string name="api_key">API key</string>

View File

@@ -5,6 +5,7 @@
<string name="disable">禁用</string>
<string name="enable">启用</string>
<string name="success">成功</string>
<string name="failure">失败</string>
<string name="failed">失败</string>
<string name="add">添加</string>
<string name="remove">移除</string>
@@ -66,6 +67,7 @@
<string name="overview">概览</string>
<string name="features">功能</string>
<string name="default_str">默认</string>
<string name="timeout">超时</string>
<!--Permissions-->
<string name="click_to_activate">点击以激活</string>
@@ -191,8 +193,6 @@
<string name="package_name">包名</string>
<string name="not_exist">不存在</string>
<string name="ca_cert">CA证书</string>
<string name="please_select_ca_cert">请选择CA证书</string>
<string name="cert_installed">证书已安装:%1$s</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">选择证书...</string>
<string name="uninstall_all_user_ca_cert">卸载所有用户证书</string>
<string name="security_logging">安全日志</string>
@@ -415,7 +415,6 @@
<string name="connectivity">更多连接</string>
<string name="media">媒体</string>
<string name="other">其他</string>
<string name="require_device_owner">需要DeviceOwner</string>
<string name="config_mobile_network">配置移动数据</string>
<string name="config_wifi">配置Wi-Fi</string>
<string name="data_roaming">数据漫游</string>
@@ -597,6 +596,7 @@
<string name="lock_in_background">处于后台时锁定</string>
<string name="clear_storage">清除存储空间</string>
<string name="skipped_authentication">验证已跳过,因为不可用</string>
<string name="failed_to_authenticate">验证失败</string>
<string name="api_key">API密钥</string>
<string name="api_key_exist">API密钥已存在设置新的密钥将会覆盖当前密钥</string>

View File

@@ -222,8 +222,6 @@
<string name="ltf_keyguard">Allow keyguard</string>
<string name="ltf_block_activity_start_in_task">Block activity start in task</string>
<string name="ca_cert">CA certificate</string>
<string name="please_select_ca_cert">Please select a certificate</string>
<string name="cert_installed">Certificate installed: %1$s</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">Select certificate...</string>
<string name="uninstall_all_user_ca_cert">Uninstall all user CA certificate</string>
<string name="security_logging">Security logging</string>
@@ -454,7 +452,6 @@
<string name="connectivity">Other connection</string>
<string name="media">Media</string>
<string name="other">Other</string>
<string name="require_device_owner">Require device owner</string>
<string name="config_mobile_network">Configure mobile network</string>
<string name="config_wifi">Configure Wi-Fi</string>
<string name="data_roaming">Data roaming</string>
@@ -637,6 +634,7 @@
<string name="lock_in_background">Lock when switch to background</string>
<string name="clear_storage">Clear storage</string>
<string name="skipped_authentication">Skipped authentication because it is unavailable.</string>
<string name="failed_to_authenticate">Failed to authenticate</string>
<string name="api_key">API key</string>
<string name="api_key_exist">The API key already exists, setting a new key will overwrite the current key.</string>