From 5cef8eb1934035f282b0da659c479a531a1815e4 Mon Sep 17 00:00:00 2001 From: BinTianqi <1220958406@qq.com> Date: Sun, 25 Feb 2024 15:27:51 +0800 Subject: [PATCH] support become device owner with shizuku --- Guide.md | 14 +- app/build.gradle.kts | 2 +- app/src/main/assets/rish.sh | 21 -- .../com/binbin/androidowner/MainActivity.kt | 1 - .../com/binbin/androidowner/ManagedProfile.kt | 6 +- .../com/binbin/androidowner/Permissions.kt | 16 +- .../binbin/androidowner/ShizukuActivate.kt | 257 +++++++++++------- 7 files changed, 184 insertions(+), 133 deletions(-) diff --git a/Guide.md b/Guide.md index eb0f812..2f30b1b 100644 --- a/Guide.md +++ b/Guide.md @@ -142,14 +142,22 @@ adb shell dpm remove-active-admin com.binbin.androidowner/com.binbin.androidowne ### Shizuku -可以用来 +请自己学习如何启动[Shizuku](https://github.com/RikkaApps/Shizuku) + +检查权限时如果返回“请更新Shizuku”,说明你的Shizuku版本小于v11,建议更新Shizuku。如果你的安卓版本不支持新的Shizuku,你仍然可以尝试使用下面这些功能 + +功能: - 激活Device admin - 激活Profile owner - 激活Device admin -- 激活由组织拥有的工作资料 +- 激活[由组织拥有的工作资料](#由组织拥有的工作资料) -局限性:不能在工作资料中使用 +Shizuku的本质是ADB。在安卓10或以下,你还是要连接电脑激活Shizuku + +不能在非主用户中使用,即使检查权限返回“已授权” + +因为作者懒得研究Shizuku-API,所以直接套壳了rish。因为是套壳的rish,所以不支持Sui ### 设备唯一标识码 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3218533..3d9de24 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,5 +73,5 @@ dependencies { androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") - debugImplementation("androidx.compose.ui:ui-test-manifest") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.0") } \ No newline at end of file diff --git a/app/src/main/assets/rish.sh b/app/src/main/assets/rish.sh index c944001..c3dcac4 100644 --- a/app/src/main/assets/rish.sh +++ b/app/src/main/assets/rish.sh @@ -1,26 +1,5 @@ #!/system/bin/sh BASEDIR=$(dirname "$0") DEX="$BASEDIR"/rish_shizuku.dex - -if [ ! -f "$DEX" ]; then - echo "Cannot find $DEX, please check the tutorial in Shizuku app" - exit 1 -fi - -if [ $(getprop ro.build.version.sdk) -ge 34 ]; then - if [ -w $DEX ]; then - echo "On Android 14+, app_process cannot load writable dex." - echo "Attempting to remove the write permission..." - echo "上面那两行是Shizuku的提示,可以忽略" - echo "" - chmod 400 $DEX - fi - if [ -w $DEX ]; then - echo "Cannot remove the write permission of $DEX." - echo "You can copy to file to terminal app's private directory (/data/data/, so that remove write permission is possible" - exit 1 - fi -fi - [ -z "$RISH_APPLICATION_ID" ] && export RISH_APPLICATION_ID="com.binbin.androidowner" /system/bin/app_process -Djava.class.path="$DEX" /system/bin --nice-name=rish rikka.shizuku.shell.ShizukuShellLoader "$@" diff --git a/app/src/main/java/com/binbin/androidowner/MainActivity.kt b/app/src/main/java/com/binbin/androidowner/MainActivity.kt index 2af8ee0..e087b84 100644 --- a/app/src/main/java/com/binbin/androidowner/MainActivity.kt +++ b/app/src/main/java/com/binbin/androidowner/MainActivity.kt @@ -104,7 +104,6 @@ class MainActivity : ComponentActivity() { MyScaffold() } } - deleteRish(applicationContext) } } diff --git a/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt b/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt index a462553..3bfc71f 100644 --- a/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt +++ b/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt @@ -64,7 +64,7 @@ fun ManagedProfile() { Text("跳转至个人应用") } }else{ - if(!myDpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE)&&!isDeviceOwner(myDpm)&&!isProfileOwner(myDpm)){ + if(!myDpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE)&&!isDeviceOwner(myDpm)){ Button( onClick = { myContext.startActivity(Intent("com.binbin.androidowner.MAIN_ACTION")) }, modifier = Modifier.fillMaxWidth() ){ @@ -115,7 +115,7 @@ fun ManagedProfile() { } } - if(VERSION.SDK_INT>=30&&isProfileOwner(myDpm)&&myDpm.isOrganizationOwnedDeviceWithManagedProfile){ + if(VERSION.SDK_INT>=30&&isProfileOwner(myDpm)&&myDpm.isManagedProfile(myComponent)&&myDpm.isOrganizationOwnedDeviceWithManagedProfile){ Row(modifier = sections(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically){ var suspended by remember{mutableStateOf(false)} suspended = myDpm.getPersonalAppsSuspendedReasons(myComponent)!=PERSONAL_APPS_NOT_SUSPENDED @@ -130,7 +130,7 @@ fun ManagedProfile() { } } - if(VERSION.SDK_INT>=30&&isProfileOwner(myDpm)&&myDpm.isOrganizationOwnedDeviceWithManagedProfile){ + if(VERSION.SDK_INT>=30&&isProfileOwner(myDpm)&&myDpm.isManagedProfile(myComponent)&&myDpm.isOrganizationOwnedDeviceWithManagedProfile){ Column(modifier = sections()){ var time by remember{mutableStateOf("")} time = myDpm.getManagedProfileMaximumTimeOff(myComponent).toString() diff --git a/app/src/main/java/com/binbin/androidowner/Permissions.kt b/app/src/main/java/com/binbin/androidowner/Permissions.kt index 6a80680..1b9a837 100644 --- a/app/src/main/java/com/binbin/androidowner/Permissions.kt +++ b/app/src/main/java/com/binbin/androidowner/Permissions.kt @@ -55,7 +55,7 @@ fun DpmPermissions(navCtrl:NavHostController){ modifier = sections(onClick = {navCtrl.navigate("ShizukuActivate")}, clickable = true), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ){ - Text(text = "Shizuku", style = typography.titleLarge, color = titleColor) + Text(text = "Shizuku", style = typography.titleLarge, color = titleColor, modifier = Modifier.padding(vertical = 2.dp)) Icon(imageVector = Icons.Default.KeyboardArrowRight,contentDescription = null, tint = colorScheme.onPrimaryContainer) } if(!myDpm.isAdminActive(myComponent)&&isWear){ @@ -74,13 +74,15 @@ fun DpmPermissions(navCtrl:NavHostController){ } if(!isWear) if(isda){ - Button( - onClick = { - myDpm.removeActiveAdmin(myComponent) - navCtrl.navigateUp() + if(!isDeviceOwner(myDpm)&&!isProfileOwner(myDpm)){ + Button( + onClick = { + myDpm.removeActiveAdmin(myComponent) + navCtrl.navigateUp() + } + ) { + Text("撤销") } - ) { - Text("撤销") } }else{ Button(onClick = { activateDeviceAdmin(myContext,myComponent) }) { diff --git a/app/src/main/java/com/binbin/androidowner/ShizukuActivate.kt b/app/src/main/java/com/binbin/androidowner/ShizukuActivate.kt index c6705f9..7cb93aa 100644 --- a/app/src/main/java/com/binbin/androidowner/ShizukuActivate.kt +++ b/app/src/main/java/com/binbin/androidowner/ShizukuActivate.kt @@ -5,9 +5,12 @@ import android.content.ComponentName import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.pm.PackageManager +import android.os.Binder import android.os.Build.VERSION import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.focusable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* @@ -32,12 +35,10 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay import org.apache.commons.io.IOUtils import rikka.shizuku.Shizuku -import java.io.BufferedReader -import java.io.ByteArrayInputStream -import java.io.File -import java.io.InputStreamReader +import java.io.* @Composable fun ShizukuActivate(){ @@ -49,49 +50,79 @@ fun ShizukuActivate(){ val isWear = sharedPref.getBoolean("isWear",false) val bodyTextStyle = if(isWear){ typography.bodyMedium }else{ typography.bodyLarge } val filesDir = myContext.filesDir - Column(modifier = Modifier.verticalScroll(rememberScrollState())){ + var launchExtractRish by remember{mutableStateOf(false)} + LaunchedEffect(launchExtractRish){ if(launchExtractRish){ extractRish(myContext);launchExtractRish=false } } + val scrollState = rememberScrollState() + Column(modifier = Modifier.verticalScroll(scrollState)){ var outputText by remember{mutableStateOf("")} - if(VERSION.SDK_INT>=30&&isProfileOwner(myDpm)&&myDpm.isManagedProfile(myComponent)){ + if(Binder.getCallingUid()/100000!=0){ Row(modifier = sections(colorScheme.errorContainer), verticalAlignment = Alignment.CenterVertically){ Icon(imageVector = Icons.Rounded.Warning, contentDescription = null, tint = colorScheme.onErrorContainer) - Text(text = "暂不支持在工作资料中使用Shizuku", style = bodyTextStyle, color = colorScheme.onErrorContainer) - } - } - Button(onClick = {outputText=checkPermission()}, enabled = VERSION.SDK_INT>=24, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { - Text(text = "检查权限") - } - Column(modifier = sections()){ - Text(text = "激活", style = typography.titleLarge, color = colorScheme.onPrimaryContainer) - Button( - onClick = { - extractRish(myContext) - outputText = executeCommand("sh rish.sh", "dpm set-active-admin com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Device admin") - } - Button( - onClick = { - extractRish(myContext) - outputText = executeCommand("sh rish.sh", "dpm set-profile-owner com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Profile owner") - } - Button( - onClick = { - extractRish(myContext) - outputText = executeCommand("sh rish.sh", "dpm set-device-owner com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Device owner") + Text(text = "暂不支持在非主用户中使用Shizuku", style = bodyTextStyle, color = colorScheme.onErrorContainer) } } - if(VERSION.SDK_INT>=30&&!myDpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)){ + var launchPermissionCheck by remember{mutableStateOf(false)} + LaunchedEffect(launchPermissionCheck){ + if(launchPermissionCheck){ + outputText = checkPermission() + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + launchPermissionCheck=false + } + } + Button( + onClick = {launchPermissionCheck=true}, + enabled = VERSION.SDK_INT>=24, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) + ) { + Text(text = "检查权限") + } + + if(!isDeviceOwner(myDpm)&&!isProfileOwner(myDpm)){ + Column(modifier = sections()){ + Text(text = "激活", style = typography.titleLarge, color = colorScheme.onPrimaryContainer) + + if(!myDpm.isAdminActive(myComponent)){ + var launchActivateDA by remember{mutableStateOf(false)} + LaunchedEffect(launchActivateDA){ + if(launchActivateDA){ + outputText = executeCommand("sh rish.sh", "dpm set-active-admin com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir) + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + launchActivateDA=false + } + } + Button(onClick = {launchActivateDA=true}, modifier = Modifier.fillMaxWidth()) { + Text(text = "Device admin") + } + } + + var launchActivatePO by remember{mutableStateOf(false)} + LaunchedEffect(launchActivatePO){ + if(launchActivatePO){ + outputText = executeCommand("sh rish.sh", "dpm set-profile-owner com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir) + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + launchActivatePO=false + } + } + Button(onClick = {launchActivatePO=true}, modifier = Modifier.fillMaxWidth()) { + Text(text = "Profile owner") + } + + var launchActivateDO by remember{mutableStateOf(false)} + LaunchedEffect(launchActivateDO){ + if(launchActivateDO){ + outputText = executeCommand("sh rish.sh", "dpm set-device-owner com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir) + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + launchActivateDO=false + } + } + Button(onClick = {launchActivateDO=true}, modifier = Modifier.fillMaxWidth()) { + Text(text = "Device owner") + } + + } + } + + if(VERSION.SDK_INT>=30&&!isDeviceOwner(myDpm)&&!myDpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)&&!myDpm.isOrganizationOwnedDeviceWithManagedProfile){ Column(modifier = sections()){ Text(text = "组织拥有工作资料", style = typography.titleLarge, color = colorScheme.onPrimaryContainer) Text(text = "请输入工作资料的UserID", style = bodyTextStyle) @@ -103,14 +134,22 @@ fun ShizukuActivate(){ keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus()}), modifier = Modifier.focusable().fillMaxWidth().padding(vertical = 2.dp) ) - Button( - onClick = { - extractRish(myContext) + var launchActivateOrgProfile by remember{mutableStateOf(false)} + LaunchedEffect(launchActivateOrgProfile){ + if(launchActivateOrgProfile){ + focusMgr.clearFocus() outputText = executeCommand( "sh rish.sh", "dpm mark-profile-owner-on-organization-owned-device --user $inputUserID com.binbin.androidowner/com.binbin.androidowner.MyDeviceAdminReceiver", null, filesDir ) + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + launchActivateOrgProfile=false + } + } + Button( + onClick = { + launchActivateOrgProfile=true if(myDpm.isOrganizationOwnedDeviceWithManagedProfile){ Toast.makeText(myContext,"成功",Toast.LENGTH_SHORT).show() } @@ -121,82 +160,106 @@ fun ShizukuActivate(){ } } } + + var launchListOwners by remember{mutableStateOf(false)} + LaunchedEffect(launchListOwners){ + if(launchListOwners){ + outputText=executeCommand("sh rish.sh","dpm list-owners",null,filesDir) + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + launchListOwners=false + } + } Button( - onClick = { - extractRish(myContext) - outputText = executeCommand("sh rish.sh", "pwd", null, filesDir) - }, - modifier = Modifier.fillMaxWidth() + onClick = {launchListOwners=true}, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) ) { - Text(text = "test") + Text(text = "列出Owners") } - SelectionContainer(modifier = Modifier.padding(3.dp)){ - Text(text = outputText, style = bodyTextStyle, softWrap = false, modifier = Modifier.horizontalScroll(rememberScrollState())) + + var launchTest by remember{mutableStateOf(false)} + LaunchedEffect(launchTest){ + if(launchTest){ + outputText="下面应该出现一行包含“2000”或“0”的文本\n" + scrollState.animateScrollTo(scrollState.maxValue, scrollAnim()) + outputText+=executeCommand("sh rish.sh","id",null,filesDir) + launchTest=false + } } + Button( + onClick = {launchTest=true}, modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text(text = "测试rish") + } + + SelectionContainer(modifier = Modifier.horizontalScroll(rememberScrollState())){ + Text(text = outputText, style = bodyTextStyle, softWrap = false, modifier = Modifier.padding(4.dp)) + } + Spacer(Modifier.padding(vertical = 30.dp)) } } +@Stable +fun scrollAnim( + dampingRatio: Float = Spring.DampingRatioNoBouncy, + stiffness: Float = Spring.StiffnessMedium, + visibilityThreshold: T? = null +): SpringSpec = SpringSpec(dampingRatio, stiffness, visibilityThreshold) + fun extractRish(myContext:Context){ val assetsMgr = myContext.assets - val fileList = myContext.fileList() - if("rish.sh" !in fileList){ - val shInput = assetsMgr.open("rish.sh") - val shOutput = myContext.openFileOutput("rish.sh",MODE_PRIVATE) - IOUtils.copy(shInput,shOutput) - shOutput.close() - } - if("rish_shizuku.dex" !in fileList){ - val dexInput = assetsMgr.open("rish_shizuku.dex") - val dexOutput = myContext.openFileOutput("rish_shizuku.dex",MODE_PRIVATE) - IOUtils.copy(dexInput,dexOutput) - dexOutput.close() - } -} - -fun deleteRish(myContext: Context){ myContext.deleteFile("rish.sh") myContext.deleteFile("rish_shizuku.dex") + val shInput = assetsMgr.open("rish.sh") + val shOutput = myContext.openFileOutput("rish.sh",MODE_PRIVATE) + IOUtils.copy(shInput,shOutput) + shOutput.close() + val dexInput = assetsMgr.open("rish_shizuku.dex") + val dexOutput = myContext.openFileOutput("rish_shizuku.dex",MODE_PRIVATE) + IOUtils.copy(dexInput,dexOutput) + dexOutput.close() + if(VERSION.SDK_INT>=34){ Runtime.getRuntime().exec("chmod 400 rish_shizuku.dex",null,myContext.filesDir) } } private fun checkPermission():String { - if(Shizuku.isPreV11()) { - return "有可能不支持v11以下的Shizuku\n你仍然可以尝试使用这些功能" + return if(Shizuku.isPreV11()) { + "请更新Shizuku" }else{ - try { + try{ if(Shizuku.checkSelfPermission()==PackageManager.PERMISSION_GRANTED) { - val permission = when(Shizuku.getUid()){ - 0->"Root" - 2000->"Shell" - else->"未知权限" - } - return "Shizuku v${Shizuku.getVersion()}\n已授权($permission)" - } else if(Shizuku.shouldShowRequestPermissionRationale()) { - return "用户拒绝" - } else { - Shizuku.requestPermission(0) - } - } catch(e: Throwable) { - return "服务未启动" - } + val permission = when(Shizuku.getUid()){ 0->"Root"; 2000->"Shell"; else->"未知权限" } + "Shizuku v${Shizuku.getVersion()}\n已授权($permission)" + }else if(Shizuku.shouldShowRequestPermissionRationale()){ "用户拒绝" } + else{ Shizuku.requestPermission(0); "请求授权" } + }catch(e: Throwable){ "服务未启动" } } - return "未知" } -fun executeCommand(command: String, subCommand:String, env: Array?, dir:File): String { - val output = StringBuilder() +fun executeCommand(command: String, subCommand:String, env: Array?, dir:File?): String { + var result = "" + val tunnel:ByteArrayInputStream + val process:Process + val outputStream:OutputStream try { - val tunnel = ByteArrayInputStream(subCommand.toByteArray()) - val process = Runtime.getRuntime().exec(command,env,dir) - val outputStream = process.outputStream + tunnel = ByteArrayInputStream(subCommand.toByteArray()) + process = Runtime.getRuntime().exec(command,env,dir) + outputStream = process.outputStream IOUtils.copy(tunnel,outputStream) outputStream.close() - process.waitFor() - val reader = BufferedReader(InputStreamReader(process.inputStream)) - var line: String - while(reader.readLine().also {line = it}!=null) { output.append(line+"\n") } - } catch(e: Exception) { + val exitCode = process.waitFor() + if(exitCode!=0){ result+="出错了!退出码:$exitCode" } + }catch(e:Exception){ + e.printStackTrace() + return e.toString() + } + try { + val outputReader = BufferedReader(InputStreamReader(process.inputStream)) + var outputLine: String + while(outputReader.readLine().also {outputLine = it}!=null) { result+="$outputLine\n" } + val errorReader = BufferedReader(InputStreamReader(process.errorStream)) + var errorLine: String + while(errorReader.readLine().also {errorLine = it}!=null) { result+="$errorLine\n" } + } catch(e: NullPointerException) { e.printStackTrace() } - return output.toString() + return result }