├── .gitignore ├── .idea ├── .gitignore ├── appInsightsSettings.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── dictionaries │ └── xxin.xml ├── encodings.xml ├── gradle.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── xyz │ │ └── xxin │ │ └── adbshellutils │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── xyz │ │ │ └── xxin │ │ │ └── adbshellutils │ │ │ └── IUserService.aidl │ ├── java │ │ └── xyz │ │ │ └── xxin │ │ │ └── adbshellutils │ │ │ ├── MainActivity.java │ │ │ └── UserService.java │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── xyz │ └── xxin │ └── adbshellutils │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/dictionaries/xxin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | app连接shizuku服务,执行shell命令 2 | =============================== 3 | 4 | 注意: 5 | 6 | 1. 使用前请确保已经安装了`Shizuku`并授予了`shell`权限 7 | 2. 并不能执行所有`shell`命令,如`cd` 8 | 3. 当执行的命令中,路径带有空格时,使用双引号包裹,如`ls "/temp dir/"` 9 | 10 | 11 | 详细开发过程记录在:https://blog.xxin.xyz/2024/04/28/Shizuku%E5%BC%80%E5%8F%91/ 12 | 13 | 参考: 14 | 15 | Shizuku开源地址:https://github.com/RikkaApps/Shizuku 16 | 17 | Shizuku开发指南:https://github.com/RikkaApps/Shizuku-API -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | namespace 'xyz.xxin.adbshellutils' 7 | compileSdk 34 8 | 9 | defaultConfig { 10 | applicationId "xyz.xxin.adbshellutils" 11 | minSdk 24 12 | targetSdk 34 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildFeatures { 20 | buildConfig true // 开启BuildConfig类的生成 21 | aidl true // 启用aidl 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | implementation 'androidx.appcompat:appcompat:1.6.1' 39 | implementation 'com.google.android.material:material:1.11.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 41 | testImplementation 'junit:junit:4.13.2' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 44 | 45 | def shizuku_version = "13.1.5" 46 | implementation "dev.rikka.shizuku:api:$shizuku_version" 47 | implementation "dev.rikka.shizuku:provider:$shizuku_version" 48 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/xyz/xxin/adbshellutils/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package xyz.xxin.adbshellutils; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("xyz.xxin.adbshellutils", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 16 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/aidl/xyz/xxin/adbshellutils/IUserService.aidl: -------------------------------------------------------------------------------- 1 | package xyz.xxin.adbshellutils; 2 | 3 | interface IUserService { 4 | 5 | /** 6 | * Shizuku服务端定义的销毁方法 7 | */ 8 | void destroy() = 16777114; 9 | 10 | /** 11 | * 自定义的退出方法 12 | */ 13 | void exit() = 1; 14 | 15 | /** 16 | * 执行命令 17 | */ 18 | String execLine(String command) = 2; 19 | 20 | /** 21 | * 执行数组中分离的命令 22 | */ 23 | String execArr(in String[] command) = 3; 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/xxin/adbshellutils/MainActivity.java: -------------------------------------------------------------------------------- 1 | package xyz.xxin.adbshellutils; 2 | 3 | import android.content.ComponentName; 4 | import android.content.ServiceConnection; 5 | import android.content.pm.PackageManager; 6 | import android.os.Bundle; 7 | import android.os.IBinder; 8 | import android.os.RemoteException; 9 | import android.text.TextUtils; 10 | import android.widget.Button; 11 | import android.widget.EditText; 12 | import android.widget.TextView; 13 | import android.widget.Toast; 14 | 15 | import androidx.appcompat.app.AppCompatActivity; 16 | 17 | import java.util.ArrayList; 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | 21 | import rikka.shizuku.Shizuku; 22 | 23 | public class MainActivity extends AppCompatActivity { 24 | private final static int PERMISSION_CODE = 10001; 25 | private boolean shizukuServiceState = false; 26 | private Button judge_permission; 27 | private Button request_permission; 28 | private Button connect_shizuku; 29 | private Button execute_command; 30 | private EditText input_command; 31 | private TextView execute_result; 32 | private IUserService iUserService; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | findView(); 40 | addEvent(); 41 | initShizuku(); 42 | } 43 | 44 | 45 | private void initShizuku() { 46 | // 添加权限申请监听 47 | Shizuku.addRequestPermissionResultListener(onRequestPermissionResultListener); 48 | 49 | // Shiziku服务启动时调用该监听 50 | Shizuku.addBinderReceivedListenerSticky(onBinderReceivedListener); 51 | 52 | // Shiziku服务终止时调用该监听 53 | Shizuku.addBinderDeadListener(onBinderDeadListener); 54 | } 55 | 56 | @Override 57 | protected void onDestroy() { 58 | super.onDestroy(); 59 | // 移除权限申请监听 60 | Shizuku.removeRequestPermissionResultListener(onRequestPermissionResultListener); 61 | 62 | Shizuku.removeBinderReceivedListener(onBinderReceivedListener); 63 | 64 | Shizuku.removeBinderDeadListener(onBinderDeadListener); 65 | 66 | Shizuku.unbindUserService(userServiceArgs, serviceConnection, true); 67 | } 68 | 69 | private final Shizuku.OnBinderReceivedListener onBinderReceivedListener = () -> { 70 | shizukuServiceState = true; 71 | Toast.makeText(MainActivity.this, "Shizuku服务已启动", Toast.LENGTH_SHORT).show(); 72 | }; 73 | 74 | private final Shizuku.OnBinderDeadListener onBinderDeadListener = () -> { 75 | shizukuServiceState = false; 76 | iUserService = null; 77 | Toast.makeText(MainActivity.this, "Shizuku服务被终止", Toast.LENGTH_SHORT).show(); 78 | }; 79 | 80 | private void addEvent() { 81 | // 判断权限 82 | judge_permission.setOnClickListener(view -> { 83 | if (!shizukuServiceState) { 84 | Toast.makeText(this, "Shizuku服务状态异常", Toast.LENGTH_SHORT).show(); 85 | return; 86 | } 87 | 88 | if (checkPermission()) { 89 | Toast.makeText(this, "已拥有权限", Toast.LENGTH_SHORT).show(); 90 | } else { 91 | Toast.makeText(this, "未拥有权限", Toast.LENGTH_SHORT).show(); 92 | } 93 | }); 94 | 95 | // 动态申请权限 96 | request_permission.setOnClickListener(view -> { 97 | if (!shizukuServiceState) { 98 | Toast.makeText(this, "Shizuku服务状态异常", Toast.LENGTH_SHORT).show(); 99 | return; 100 | } 101 | 102 | requestShizukuPermission(); 103 | }); 104 | 105 | // 连接Shizuku服务 106 | connect_shizuku.setOnClickListener(view -> { 107 | if (!shizukuServiceState) { 108 | Toast.makeText(this, "Shizuku服务状态异常", Toast.LENGTH_SHORT).show(); 109 | return; 110 | } 111 | 112 | if (!checkPermission()) { 113 | Toast.makeText(this, "没有Shizuku权限", Toast.LENGTH_SHORT).show(); 114 | return; 115 | } 116 | 117 | if (iUserService != null) { 118 | Toast.makeText(MainActivity.this, "已连接Shizuku服务", Toast.LENGTH_SHORT).show(); 119 | return; 120 | } 121 | 122 | // 绑定shizuku服务 123 | Shizuku.bindUserService(userServiceArgs, serviceConnection); 124 | }); 125 | 126 | // 执行命令 127 | execute_command.setOnClickListener(view -> { 128 | if (iUserService == null) { 129 | Toast.makeText(this, "请先连接Shizuku服务", Toast.LENGTH_SHORT).show(); 130 | return; 131 | } 132 | 133 | String command = input_command.getText().toString().trim(); 134 | 135 | // 命令不能为空 136 | if (TextUtils.isEmpty(command)) { 137 | Toast.makeText(this, "命令不能为空", Toast.LENGTH_SHORT).show(); 138 | return; 139 | } 140 | 141 | try { 142 | // 执行命令,返回执行结果 143 | String result = exec(command); 144 | 145 | if (result == null) { 146 | result = "返回结果为null"; 147 | } else if (TextUtils.isEmpty(result.trim())) { 148 | result = "返回结果为空"; 149 | } 150 | 151 | // 将执行结果显示 152 | execute_result.setText(result); 153 | } catch (Exception e) { 154 | execute_result.setText(e.toString()); 155 | e.printStackTrace(); 156 | } 157 | }); 158 | } 159 | 160 | private String exec(String command) throws RemoteException { 161 | // 检查是否存在包含任意内容的双引号 162 | Pattern pattern = Pattern.compile("\"([^\"]*)\""); 163 | Matcher matcher = pattern.matcher(command); 164 | 165 | // 下面展示了两种不同的命令执行方法 166 | if (matcher.find()) { 167 | ArrayList list = new ArrayList<>(); 168 | Pattern pattern2 = Pattern.compile("\"([^\"]*)\"|(\\S+)"); 169 | Matcher matcher2 = pattern2.matcher(command); 170 | 171 | while (matcher2.find()) { 172 | if (matcher2.group(1) != null) { 173 | // 如果是引号包裹的内容,取group(1) 174 | list.add(matcher2.group(1)); 175 | } else { 176 | // 否则取group(2),即普通的单词 177 | list.add(matcher2.group(2)); 178 | } 179 | } 180 | 181 | // 这种方法可用于执行路径中带空格的命令,例如 ls /storage/0/emulated/temp dir/ 182 | // 当然也可以执行不带空格的命令,实际上是要强于另一种执行方式的 183 | return iUserService.execArr(list.toArray(new String[0])); 184 | } else { 185 | // 这种方法仅用于执行路径中不包含空格的命令,例如 ls /storage/0/emulated/ 186 | return iUserService.execLine(command); 187 | } 188 | } 189 | 190 | private final ServiceConnection serviceConnection = new ServiceConnection() { 191 | @Override 192 | public void onServiceConnected(ComponentName componentName, IBinder iBinder) { 193 | Toast.makeText(MainActivity.this, "Shizuku服务连接成功", Toast.LENGTH_SHORT).show(); 194 | 195 | if (iBinder != null && iBinder.pingBinder()) { 196 | iUserService = IUserService.Stub.asInterface(iBinder); 197 | } 198 | } 199 | 200 | @Override 201 | public void onServiceDisconnected(ComponentName componentName) { 202 | Toast.makeText(MainActivity.this, "Shizuku服务连接断开", Toast.LENGTH_SHORT).show(); 203 | iUserService = null; 204 | } 205 | }; 206 | 207 | private final Shizuku.UserServiceArgs userServiceArgs = 208 | new Shizuku.UserServiceArgs(new ComponentName(BuildConfig.APPLICATION_ID, UserService.class.getName())) 209 | .daemon(false) 210 | .processNameSuffix("adb_service") 211 | .debuggable(BuildConfig.DEBUG) 212 | .version(BuildConfig.VERSION_CODE); 213 | 214 | 215 | /** 216 | * 动态申请Shizuku adb shell权限 217 | */ 218 | private void requestShizukuPermission() { 219 | if (Shizuku.isPreV11()) { 220 | Toast.makeText(this, "当前shizuku版本不支持动态申请权限", Toast.LENGTH_SHORT).show(); 221 | return; 222 | } 223 | 224 | if (checkPermission()) { 225 | Toast.makeText(this, "已拥有Shizuku权限", Toast.LENGTH_SHORT).show(); 226 | return; 227 | } 228 | 229 | // 动态申请权限 230 | Shizuku.requestPermission(MainActivity.PERMISSION_CODE); 231 | } 232 | 233 | private final Shizuku.OnRequestPermissionResultListener onRequestPermissionResultListener = new Shizuku.OnRequestPermissionResultListener() { 234 | @Override 235 | public void onRequestPermissionResult(int requestCode, int grantResult) { 236 | boolean granted = grantResult == PackageManager.PERMISSION_GRANTED; 237 | if (granted) { 238 | Toast.makeText(MainActivity.this, "Shizuku授权成功", Toast.LENGTH_SHORT).show(); 239 | } else { 240 | Toast.makeText(MainActivity.this, "Shizuku授权失败", Toast.LENGTH_SHORT).show(); 241 | } 242 | } 243 | }; 244 | 245 | /** 246 | * 判断是否拥有shizuku adb shell权限 247 | */ 248 | private boolean checkPermission() { 249 | return Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED; 250 | } 251 | 252 | private void findView() { 253 | judge_permission = findViewById(R.id.judge_permission); 254 | request_permission = findViewById(R.id.request_permission); 255 | connect_shizuku = findViewById(R.id.connect_shizuku); 256 | execute_command = findViewById(R.id.execute_command); 257 | input_command = findViewById(R.id.input_command); 258 | execute_result = findViewById(R.id.execute_result); 259 | } 260 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/xxin/adbshellutils/UserService.java: -------------------------------------------------------------------------------- 1 | package xyz.xxin.adbshellutils; 2 | 3 | import android.os.RemoteException; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | 9 | public class UserService extends IUserService.Stub { 10 | 11 | @Override 12 | public void destroy() throws RemoteException { 13 | System.exit(0); 14 | } 15 | 16 | @Override 17 | public void exit() throws RemoteException { 18 | destroy(); 19 | } 20 | 21 | @Override 22 | public String execLine(String command) throws RemoteException { 23 | try { 24 | // 执行shell命令 25 | Process process = Runtime.getRuntime().exec(command); 26 | // 读取执行结果 27 | return readResult(process); 28 | } catch (IOException | InterruptedException e) { 29 | throw new RemoteException(); 30 | } 31 | } 32 | 33 | @Override 34 | public String execArr(String[] command) throws RemoteException { 35 | try { 36 | // 执行shell命令 37 | Process process = Runtime.getRuntime().exec(command); 38 | // 读取执行结果 39 | return readResult(process); 40 | } catch (IOException | InterruptedException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | 45 | /** 46 | * 读取执行结果,如果有异常会向上抛 47 | */ 48 | public String readResult(Process process) throws IOException, InterruptedException { 49 | StringBuilder stringBuilder = new StringBuilder(); 50 | // 读取执行结果 51 | InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream()); 52 | BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 53 | String line; 54 | while ((line = bufferedReader.readLine()) != null) { 55 | stringBuilder.append(line).append("\n"); 56 | } 57 | inputStreamReader.close(); 58 | process.waitFor(); 59 | return stringBuilder.toString(); 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 20 | 21 | 22 | 27 | 28 | 34 | 35 |