diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b316e78..4196d93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -94,6 +94,7 @@ dependencies { implementation(libs.dhizuku.api) implementation(libs.androidx.fragment) implementation(libs.hiddenApiBypass) + implementation(libs.libsu) implementation(libs.serialization) implementation(kotlin("reflect")) } \ No newline at end of file diff --git a/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl b/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl index 02b9446..344371e 100644 --- a/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl +++ b/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl @@ -1,10 +1,9 @@ package com.bintianqi.owndroid; import android.accounts.Account; +import android.os.Bundle; interface IUserService { - String execute(String command) = 1; - int getUid() = 2; - Account[] listAccounts() = 3; + Bundle execute(String command) = 1; 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 a41af75..28f5b05 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -2,7 +2,6 @@ package com.bintianqi.owndroid import android.annotation.SuppressLint import android.app.Activity -import android.app.admin.DevicePolicyManager import android.os.Build.VERSION import android.os.Bundle import android.widget.Toast @@ -22,14 +21,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -57,8 +60,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute -import com.bintianqi.owndroid.dpm.Accounts -import com.bintianqi.owndroid.dpm.AccountsScreen import com.bintianqi.owndroid.dpm.AddApnSetting import com.bintianqi.owndroid.dpm.AddApnSettingScreen import com.bintianqi.owndroid.dpm.AddDelegatedAdmin @@ -107,8 +108,6 @@ import com.bintianqi.owndroid.dpm.DeleteWorkProfile import com.bintianqi.owndroid.dpm.DeleteWorkProfileScreen import com.bintianqi.owndroid.dpm.DeviceInfo import com.bintianqi.owndroid.dpm.DeviceInfoScreen -import com.bintianqi.owndroid.dpm.DeviceOwner -import com.bintianqi.owndroid.dpm.DeviceOwnerScreen import com.bintianqi.owndroid.dpm.DisableAccountManagement import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen import com.bintianqi.owndroid.dpm.DisableMeteredData @@ -172,8 +171,6 @@ import com.bintianqi.owndroid.dpm.PreferentialNetworkService import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceScreen import com.bintianqi.owndroid.dpm.PrivateDns import com.bintianqi.owndroid.dpm.PrivateDnsScreen -import com.bintianqi.owndroid.dpm.ProfileOwner -import com.bintianqi.owndroid.dpm.ProfileOwnerScreen import com.bintianqi.owndroid.dpm.QueryNetworkStats import com.bintianqi.owndroid.dpm.RecommendedGlobalProxy import com.bintianqi.owndroid.dpm.RecommendedGlobalProxyScreen @@ -191,7 +188,6 @@ import com.bintianqi.owndroid.dpm.SecurityLoggingScreen import com.bintianqi.owndroid.dpm.SetDefaultDialer import com.bintianqi.owndroid.dpm.SetDefaultDialerScreen import com.bintianqi.owndroid.dpm.SetSystemUpdatePolicy -import com.bintianqi.owndroid.dpm.ShizukuScreen import com.bintianqi.owndroid.dpm.SupportMessage import com.bintianqi.owndroid.dpm.SupportMessageScreen import com.bintianqi.owndroid.dpm.Suspend @@ -230,6 +226,8 @@ import com.bintianqi.owndroid.dpm.WifiSecurityLevelScreen import com.bintianqi.owndroid.dpm.WifiSsidPolicyScreen import com.bintianqi.owndroid.dpm.WipeData import com.bintianqi.owndroid.dpm.WipeDataScreen +import com.bintianqi.owndroid.dpm.WorkModes +import com.bintianqi.owndroid.dpm.WorkModesScreen import com.bintianqi.owndroid.dpm.WorkProfile import com.bintianqi.owndroid.dpm.WorkProfileScreen import com.bintianqi.owndroid.dpm.dhizukuErrorStatus @@ -299,6 +297,14 @@ fun Home(vm: MyViewModel) { val userRestrictions by vm.userRestrictions.collectAsStateWithLifecycle() fun navigateUp() { navController.navigateUp() } fun navigate(destination: Any) { navController.navigate(destination) } + LaunchedEffect(Unit) { + val privilege = myPrivilege.value + if(!privilege.device && !privilege.profile) { + navController.navigate(WorkModes(false)) { + popUpTo { inclusive = true } + } + } + } @Suppress("NewApi") NavHost( navController = navController, startDestination = Home, @@ -313,20 +319,35 @@ fun Home(vm: MyViewModel) { popExitTransition = Animations.navHostPopExitTransition ) { composable { HomeScreen { navController.navigate(it) } } + composable { + WorkModesScreen(it.toRoute(), ::navigateUp, { + navController.navigate(Home) { + popUpTo { inclusive = true } + } + }, { + navController.navigate(WorkModes(false)) { + popUpTo { inclusive = true } + } + }, { + navController.navigate(it) + }) + } composable { - PermissionsScreen(::navigateUp, { navController.navigate(it) }) { navController.navigate(ShizukuScreen, it) } + PermissionsScreen(::navigateUp) { navController.navigate(it) } } - composable { ShizukuScreen(it.arguments!!, ::navigateUp) { dest -> navController.navigate(dest) } } - composable(mapOf(serializableNavTypePair>())) { AccountsScreen(it.toRoute(), ::navigateUp) } - composable { ProfileOwnerScreen(::navigateUp) } - composable { DeviceOwnerScreen(::navigateUp) } composable { DelegatedAdminsScreen(::navigateUp, ::navigate) } composable{ AddDelegatedAdminScreen(it.toRoute(), ::navigateUp) } composable { DeviceInfoScreen(::navigateUp) } composable { LockScreenInfoScreen(::navigateUp) } composable { SupportMessageScreen(::navigateUp) } - composable { TransferOwnershipScreen(::navigateUp) } + composable { + TransferOwnershipScreen(::navigateUp) { + navController.navigate(WorkModes(false)) { + popUpTo(Home) { inclusive = true } + } + } + } composable { SystemManagerScreen(::navigateUp, ::navigate) } composable { SystemOptionsScreen(::navigateUp) } @@ -492,10 +513,10 @@ fun Home(vm: MyViewModel) { @Serializable private object Home +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun HomeScreen(onNavigate: (Any) -> Unit) { val context = LocalContext.current - val dpm = context.getDPM() val privilege by myPrivilege.collectAsStateWithLifecycle() val activateType = (if(privilege.dhizuku) context.getString(R.string.dhizuku) + " - " else "") + context.getString( @@ -504,9 +525,18 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) { else if(privilege.profile) R.string.profile_owner else R.string.click_to_activate ) - Scaffold { + Scaffold( + topBar = { + TopAppBar( + {}, actions = { + IconButton({ onNavigate(WorkModes(true)) }) { Icon(painterResource(R.drawable.security_fill0), null) } + IconButton({ onNavigate(Settings) }) { Icon(Icons.Default.Settings, null) } + } + ) + } + ) { Column(modifier = Modifier.padding(it).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 25.dp)) + Spacer(Modifier.padding(vertical = 8.dp)) Text( text = stringResource(R.string.app_name), style = typography.headlineLarge, modifier = Modifier.padding(start = HorizontalPadding) @@ -541,16 +571,9 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) { HomePageItem(R.string.system, R.drawable.android_fill0) { onNavigate(SystemManager) } HomePageItem(R.string.network, R.drawable.wifi_fill0) { onNavigate(Network) } } - if( - privilege.work || (VERSION.SDK_INT < 24 && !privilege.device) || - dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) - ) { + if(privilege.work) { HomePageItem(R.string.work_profile, R.drawable.work_fill0) { - onNavigate( - if(VERSION.SDK_INT < 24 || - dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) - ) WorkProfile else CreateWorkProfile - ) + onNavigate(WorkProfile) } } if(privilege.device || privilege.profile) { @@ -563,7 +586,6 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) { HomePageItem(R.string.users,R.drawable.manage_accounts_fill0) { onNavigate(Users) } HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0) { onNavigate(Password) } } - HomePageItem(R.string.settings, R.drawable.settings_fill0) { onNavigate(Settings) } Spacer(Modifier.padding(vertical = 20.dp)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 976b30c..c315486 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.core.content.edit import androidx.core.net.toUri +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.Notes @@ -60,6 +61,7 @@ import java.util.Locale @Composable fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current + val privilege by myPrivilege.collectAsStateWithLifecycle() val exportLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { if(it != null) exportLogs(context, it) } @@ -67,8 +69,10 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) } FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) } FunctionItem(R.string.app_lock, icon = R.drawable.lock_fill0) { onNavigate(AppLockSettings) } - FunctionItem(title = R.string.api, icon = R.drawable.code_fill0) { onNavigate(ApiSettings) } - FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) } + if (privilege.device || privilege.profile) + FunctionItem(title = R.string.api, icon = R.drawable.code_fill0) { onNavigate(ApiSettings) } + if (privilege.device && !privilege.dhizuku) + FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) } FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) { val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis())) exportLogsLauncher.launch("owndroid_log_$time") diff --git a/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt b/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt new file mode 100644 index 0000000..c19c106 --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt @@ -0,0 +1,72 @@ +package com.bintianqi.owndroid + +import android.content.ComponentName +import android.content.Context +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.IBinder +import android.widget.Toast +import androidx.annotation.Keep +import rikka.shizuku.Shizuku +import rikka.sui.Sui +import kotlin.system.exitProcess + +@Keep +class ShizukuService: IUserService.Stub() { + override fun execute(command: String): Bundle? { + try { + val bundle = Bundle() + val process = Runtime.getRuntime().exec(command) + val exitCode = process.waitFor() + bundle.putInt("code", exitCode) + bundle.putString("output", process.inputStream.readBytes().decodeToString()) + bundle.putString("error", process.errorStream.readBytes().decodeToString()) + return bundle + } catch(e: Exception) { + e.printStackTrace() + return null + } + } + + override fun destroy() { + exitProcess(0) + } +} + +fun getShizukuArgs(context: Context): Shizuku.UserServiceArgs { + return Shizuku.UserServiceArgs(ComponentName(context, ShizukuService::class.java)) + .daemon(false) + .processNameSuffix("shizuku-service") + .debuggable(false) + .version(1) +} + +fun useShizuku(context: Context, action: (IBinder?) -> Unit) { + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + action(service) + Shizuku.unbindUserService(getShizukuArgs(context), this, true) + } + override fun onServiceDisconnected(name: ComponentName?) {} + } + try { + if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + Shizuku.bindUserService(getShizukuArgs(context), connection) + } else if(Shizuku.shouldShowRequestPermissionRationale()) { + Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_SHORT).show() + } else { + Sui.init(context.packageName) + 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(::requestPermissionResultListener) + Shizuku.requestPermission(0) + } + } catch (e: Exception) { + e.printStackTrace() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 204a01d..d4c2362 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -1062,7 +1062,7 @@ fun EnableSystemAppScreen(onNavigateUp: () -> Unit) { ) { Text(stringResource(R.string.enable)) } - Notes(R.string.enable_system_app) + Notes(R.string.info_enable_system_app) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index 3b1a698..f34262e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -61,17 +61,8 @@ val Context.isProfileOwner: Boolean return dpm.isProfileOwnerApp("com.bintianqi.owndroid") } -val Context.dpcPackageName: String - get() { - return if(SharedPrefs(this).dhizuku) { - Dhizuku.getOwnerPackageName() - } else { - "com.bintianqi.owndroid" - } - } - @SuppressLint("PrivateApi") -private fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? { +fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? { try { val context = appContext.createPackageContext(Dhizuku.getOwnerComponent().packageName, Context.CONTEXT_IGNORE_SECURITY) val manager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager 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 e4a5fff..91d314c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -4,31 +4,67 @@ import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context 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.PersistableBundle import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Edit -import androidx.compose.material3.* +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography -import androidx.compose.runtime.* +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -37,30 +73,36 @@ 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 com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.HorizontalPadding +import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.R +import com.bintianqi.owndroid.Receiver +import com.bintianqi.owndroid.Settings import com.bintianqi.owndroid.SharedPrefs -import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.myPrivilege import com.bintianqi.owndroid.showOperationResultToast -import com.bintianqi.owndroid.ui.* +import com.bintianqi.owndroid.ui.FunctionItem +import com.bintianqi.owndroid.ui.InfoItem +import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.MySmallTitleScaffold +import com.bintianqi.owndroid.ui.NavIcon +import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.updatePrivilege +import com.bintianqi.owndroid.useShizuku import com.bintianqi.owndroid.writeClipBoard import com.bintianqi.owndroid.yesOrNo import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuRequestPermissionListener +import com.topjohnwu.superuser.Shell import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import rikka.shizuku.Shizuku -import rikka.sui.Sui @Serializable object Permissions @Composable -fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNavigateToShizuku: (Bundle) -> Unit) { +fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -69,57 +111,6 @@ fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNav var bindingShizuku by remember { mutableStateOf(false) } val enrollmentSpecificId = if(VERSION.SDK_INT >= 31 && (privilege.device || privilege.profile)) dpm.enrollmentSpecificId else "" MyScaffold(R.string.permissions, onNavigateUp, 0.dp) { - if(!dpm.isDeviceOwnerApp(context.packageName)) { - SwitchItem( - R.string.dhizuku, - getState = { SharedPrefs(context).dhizuku }, - onCheckedChange = { toggleDhizukuMode(it, context) }, - onClickBlank = { dialog = 4 } - ) - } - if(privilege.profile || !privilege.primary) { - FunctionItem( - R.string.profile_owner, stringResource(if(privilege.profile) R.string.activated else R.string.deactivated), - operation = { onNavigate(ProfileOwner) } - ) - } - if(!privilege.profile && privilege.primary) { - FunctionItem( - R.string.device_owner, stringResource(if(privilege.device) R.string.activated else R.string.deactivated), - operation = { onNavigate(DeviceOwner) } - ) - } - FunctionItem(R.string.shizuku) { - try { - if(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - bindingShizuku = true - fun onBind(binder: IBinder) { - bindingShizuku = false - onNavigateToShizuku(bundleOf("binder" to binder)) - } - 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) - 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(::requestPermissionResultListener) - Shizuku.requestPermission(0) - } - } catch(_: IllegalStateException) { - Toast.makeText(context, R.string.shizuku_not_started, Toast.LENGTH_SHORT).show() - } - } if(VERSION.SDK_INT >= 26) FunctionItem(R.string.delegated_admins) { onNavigate(DelegatedAdmins) } FunctionItem(R.string.device_info, icon = R.drawable.perm_device_information_fill0) { onNavigate(DeviceInfo) } if(VERSION.SDK_INT >= 24 && (privilege.profile || (VERSION.SDK_INT >= 26 && privilege.device))) { @@ -137,9 +128,6 @@ fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNav if(VERSION.SDK_INT >= 24) { FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { onNavigate(SupportMessage) } } - if(VERSION.SDK_INT >= 28) { - FunctionItem(R.string.transfer_ownership, icon = R.drawable.admin_panel_settings_fill0) { onNavigate(TransferOwnership) } - } } if(bindingShizuku) { Dialog(onDismissRequest = { bindingShizuku = false }) { @@ -225,39 +213,329 @@ fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNav } } -private fun toggleDhizukuMode(status: Boolean, context: Context) { - val sp = SharedPrefs(context) - if(!status) { - sp.dhizuku = false - backToHomeStateFlow.value = true - return - } - if(!Dhizuku.init(context)) { - dhizukuErrorStatus.value = 1 - return - } - if(dhizukuPermissionGranted()) { - sp.dhizuku = true - Dhizuku.init(context) - updatePrivilege(context) - backToHomeStateFlow.value = true - } else { - Dhizuku.requestPermission(object: DhizukuRequestPermissionListener() { - @Throws(RemoteException::class) - override fun onRequestPermission(grantResult: Int) { - if(grantResult == PackageManager.PERMISSION_GRANTED) { - sp.dhizuku = true - Dhizuku.init(context) - updatePrivilege(context) - backToHomeStateFlow.value = true - } else { - dhizukuErrorStatus.value = 2 +@Serializable data class WorkModes(val canNavigateUp: Boolean) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WorkModesScreen( + params: WorkModes, onNavigateUp: () -> Unit, onActivate: () -> Unit, onDeactivate: () -> Unit, + onNavigate: (Any) -> Unit +) { + val context = LocalContext.current + val coroutine = rememberCoroutineScope() + val privilege by myPrivilege.collectAsStateWithLifecycle() + /** 0: none, 1: device owner, 2: circular progress indicator, 3: result, 4: deactivate, 5: command */ + var dialog by remember { mutableIntStateOf(0) } + Scaffold( + topBar = { + TopAppBar( + { + if(!params.canNavigateUp) { + Column { + Text(stringResource(R.string.app_name)) + Text(stringResource(R.string.choose_work_mode), Modifier.alpha(0.8F), style = typography.bodyLarge) + } + } + }, + navigationIcon = { + if(params.canNavigateUp) NavIcon(onNavigateUp) + }, + actions = { + var expanded by remember { mutableStateOf(false) } + if(privilege.device || privilege.profile) Box { + IconButton({ expanded = true }) { + Icon(Icons.Default.MoreVert, null) + } + DropdownMenu(expanded, { expanded = false }) { + DropdownMenuItem({ Text(stringResource(R.string.deactivate)) }, { dialog = 4 }) + if(!privilege.dhizuku && VERSION.SDK_INT >= 28) DropdownMenuItem( + { Text(stringResource(R.string.transfer_ownership)) }, + { + expanded = false + onNavigate(TransferOwnership) + } + ) + } + } + if(!params.canNavigateUp) IconButton({ onNavigate(Settings) }) { + Icon(Icons.Default.Settings, null) + } } + ) + } + ) { paddingValues -> + var operationSucceed by remember { mutableStateOf(false) } + var resultText by remember { mutableStateOf("") } + fun handleResult(succeeded: Boolean, activateSucceeded: Boolean, output: String?) { + if(succeeded) { + operationSucceed = activateSucceeded + resultText = output ?: "" + dialog = 3 + updatePrivilege(context) + handlePrivilegeChange(context) + } else { + context.showOperationResultToast(false) } - }) + } + 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.device) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null, + tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground + ) + } + if(privilege.profile) Row( + Modifier + .fillMaxWidth() + .background(if(privilege.device) colorScheme.primaryContainer else Color.Transparent) + .padding(HorizontalPadding, 10.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Column { + Text(stringResource(R.string.profile_owner), style = typography.titleLarge) + } + Icon( + if(privilege.device) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null, + tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground + ) + } + if(privilege.dhizuku || !(privilege.device || privilege.profile)) Row( + Modifier + .fillMaxWidth() + .clickable(!privilege.dhizuku) { + dialog = 2 + activateDhizukuMode(context, ::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( + privilege.work || (VERSION.SDK_INT < 24 || + context.getDPM().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 { + Text(stringResource(R.string.work_profile), style = typography.titleLarge) + } + Icon( + if(privilege.work) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null, + tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground + ) + } + Column(Modifier.padding(HorizontalPadding, 20.dp)) { + Row(Modifier.padding(bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Outlined.Warning, null, Modifier.padding(end = 4.dp), colorScheme.error) + Text(stringResource(R.string.warning), color = colorScheme.error, style = typography.labelLarge) + } + Text(stringResource(R.string.owndroid_warning)) + } + } + if(dialog == 1) AlertDialog( + title = { Text(stringResource(R.string.activate_method)) }, + text = { + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + if(!privilege.dhizuku) Button({ + dialog = 2 + coroutine.launch { + activateUsingShizuku(context, ::handleResult) + } + }) { + Text(stringResource(R.string.shizuku)) + } + if(!privilege.dhizuku) Button({ + dialog = 2 + activateUsingRoot(context, ::handleResult) + }) { + Text("Root") + } + if(VERSION.SDK_INT >= 28) Button({ + dialog = 2 + activateUsingDhizuku(context, ::handleResult) + }) { + Text(stringResource(R.string.dhizuku)) + } + Button({ dialog = 5 }) { Text(stringResource(R.string.adb_command)) } + } + }, + confirmButton = { + TextButton({ dialog = 0 }) { Text(stringResource(R.string.cancel)) } + }, + onDismissRequest = { dialog = 0 } + ) + if(dialog == 2) Dialog({}) { + CircularProgressIndicator() + } + if(dialog == 3) AlertDialog( + title = { Text(stringResource(if(operationSucceed) R.string.succeeded else R.string.failed)) }, + text = { + Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Text(resultText) + } + }, + confirmButton = { + TextButton({ + dialog = 0 + if(operationSucceed && !params.canNavigateUp) onActivate() + }) { + Text(stringResource(R.string.confirm)) + } + }, + onDismissRequest = {} + ) + if(dialog == 4) AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + text = { Text(stringResource(R.string.info_deactivate)) }, + confirmButton = { + TextButton( + { + if(privilege.dhizuku) { + SharedPrefs(context).dhizuku = false + } else { + val dpm = context.getDPM() + if(privilege.device) { + dpm.clearDeviceOwnerApp(context.packageName) + } else if(VERSION.SDK_INT >= 24) { + dpm.clearProfileOwner(ComponentName(context, Receiver::class.java)) + } + } + updatePrivilege(context) + handlePrivilegeChange(context) + onDeactivate() + }, + colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + ) { Text(stringResource(R.string.confirm)) } + }, + dismissButton = { + TextButton({ dialog = 0 }) { Text(stringResource(R.string.cancel)) } + }, + onDismissRequest = { dialog = 0 } + ) + if(dialog == 5) AlertDialog( + text = { + SelectionContainer { + Text(ACTIVATE_DEVICE_OWNER_COMMAND) + } + }, + confirmButton = { + TextButton({ dialog = 0 }) { Text(stringResource(R.string.confirm)) } + }, + onDismissRequest = { dialog = 0 } + ) } } +fun activateUsingShizuku(context: Context, callback: (Boolean, Boolean, String?) -> Unit) { + useShizuku(context) { service -> + try { + val result = IUserService.Stub.asInterface(service).execute(ACTIVATE_DEVICE_OWNER_COMMAND) + if (result == null) { + callback(false, false, null) + } else { + callback( + true, result.getInt("code", -1) == 0, + result.getString("output") + "\n" + result.getString("error") + ) + } + } 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 { + val dpm = binderWrapperDevicePolicyManager(context) + if(dpm == null) { + context.showOperationResultToast(false) + } else { + dpm.transferOwnership( + Dhizuku.getOwnerComponent(), + ComponentName(context, Receiver::class.java), PersistableBundle() + ) + callback(true, true, null) + } + } catch (e: Exception) { + e.printStackTrace() + callback(true, false, null) + } + } + if(Dhizuku.init()) { + 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() { + SharedPrefs(context).dhizuku = true + callback(true, true, null) + } + if(Dhizuku.init()) { + 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/com.bintianqi.owndroid.Receiver" + @Serializable object LockScreenInfo @RequiresApi(24) @@ -302,135 +580,6 @@ fun LockScreenInfoScreen(onNavigateUp: () -> Unit) { } } -@Serializable object ProfileOwner - -@Composable -fun ProfileOwnerScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - var deactivateDialog by remember { mutableStateOf(false) } - val privilege by myPrivilege.collectAsStateWithLifecycle() - val profileOwner = privilege.profile - MyScaffold(R.string.profile_owner, onNavigateUp) { - Text(stringResource(if(profileOwner) R.string.activated else R.string.deactivated), style = typography.titleLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT >= 24 && profileOwner) { - Button( - onClick = { deactivateDialog = true }, - enabled = !dpm.isManagedProfile(receiver), - colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) - ) { - Text(stringResource(R.string.deactivate)) - } - } - if(!profileOwner) { - val command = context.getString(R.string.activate_profile_owner_command, (Binder.getCallingUid() / 100000).toString()) - SelectionContainer { - Text(command) - } - CopyTextButton(R.string.copy_command, command) - } - } - if(deactivateDialog && VERSION.SDK_INT >= 24) { - AlertDialog( - title = { Text(stringResource(R.string.deactivate)) }, - onDismissRequest = { deactivateDialog = false }, - dismissButton = { - TextButton( - onClick = { deactivateDialog = false } - ) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - TextButton( - onClick = { - dpm.clearProfileOwner(receiver) - deactivateDialog = false - }, - colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) - ) { - Text(stringResource(R.string.confirm)) - } - } - ) - } -} - -@Serializable object DeviceOwner - -@Composable -fun DeviceOwnerScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val dpm = context.getDPM() - var deactivateDialog by remember { mutableStateOf(false) } - val privilege by myPrivilege.collectAsStateWithLifecycle() - val deviceOwner = privilege.device - MyScaffold(R.string.device_owner, onNavigateUp) { - Text(text = stringResource(if(deviceOwner) R.string.activated else R.string.deactivated), style = typography.titleLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - AnimatedVisibility(deviceOwner) { - Button( - onClick = { deactivateDialog = true }, - colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) - ) { - Text(text = stringResource(R.string.deactivate)) - } - } - AnimatedVisibility(!deviceOwner) { - Column { - SelectionContainer{ - Text(text = stringResource(R.string.activate_device_owner_command)) - } - CopyTextButton(R.string.copy_command, stringResource(R.string.activate_device_owner_command)) - } - } - } - if(deactivateDialog) { - val sp = SharedPrefs(context) - var resetPolicy by remember { mutableStateOf(false) } - val coroutine = rememberCoroutineScope() - AlertDialog( - title = { Text(stringResource(R.string.deactivate)) }, - text = { - Column { - if(sp.dhizuku) Text(stringResource(R.string.dhizuku_will_be_deactivated)) - Spacer(Modifier.padding(vertical = 4.dp)) - CheckBoxItem(text = R.string.reset_device_policy, checked = resetPolicy, operation = { resetPolicy = it }) - } - }, - onDismissRequest = { deactivateDialog = false }, - dismissButton = { - TextButton( - onClick = { deactivateDialog = false } - ) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - TextButton( - onClick = { - coroutine.launch { - if(resetPolicy) context.resetDevicePolicy() - dpm.clearDeviceOwnerApp(context.dpcPackageName) - if(sp.dhizuku) { - if (!Dhizuku.init(context)) { - sp.dhizuku = false - backToHomeStateFlow.value = true - } - } - deactivateDialog = false - } - } - ) { - Text(stringResource(R.string.confirm)) - } - } - ) - } -} - @Keep @Suppress("InlinedApi") enum class DelegatedScope(val id: String, @StringRes val string: Int, val requiresApi: Int = 0) { @@ -698,7 +847,7 @@ fun SupportMessageScreen(onNavigateUp: () -> Unit) { @RequiresApi(28) @Composable -fun TransferOwnershipScreen(onNavigateUp: () -> Unit) { +fun TransferOwnershipScreen(onNavigateUp: () -> Unit, onTransferred: () -> Unit) { val context = LocalContext.current val privilege by myPrivilege.collectAsStateWithLifecycle() val focusMgr = LocalFocusManager.current @@ -742,7 +891,7 @@ fun TransferOwnershipScreen(onNavigateUp: () -> Unit) { context.showOperationResultToast(true) updatePrivilege(context) dialog = false - onNavigateUp() + onTransferred() } catch(e: Exception) { e.printStackTrace() context.showOperationResultToast(false) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt deleted file mode 100644 index 19698b1..0000000 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt +++ /dev/null @@ -1,200 +0,0 @@ -package com.bintianqi.owndroid.dpm - -import android.content.ComponentName -import android.content.Context -import android.content.ServiceConnection -import android.os.Binder -import android.os.Build.VERSION -import android.os.Bundle -import android.os.IBinder -import android.util.Log -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.IUserService -import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.myPrivilege -import com.bintianqi.owndroid.ui.MySmallTitleScaffold -import com.bintianqi.owndroid.updatePrivilege -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import rikka.shizuku.Shizuku - -@Serializable object ShizukuScreen - -@Composable -fun ShizukuScreen(navArgs: Bundle, onNavigateUp: () -> Unit, onNavigateToAccountsViewer: (Accounts) -> Unit) { - val context = LocalContext.current - val privilege by myPrivilege.collectAsStateWithLifecycle() - val coroutine = rememberCoroutineScope() - val outputTextScrollState = rememberScrollState() - var outputText by rememberSaveable { mutableStateOf("") } - val binder = navArgs.getBinder("binder")!! - var service by remember { mutableStateOf(null) } - LaunchedEffect(Unit) { - service = if(binder.pingBinder()) { - IUserService.Stub.asInterface(binder) - } else { - null - } - } - MySmallTitleScaffold(R.string.shizuku, onNavigateUp, 0.dp) { - - Button( - onClick = { - coroutine.launch{ - outputText = service!!.execute("dpm list-owners") - outputTextScrollState.animateScrollTo(0) - } - }, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text(text = stringResource(R.string.list_owners)) - } - Button( - onClick = { - coroutine.launch{ - outputText = service!!.execute("pm list users") - outputTextScrollState.animateScrollTo(0) - } - }, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text(text = stringResource(R.string.list_users)) - } - Button( - onClick = { - Log.d("Shizuku", "List accounts") - try { - val accounts = service!!.listAccounts().map { - Accounts.Account(it.type, it.name) - } - onNavigateToAccountsViewer(Accounts(accounts)) - } catch(_: Exception) { - outputText = service!!.execute("dumpsys account") - coroutine.launch{ - outputTextScrollState.animateScrollTo(0) - } - } - }, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text(text = stringResource(R.string.list_accounts)) - } - Spacer(Modifier.padding(vertical = 5.dp)) - - AnimatedVisibility(!privilege.device, modifier = Modifier.align(Alignment.CenterHorizontally)) { - Button( - onClick = { - coroutine.launch{ - outputText = service!!.execute(context.getString(R.string.dpm_activate_do_command)) - outputTextScrollState.animateScrollTo(0) - delay(500) - updatePrivilege(context) - } - } - ) { - Text(text = stringResource(R.string.activate_device_owner)) - } - } - - AnimatedVisibility(VERSION.SDK_INT >= 30 && privilege.work && !privilege.org) { - Button( - onClick = { - coroutine.launch{ - val userID = Binder.getCallingUid() / 100000 - outputText = service!!.execute( - "dpm mark-profile-owner-on-organization-owned-device --user $userID com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver" - ) - outputTextScrollState.animateScrollTo(0) - delay(500) - updatePrivilege(context) - } - }, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text(text = stringResource(R.string.activate_org_profile)) - } - } - - SelectionContainer(modifier = Modifier.fillMaxWidth().horizontalScroll(outputTextScrollState)) { - Text(text = outputText, softWrap = false, modifier = Modifier.padding(vertical = 9.dp, horizontal = 12.dp)) - } - } -} - -fun controlShizukuService( - context: Context, - onServiceConnected: (IBinder) -> Unit, - onServiceDisconnected: () -> Unit, - state: Boolean -) { - val userServiceConnection = object : ServiceConnection { - override fun onServiceConnected(componentName: ComponentName, binder: IBinder) { - onServiceConnected(binder) - } - override fun onServiceDisconnected(componentName: ComponentName) { - onServiceDisconnected() - } - } - val userServiceArgs = Shizuku.UserServiceArgs(ComponentName(context, ShizukuService::class.java)) - .daemon(false) - .processNameSuffix("shizuku-service") - .debuggable(false) - .version(26) - if(state) Shizuku.bindUserService(userServiceArgs, userServiceConnection) - else Shizuku.unbindUserService(userServiceArgs, userServiceConnection, true) -} - -@Serializable -data class Accounts( - val list: List -) { - @Serializable data class Account(val type: String, val name: String) -} - -@Composable -fun AccountsScreen(accounts: Accounts, onNavigateUp: () -> Unit) { - MySmallTitleScaffold(R.string.accounts, onNavigateUp) { - accounts.list.forEach { - Column( - modifier = Modifier - .fillMaxWidth().padding(vertical = 4.dp) - .clip(RoundedCornerShape(15)).background(MaterialTheme.colorScheme.surfaceVariant) - ) { - SelectionContainer { - Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { - Text(stringResource(R.string.type) + ": " + it.type) - Text(stringResource(R.string.name) + ": " + it.name) - } - } - } - } - } -} diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt deleted file mode 100644 index bb2e31b..0000000 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.bintianqi.owndroid.dpm - -import android.accounts.Account -import android.accounts.AccountManager -import android.annotation.SuppressLint -import android.content.Context -import android.system.Os -import androidx.annotation.Keep -import com.bintianqi.owndroid.IUserService -import com.bintianqi.owndroid.getContext -import kotlin.system.exitProcess - -@Keep -class ShizukuService: IUserService.Stub() { - override fun execute(command: String): String { - var result = "" - val process: Process - try { - process = Runtime.getRuntime().exec(command) - val exitCode = process.waitFor() - if(exitCode != 0) { result += "Error: $exitCode" } - } catch(e: Exception) { - e.printStackTrace() - return e.toString() - } - try { - val outputReader = process.inputStream.bufferedReader() - var outputLine: String - while(outputReader.readLine().also {outputLine = it} != null) { result += "$outputLine\n" } - val errorReader = process.errorStream.bufferedReader() - var errorLine: String - while(errorReader.readLine().also {errorLine = it} != null) { result += "$errorLine\n" } - } catch(e: NullPointerException) { - e.printStackTrace() - } - return result - } - - override fun getUid(): Int = Os.getuid() - - @SuppressLint("MissingPermission") - override fun listAccounts(): Array { - val am = getContext().getSystemService(Context.ACCOUNT_SERVICE) as AccountManager - return am.accounts - } - - override fun destroy() { - exitProcess(0) - } -} diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt index c44c4ca..1d6873f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt @@ -14,7 +14,9 @@ import android.app.admin.DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED import android.app.admin.DevicePolicyManager.PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT import android.app.admin.DevicePolicyManager.WIPE_EUICC import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE -import android.content.* +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.IntentFilter import android.os.Binder import android.os.Build.VERSION import android.widget.Toast @@ -54,17 +56,16 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.R import com.bintianqi.owndroid.myPrivilege import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem -import com.bintianqi.owndroid.ui.CopyTextButton import com.bintianqi.owndroid.ui.FunctionItem -import com.bintianqi.owndroid.ui.InfoItem import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.yesOrNo +import com.bintianqi.owndroid.useShizuku import kotlinx.serialization.Serializable @Serializable object WorkProfile @@ -73,7 +74,7 @@ import kotlinx.serialization.Serializable fun WorkProfileScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val privilege by myPrivilege.collectAsStateWithLifecycle() MyScaffold(R.string.work_profile, onNavigateUp, 0.dp) { - if(VERSION.SDK_INT >= 30) { + if(VERSION.SDK_INT >= 30 && !privilege.org) { FunctionItem(R.string.org_owned_work_profile, icon = R.drawable.corporate_fare_fill0) { onNavigate(OrganizationOwnedProfile) } } if(privilege.org) { @@ -162,22 +163,38 @@ fun CreateWorkProfileScreen(onNavigateUp: () -> Unit) { @Composable fun OrganizationOwnedProfileScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current - val dpm = context.getDPM() + var dialog by remember { mutableStateOf(false) } MyScaffold(R.string.org_owned_work_profile, onNavigateUp) { - InfoItem(R.string.org_owned_work_profile, dpm.isOrganizationOwnedDeviceWithManagedProfile.yesOrNo) - Spacer(Modifier.padding(vertical = 5.dp)) - if(!dpm.isOrganizationOwnedDeviceWithManagedProfile) { - SelectionContainer { - Text( - text = stringResource(R.string.activate_org_profile_command, Binder.getCallingUid()/100000), - color = colorScheme.onTertiaryContainer - ) + Button({ + useShizuku(context) { service -> + val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand) + if (result?.getInt("code", -1) == 0) { + context.showOperationResultToast(true) + } else { + context.showOperationResultToast(false) + } } - CopyTextButton(R.string.copy_command, stringResource(R.string.activate_org_profile_command, Binder.getCallingUid()/100000)) + }) { + Text(stringResource(R.string.shizuku)) } + Button({ dialog = true }) { Text(stringResource(R.string.adb_command)) } + if(dialog) AlertDialog( + text = { + SelectionContainer { + Text(activateOrgProfileCommand) + } + }, + confirmButton = { + TextButton({ dialog = false }) { Text(stringResource(R.string.confirm)) } + }, + onDismissRequest = { dialog = false } + ) } } +val activateOrgProfileCommand = "dpm mark-profile-owner-on-organization-owned-device --user " + + "${Binder.getCallingUid()/100000} com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver" + @Serializable object SuspendPersonalApp @RequiresApi(30) diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index b6ec15c..059f63b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -211,31 +211,6 @@ fun SwitchItem( } } -@Composable -fun CopyTextButton(@StringRes label: Int, content: String) { - val context = LocalContext.current - var ok by remember{ mutableStateOf(false) } - val scope = rememberCoroutineScope() - Button( - onClick = { - if(!ok) { - scope.launch { - if(writeClipBoard(context,content)) { ok = true; delay(2000); ok = false } - else{ Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } - } - } - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, modifier = Modifier.animateContentSize() - ) { - Icon(painter = painterResource(if(ok) R.drawable.check_fill0 else R.drawable.content_copy_fill0), contentDescription = null) - Spacer(modifier = Modifier.padding(horizontal = 2.dp)) - Text(text = stringResource(if(ok) R.string.success else label)) - } - } -} - @Composable fun InfoItem(title: Int, text: Int, withInfo: Boolean = false, onClick: () -> Unit = {}) = InfoItem(title, stringResource(text), withInfo, onClick) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 615afe6..7fd66b3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -7,6 +7,7 @@ Успешно Ошибка Ошибка + Succeeded Добавить Удалить Установить @@ -40,7 +41,6 @@ Определять пользователем Не поддерживается Опции - Копировать команду Имя пакета Не существует Копировать @@ -85,7 +85,6 @@ Изменить состояние пакета Предоставить разрешения Добавить делегированного админа - Dhizuku будет деактивирован Сбросить политику устройства Информация об устройстве Поддержка аттестации идентификатора устройства @@ -110,8 +109,7 @@ Краткое сообщение Подробное сообщение Передать - %1$s привилегия будет передана %2$s - Активируйте администратора устройства здесь. + %1$s привилегия будет передана %2$s Рабочий профиль успешно создан @@ -120,14 +118,6 @@ Не удалось инициализировать Dhizuku Разрешение Dhizuku не предоставлено Режим Dhizuku отключен - - Список владельцев - Список пользователей - Список аккаунтов - Shizuku не запущен. - Активировать владельца устройства - Активировать рабочий профиль, принадлежащий организации - Аккаунты Система diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 079afed..55e310e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -7,6 +7,7 @@ Başarılı Başarısız Başarısız Oldu + Succeeded Ekle Kaldır Yükle @@ -40,7 +41,6 @@ Kullanıcı Tarafından Karar Ver Desteklenmiyor Seçenekler - Komutu Kopyala Paket Adı Mevcut Değil Kopyala @@ -84,7 +84,6 @@ Paket Durumunu Değiştir İzinleri Ver Yetkilendirilmiş Yönetici Ekle - Dhizuku Devre Dışı Bırakılacak Cihaz Politikasını Sıfırla Cihaz Bilgisi Cihaz Kimliği Doğrulamasını Destekler @@ -110,7 +109,6 @@ Uzun Mesaj Aktar %1$s yetkisi %2$s\'e aktarılacak - Cihaz Yöneticisini Burada Etkinleştir. İş Profili Başarıyla Oluşturuldu @@ -119,14 +117,6 @@ Dhizuku Başlatılamadı Dhizuku İzni Verilmedi Dhizuku Modu Devre Dışı - - Sahip Listesi - Kullanıcı Listesi - Hesap Listesi - Shizuku Başlatılmadı. - Cihaz Sahibini Etkinleştir - Kuruluşa Ait İş Profilini Etkinleştir - Hesaplar Sistem diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 979865a..723b63d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -7,6 +7,7 @@ 成功 失败 失败 + 成功 添加 移除 安装 @@ -40,7 +41,6 @@ 由用户决定 不支持 选项 - 复制代码 复制 文件不存在 IO异常 @@ -83,7 +83,6 @@ 授予权限 添加委托管理员 重置设备策略 - Dhizuku将被停用 设备信息 支持设备ID认证 支持唯一设备认证 @@ -107,7 +106,6 @@ 提供支持的长消息 转移 %1$s 特权将被转移至 %2$s - 在这里激活Device admin 创建工作资料成功 @@ -117,15 +115,6 @@ Dhizuku未授权 Dhizuku模式已禁用 - - 列出Owners - 列出用户 - 列出账号 - 服务未启动 - 激活Device owner - 激活由组织拥有的工作资料 - 账号 - 系统 禁用相机 @@ -555,7 +544,7 @@ 设置 显示危险功能 - Material you 颜色 + Material You 颜色 深色主题 跟随系统 黑色主题 @@ -695,4 +684,11 @@ 如果设置为大于零的值,将会在输入一定次数的错误的锁屏密码后清除当前用户的所有数据 设置后,用户将无法输入与历史记录中任何密码相同的新密码。当前密码将保留,直到用户设置新密码为止,因此更改不会立即生效。值为0表示不做限制。 如果用户在这段时间内没有使用强认证(密码、PIN或图案)解锁设备,则要求使用强认证解锁设备。值为0表示OwnDroid不参与控制超时。一般来说,最少1小时,最多72小时。 + OwnDroid will lost its privilege + + 选择一个工作模式 + 推荐 + 激活方法 + ADB命令 + 此应用使用Device owner和Profile owner特权。这些特权十分危险,请谨慎使用。如果操作不当,可能会造成严重损失。开发者将不会对此负责。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 358fbb9..33de75e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Enable Success Failure + Succeeded Failed Add Remove @@ -42,7 +43,6 @@ Decide by user Unsupported Options - Copy Command Package name Not exist Copy @@ -87,10 +87,7 @@ Change package state Grant permissions Add delegated admin - Dhizuku will be deactivated Reset device policy - dpm set-profile-owner --user %1$s com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver - dpm set-device-owner com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver Device info Support Device ID attestation Support unique device attestation @@ -115,7 +112,6 @@ Long message Transfer %1$s privilege will be transferred to %2$s - Activate Device admin here. Create work profile success @@ -125,16 +121,8 @@ Failed to initialize Dhizuku Dhizuku permission not granted Dhizuku mode disabled - + Shizuku - List owners - List users - List accounts - Shizuku not started. - dpm set-device-owner com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver - Activate Device owner - Activate organization-owned work profile - Accounts System @@ -371,9 +359,6 @@ Account name Keep account Organization work profile - - dpm mark-profile-owner-on-organization-owned-device --user %1$s com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver - Skip encryption Create Suspend personal app @@ -592,7 +577,7 @@ Settings Show dangerous features - Material you color + Material You color Dark theme Follow system Black theme @@ -733,4 +718,11 @@ Setting this to a value greater than zero enables a policy that will wipe this user after too many incorrect unlock password have been entered. After setting this, the user will not be able to enter a new password that is the same as any password in the history. Note that the current password will remain until the user has set a new one, so the change does not take place immediately.\nA value of 0 means there is no restriction. Determine for how long the user will be able to use secondary, non strong auth for authentication, since last strong method authentication (password, pin or pattern) was used. After the returned timeout the user is required to use strong authentication method.\nA value of 0 means the admin is not participating in controlling the timeout. The minimum and maximum timeouts are platform-defined and are typically 1 hour and 72 hours, respectively. + OwnDroid will lost its privilege + + Choose a work mode + Recommended + Activate method + ADB command + This app uses Device owner and Profile owner privileges. These privileges are extremely dangerous, please use them with caution. If used improperly, they may result in severe losses. The developers will not be responsible for this. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07c0b3d..8dbbba2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,13 +3,14 @@ agp = "8.9.1" kotlin = "2.1.20" navigation-compose = "2.8.9" -composeBom = "2025.03.01" +composeBom = "2025.04.00" accompanist-drawablepainter = "0.35.0-alpha" accompanist-permissions = "0.37.0" shizuku = "13.1.5" fragment = "1.8.6" dhizuku = "2.5.3" hiddenApiBypass = "4.3" +libsu = "6.0.0" serialization = "1.7.3" [libraries] @@ -19,6 +20,7 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-activity-compose = { module = "androidx.activity:activity-compose" } androidx-material3 = { module = "androidx.compose.material3:material3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" } accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist-permissions" } @@ -27,7 +29,7 @@ shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizu shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } -androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" } +libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6392ad1..1de6ece 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://jitpack.io") } }