From 9528d3eb8dc4fc3834ad20cb07271684b56e0346 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 8 Feb 2025 19:07:23 +0800 Subject: [PATCH] Fix CA certs manager and User restrictions manager Add authentication to app installer Change version to 6.4 (36) --- app/build.gradle.kts | 7 +- .../owndroid/AppInstallerActivity.kt | 22 ++++- .../com/bintianqi/owndroid/MainActivity.kt | 42 +++++++--- .../java/com/bintianqi/owndroid/dpm/System.kt | 80 +++++++------------ .../bintianqi/owndroid/dpm/UserRestriction.kt | 53 +++++------- app/src/main/res/values-ru/strings.xml | 11 ++- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-zh-rCN/strings.xml | 6 +- app/src/main/res/values/strings.xml | 4 +- 9 files changed, 119 insertions(+), 112 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd8a58e..9263655 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 } diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt index 62c6b2a..619e4d7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt @@ -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()) val writingPackage = MutableStateFlow(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) { diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index c6a5c4d..65ca8e2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 337a9ef..130780c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -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(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 } + ) + } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index ba28702..1ab8e24 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -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, 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)) { + }*/ + } } } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8f92470..f7b7f91 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -5,6 +5,7 @@ Отключить Включить Успешно + Failure Ошибка Добавить Удалить @@ -69,7 +70,7 @@ Обзор Особенности По умолчанию - + Timeout Нажмите для активации @@ -194,8 +195,6 @@ Разрешить блокировку экрана Блокировать запуск активности в задаче CA-сертификат - Выберите сертификат - Сертификат установлен: %1$s Выберите сертификат... Удалить все пользовательские CA-сертификаты Журнал безопасности @@ -413,6 +412,7 @@ Тихое удаление Запросить удаление Установить приложение + Search Ограничения пользователя @@ -423,7 +423,6 @@ Другие подключения Медиа Другое - Требуется владелец устройства Настроить мобильную сеть Настроить Wi-Fi Роуминг данных @@ -608,8 +607,8 @@ Аутентифицировать Блокировать при переключении в фоновый режим Очистить хранилище - Авторизация пропущена, поскольку она недоступна. - + Авторизация пропущена, поскольку она недоступна. + Failed to authenticate API ключ API ключ уже сущетвует, установка нового перезапишет текущий diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index be39434..1c026db 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -6,6 +6,7 @@ Devre Dışı Bırak Etkinleştir Başarılı + Failure Başarısız Ekle Kaldır @@ -70,6 +71,7 @@ Overview Features Default + Timeout Etkinleştirmek İçin Tıklayın @@ -200,8 +202,6 @@ Ekran kilidine izin ver Görevde etkinlik başlatmayı engelle CA sertifikası - Lütfen bir sertifika seçin - Yüklenen sertifika: %1$s Sertifika seç... Tüm kullanıcı sertifikalarını kaldır Security logging @@ -428,7 +428,6 @@ Diğer bağlantı Medya Diğer - Cihaz sahibi gerektirir Mobil ağı yapılandır Wi-Fi\'yi yapılandır Veri dolaşımı @@ -610,6 +609,7 @@ Arka plana geçince kilitle Depolamayı temizle Skipped authentication because it is unavailable. + Failed to authenticate API key diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 31e517c..06c5014 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -5,6 +5,7 @@ 禁用 启用 成功 + 失败 失败 添加 移除 @@ -66,6 +67,7 @@ 概览 功能 默认 + 超时 点击以激活 @@ -191,8 +193,6 @@ 包名 不存在 CA证书 - 请选择CA证书 - 证书已安装:%1$s 选择证书... 卸载所有用户证书 安全日志 @@ -415,7 +415,6 @@ 更多连接 媒体 其他 - 需要DeviceOwner 配置移动数据 配置Wi-Fi 数据漫游 @@ -597,6 +596,7 @@ 处于后台时锁定 清除存储空间 验证已跳过,因为不可用 + 验证失败 API密钥 API密钥已存在,设置新的密钥将会覆盖当前密钥 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f402bc..115c9d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -222,8 +222,6 @@ Allow keyguard Block activity start in task CA certificate - Please select a certificate - Certificate installed: %1$s Select certificate... Uninstall all user CA certificate Security logging @@ -454,7 +452,6 @@ Other connection Media Other - Require device owner Configure mobile network Configure Wi-Fi Data roaming @@ -637,6 +634,7 @@ Lock when switch to background Clear storage Skipped authentication because it is unavailable. + Failed to authenticate API key The API key already exists, setting a new key will overwrite the current key.