背景
工作这么多年了,我常看到我身旁的程序员在弄需要访问一个文件夹启动个su的shell,chmod 777 somefolder,开放某个文件夹的读写权限。功能是实现了,但是想想,当执行root操作时,super user之类的防御软件弹出对话框警示,而用户选择了信任yourApp后,你把手机中某个文件夹的权限更改为777,然后任何其他app都可以无障碍地访问一些不该访问的文件时,你就不会觉得内疚?在很短的时间内更改某一个特定的文件的权限,访问完成后改回来,或许还可以接受,但是整个文件夹777,那也太夸张了。
其实只要在后台启动个shell,运行su,然后在这个shell上输入你的命令行就ok了。更何况已经有现成的库,有简单易懂的demo了。libsuperuser( http://su.chainfire.eu/)就是这样的一个库。
使用方式
方式一:普通的后台操作
List<String> Shell.SH.run(String command)
List<String> Shell.SH.run(List<String> commands)
List<String> Shell.SH.run(String[] commands)
方式二:root型操作
List<String> Shell.SU.run(String command)
List<String> Shell.SU.run(List<String> commands)
List<String> Shell.SU.run(String[] commands)
方式三:
Shell.Interactive.addCommand系列函数,支持回调函数OnCommandResultListener和OnCommandLineListener
private void sendRootCommand() { rootSession.addCommand(new String[] { "id", "date", "ls -l /" }, 0, new Shell.OnCommandResultListener() { public void onCommandResult(int commandCode, int exitCode, List<String> output) { if (exitCode < 0) { reportError("Error executing commands: exitCode " + exitCode); } else { updateResultStatus(true, output); appendLineToOutput("----------"); appendLineToOutput("ls -l /"); } } });
使用时注意要在辅助线程中调用,比如AsyncTask,再比如IntentService
功能实现的核心在下面代码段
public static List<String> run(String shell, String[] commands, String[] environment, boolean wantSTDERR) { ... // setup our process, retrieve STDIN stream, and STDOUT/STDERR // gobblers Process process = Runtime.getRuntime().exec(shell, environment); DataOutputStream STDIN = new DataOutputStream(process.getOutputStream()); StreamGobbler STDOUT = new StreamGobbler(shellUpper + "-", process.getInputStream(), res); StreamGobbler STDERR = new StreamGobbler(shellUpper + "*", process.getErrorStream(), wantSTDERR ? res : null); // start gobbling and write our commands to the shell // STDOUT和STDERR是两个工作线程,用来后台进程的输出 STDOUT.start(); STDERR.start(); try { for (String write : commands) { Debug.logCommand(String.format("[%s+] %s", shellUpper, write)); STDIN.write((write + "\n").getBytes("UTF-8")); STDIN.flush(); } STDIN.write("exit\n".getBytes("UTF-8")); STDIN.flush(); } catch (IOException e) { if (e.getMessage().contains("EPIPE")) { // method most horrid to catch broken pipe, in which case we // do nothing. the command is not a shell, the shell closed // STDIN, the script already contained the exit command, etc. // these cases we want the output instead of returning null } else { // other issues we don‘t know how to handle, leads to // returning null throw e; } } // wait for our process to finish, while we gobble away in the // background process.waitFor(); // make sure our threads are done gobbling, our streams are closed, // and the process is destroyed - while the latter two shouldn‘t be // needed in theory, and may even produce warnings, in "normal" Java // they are required for guaranteed cleanup of resources, so lets be // safe and do this on Android as well try { STDIN.close(); } catch (IOException e) { // might be closed already } STDOUT.join(); STDERR.join(); process.destroy(); // in case of su, 255 usually indicates access denied if (SU.isSU(shell) && (process.exitValue() == 255)) { res = null; } } catch (IOException e) { // shell probably not found res = null; } catch (InterruptedException e) { // this should really be re-thrown res = null; } Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); return res; }
结语
我也碰到过一个同事在做应用市场app的时候,要实现“静默安装”的功能,apk下载完毕在后台静默安装app,就是也就是在su的shell中执行pm install及一些其它的辅助功能,然后捣整了好久才问我,为什么在DeviceA上行得通,在DeviceB上就不行了。我告诉他有成熟的实现方式了,回头一试,OK了。有成熟的实现方式就直接拿来用吧,或者阅读这些代码,自己提取关键代码。