diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33cfa72..0d2c904 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,9 +61,9 @@ jobs: upload-telegram: name: Upload Builds - if: github.ref_name == 'dev' runs-on: ubuntu-latest - needs: ["build"] + needs: build + if: github.ref == 'refs/heads/dev' steps: - name: Download Artifacts uses: actions/download-artifact@v4 diff --git a/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl b/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl index c5b7b98..02b9446 100644 --- a/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl +++ b/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl @@ -6,4 +6,5 @@ interface IUserService { String execute(String command) = 1; int getUid() = 2; Account[] listAccounts() = 3; + void destroy() = 16777114; } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 9e82345..7147f25 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -71,6 +71,7 @@ import com.bintianqi.owndroid.dpm.ChangeUsername import com.bintianqi.owndroid.dpm.CreateUser import com.bintianqi.owndroid.dpm.CreateWorkProfile import com.bintianqi.owndroid.dpm.CurrentUserInfo +import com.bintianqi.owndroid.dpm.DelegatedAdmins import com.bintianqi.owndroid.dpm.DeleteWorkProfile import com.bintianqi.owndroid.dpm.DeviceAdmin import com.bintianqi.owndroid.dpm.DeviceInfo @@ -219,6 +220,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "DeviceAdmin") { DeviceAdmin(navCtrl) } composable(route = "ProfileOwner") { ProfileOwner(navCtrl) } composable(route = "DeviceOwner") { DeviceOwner(navCtrl) } + composable(route = "DelegatedAdmins") { DelegatedAdmins(navCtrl, vm) } composable(route = "DeviceInfo") { DeviceInfo(navCtrl) } composable(route = "LockScreenInfo") { LockScreenInfo(navCtrl) } composable(route = "SupportMessages") { SupportMessages(navCtrl) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 790c1ed..ed41cd2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -9,29 +9,42 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Binder import android.os.Build.VERSION -import android.os.Bundle +import android.os.IBinder import android.os.RemoteException import android.os.UserManager import android.widget.Toast import androidx.annotation.RequiresApi +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.core.os.bundleOf +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.R import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.showOperationResultToast @@ -87,39 +100,38 @@ fun Permissions(navCtrl: NavHostController) { try { if(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { bindingShizuku = true - val destination = navCtrl.graph.findNode("Shizuku")!!.id - bindShizukuService(context, { binder -> - val args = Bundle() - args.putBinder("binder", binder) + fun onBind(binder: IBinder) { + val destinationId = navCtrl.graph.findNode("Shizuku")!!.id bindingShizuku = false - navCtrl.navigate(destination, args) - }, { - Toast.makeText(context, R.string.shizuku_service_disconnected, Toast.LENGTH_SHORT).show() + navCtrl.navigate(destinationId, bundleOf("binder" to binder), NavOptions.Builder().setLaunchSingleTop(true).build()) + } + try { + controlShizukuService(context, ::onBind, { bindingShizuku = false }, true) + } catch(e: Exception) { + e.printStackTrace() bindingShizuku = false - }) + } } else if(Shizuku.shouldShowRequestPermissionRationale()) { Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_SHORT).show() } else { Sui.init(context.packageName) - val listener = object: Shizuku.OnRequestPermissionResultListener { - override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { - if(grantResult == PackageManager.PERMISSION_GRANTED) { - navCtrl.navigate("Shizuku") - } else { - Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_SHORT).show() - } - Shizuku.removeRequestPermissionResultListener(this) + fun requestPermissionResultListener(requestCode: Int, grantResult: Int) { + if(grantResult != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_SHORT).show() } + Shizuku.removeRequestPermissionResultListener(::requestPermissionResultListener) } - Shizuku.addRequestPermissionResultListener(listener) + Shizuku.addRequestPermissionResultListener(::requestPermissionResultListener) Shizuku.requestPermission(0) } } catch(_: IllegalStateException) { Toast.makeText(context, R.string.shizuku_not_started, Toast.LENGTH_SHORT).show() } } + if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) + FunctionItem(R.string.delegated_admins) { navCtrl.navigate("DelegatedAdmins") } FunctionItem(R.string.device_info, icon = R.drawable.perm_device_information_fill0) { navCtrl.navigate("DeviceInfo") } - if((VERSION.SDK_INT >= 26 && deviceOwner) || (VERSION.SDK_INT>=24 && profileOwner)) { + if((VERSION.SDK_INT >= 26 && deviceOwner) || (VERSION.SDK_INT >= 24 && profileOwner)) { FunctionItem(R.string.org_name, icon = R.drawable.corporate_fare_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 31 && (profileOwner || deviceOwner)) { @@ -476,6 +488,133 @@ fun DeviceOwner(navCtrl: NavHostController) { } } +@Suppress("InlinedApi") +private enum class DelegatedScope(val id: String, @StringRes val string: Int, val requiresApi: Int = 0) { + AppRestrictions(DevicePolicyManager.DELEGATION_APP_RESTRICTIONS, R.string.manage_application_restrictions), + BlockUninstall(DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL, R.string.block_uninstall), + CertInstall(DevicePolicyManager.DELEGATION_CERT_INSTALL, R.string.manage_certificates), + CertSelection(DevicePolicyManager.DELEGATION_CERT_SELECTION, R.string.select_keychain_certificates, 29), + EnableSystemApp(DevicePolicyManager.DELEGATION_ENABLE_SYSTEM_APP, R.string.enable_system_app), + InstallExistingPackage(DevicePolicyManager.DELEGATION_INSTALL_EXISTING_PACKAGE, R.string.install_existing_packages, 28), + KeepUninstalledPackages(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES, R.string.manage_uninstalled_packages, 28), + NetworkLogging(DevicePolicyManager.DELEGATION_NETWORK_LOGGING, R.string.network_logging, 29), + PackageAccess(DevicePolicyManager.DELEGATION_PACKAGE_ACCESS, R.string.change_package_state), + PermissionGrant(DevicePolicyManager.DELEGATION_PERMISSION_GRANT, R.string.grant_permissions), + SecurityLogging(DevicePolicyManager.DELEGATION_SECURITY_LOGGING, R.string.security_logging, 31) +} + +@RequiresApi(26) +@Composable +fun DelegatedAdmins(navCtrl: NavHostController, vm: MyViewModel) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + var dialog by rememberSaveable { mutableIntStateOf(0) } // 0:None, 1:Edit, 2:Add + var inputPackageName by rememberSaveable { mutableStateOf("") } + var selectedScopes by rememberSaveable { mutableStateOf(listOf()) } + val packages = remember { mutableStateMapOf>() } + fun refresh() { + val list = mutableMapOf>() + DelegatedScope.entries.forEach { ds -> + if(VERSION.SDK_INT >= ds.requiresApi) { + dpm.getDelegatePackages(receiver, ds.id)?.forEach { pkg -> + if(list[pkg] != null) { + list[pkg]!!.add(ds) + } else { + list[pkg] = mutableListOf(ds) + } + } + } + } + packages.clear() + packages.putAll(list) + } + LaunchedEffect(Unit) { refresh() } + MyScaffold(R.string.delegated_admins, 0.dp, navCtrl) { + packages.forEach { (pkg, scopes) -> + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { inputPackageName = pkg; selectedScopes = scopes.map { it.id }; dialog = 1 } + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text(pkg, style = typography.titleLarge) + Text(scopes.size.toString() + " " + stringResource(R.string.delegated_scope)) + } + } + if(packages.isEmpty()) + Text( + stringResource(R.string.none), + color = colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 4.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { inputPackageName = ""; selectedScopes = emptyList(); dialog = 2 } + .padding(vertical = 10.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Add, null, modifier = Modifier.padding(end = 12.dp)) + Text(stringResource(R.string.add_delegated_admin), style = typography.titleLarge) + } + if(dialog != 0) { + val selectedPackage by vm.selectedPackage.collectAsStateWithLifecycle() + LaunchedEffect(selectedPackage) { + if(selectedPackage != "") { + inputPackageName = selectedPackage + vm.selectedPackage.value = "" + } + } + AlertDialog( + text = { + val fm = LocalFocusManager.current + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + OutlinedTextField( + value = inputPackageName, onValueChange = { inputPackageName = it }, + label = { Text(stringResource(R.string.package_name)) }, + trailingIcon = { + if(dialog == 2) IconButton({ navCtrl.navigate("PackageSelector") }) { + Icon(painterResource(R.drawable.list_fill0), null) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { fm.clearFocus() }, + readOnly = dialog == 1, + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) + ) + DelegatedScope.entries.forEach { scope -> + if(VERSION.SDK_INT >= scope.requiresApi) { + CheckBoxItem(scope.string, scope.id in selectedScopes) { + if(it) selectedScopes += scope.id else selectedScopes -= scope.id + } + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.setDelegatedScopes(receiver, inputPackageName, selectedScopes) + refresh() + dialog = 0 + }, + enabled = inputPackageName.isNotBlank() + ) { + Text(stringResource(if(dialog == 1) R.string.apply else R.string.add)) + } + }, + dismissButton = { + TextButton({ dialog = 0 }) { + Text(stringResource(R.string.cancel)) + } + }, + onDismissRequest = { dialog = 0 } + ) + } + } +} + @Composable fun DeviceInfo(navCtrl: NavHostController) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt index a62f1ea..59bc771 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt @@ -151,10 +151,11 @@ fun Shizuku(navCtrl: NavHostController, navArgs: Bundle) { } } -fun bindShizukuService( +fun controlShizukuService( context: Context, onServiceConnected: (IBinder) -> Unit, - onServiceDisconnected: () -> Unit + onServiceDisconnected: () -> Unit, + state: Boolean ) { val userServiceConnection = object : ServiceConnection { override fun onServiceConnected(componentName: ComponentName, binder: IBinder) { @@ -169,11 +170,8 @@ fun bindShizukuService( .processNameSuffix("shizuku-service") .debuggable(false) .version(26) - try { - Shizuku.bindUserService(userServiceArgs, userServiceConnection) - } catch(e: Exception) { - e.printStackTrace() - } + if(state) Shizuku.bindUserService(userServiceArgs, userServiceConnection) + else Shizuku.unbindUserService(userServiceArgs, userServiceConnection, true) } @Composable diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt index 1237528..bb2e31b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt @@ -4,15 +4,11 @@ import android.accounts.Account import android.accounts.AccountManager import android.annotation.SuppressLint import android.content.Context -import android.os.Parcelable -import android.os.UserManager import android.system.Os import androidx.annotation.Keep import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.getContext -import java.io.BufferedReader -import java.io.InputStreamReader -import java.lang.Class +import kotlin.system.exitProcess @Keep class ShizukuService: IUserService.Stub() { @@ -28,10 +24,10 @@ class ShizukuService: IUserService.Stub() { return e.toString() } try { - val outputReader = BufferedReader(InputStreamReader(process.inputStream)) + val outputReader = process.inputStream.bufferedReader() var outputLine: String while(outputReader.readLine().also {outputLine = it} != null) { result += "$outputLine\n" } - val errorReader = BufferedReader(InputStreamReader(process.errorStream)) + val errorReader = process.errorStream.bufferedReader() var errorLine: String while(errorReader.readLine().also {errorLine = it} != null) { result += "$errorLine\n" } } catch(e: NullPointerException) { @@ -47,4 +43,8 @@ class ShizukuService: IUserService.Stub() { val am = getContext().getSystemService(Context.ACCOUNT_SERVICE) as AccountManager return am.accounts } + + override fun destroy() { + exitProcess(0) + } } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 09c4769..276afc9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -68,6 +68,7 @@ Edit Overview Features + Default @@ -76,6 +77,17 @@ Активировать... Владелец профиля Владелец устройства + + Delegated admins + delegated scope + Manage application restrictions + Manage certificates + Select KeyChain certificate + Install existing packages + Manage uninstalled packages + Change package state + Grant permissions + Add delegated admin Dhizuku будет деактивирован Сбросить политику устройства Активировать администратора устройства diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c684550..26cfd79 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -69,6 +69,7 @@ Edit Overview Features + Default Etkinleştirmek İçin Tıklayın @@ -76,6 +77,17 @@ Etkinleştir... Profil Sahibi Cihaz Sahibi + + Delegated admins + delegated scope + Manage application restrictions + Manage certificates + Select KeyChain certificate + Install existing packages + Manage uninstalled packages + Change package state + Grant permissions + Add delegated admin Dhizuku will be deactivated Reset device policy Cihaz Yöneticisini Etkinleştir diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8d0a798..5a6eb40 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -65,6 +65,7 @@ 编辑 概览 功能 + 默认 点击以激活 @@ -72,6 +73,16 @@ 激活... Profile owner Device owner + 委托管理员 + 委托作用域 + 管理应用限制 + 管理证书 + 选择密钥链证书 + 安装存在的软件包 + 管理已被卸载的软件包 + 修改软件包状态 + 授予权限 + 添加委托管理员 重置设备策略 Dhizuku将被停用 激活Device admin @@ -365,7 +376,7 @@ 作用域: 工作资料 应用详情 未安装 - 防卸载 + 阻止 禁止用户控制 用户将无法清除这些应用的存储空间或强制停止这些应用 应用列表: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abf376a..a65f4cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,6 +78,16 @@ Activate... Profile owner Device owner + Delegated admins + delegated scope + Manage application restrictions + Manage certificates + Select KeyChain certificate + Install existing packages + Manage uninstalled packages + Change package state + Grant permissions + Add delegated admin Dhizuku will be deactivated Reset device policy Activate Device admin diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0728b0e..b76cb63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.3" +agp = "8.8.0" kotlin = "2.0.21" navigation-compose = "2.8.5" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c2efe93..f9e8738 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Jan 12 20:22:20 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists