commit e43f46bdf84c773235dab300d64c2d05931e85eb Author: gtf35 Date: Sun Feb 24 00:26:11 2019 +0800 初次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5edb4ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..2272d8c Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7ac24c7 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 0000000..1cf4cf1 --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml new file mode 100644 index 0000000..57927c5 --- /dev/null +++ b/.idea/markdown-navigator/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..80c79a1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8467219 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 gtf35 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..826a45e --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# 免Root实现静默安装和点击任意位置 + +## 0 前言 + + 最近有了个需求:免 root 实现任意位置点击和静默安装。这个做过的小伙伴应该都知道正常情况下是不可能实现的。无障碍只能实现对已知控件的点击,并不能指定坐标。但是确实有人另辟蹊径做出来了,譬如做游戏手柄的飞智,他们是用一个激活器,手机开 usb 调试,然后插在激活器上并授权,飞智游戏厅就被「激活」了,然后可以实现任意位置点击。如果不了解的可以去他们官网了解下,在这里不多赘述了。无独有偶,黑域也使用了类似的手段,也可以用电脑的usb调试激活。我们知道,任意位置坐标xy点击是可以在 pc 上通过 shell 命令「input tap x y」来实现的,也不需要 root 权限。但是在应用内通过「Runtime.getRuntime().exec」执行这个 shell 命令却提示「permission denied」也就是权限不足。但是飞智或者黑域却好像使用了某种魔法,提升了自己的权限,那么问题来了:如何用 usb 调试给 app 提权? + +## 1 原理揭晓 + + 「如何用 usb 调试给 app 提权」这个问题乍一看确实没问题,但是知乎有个回答是「先问是不是,再问为什么」我觉得说的很好。我被这个问题给困扰了很久,最后发现我问错了。先放出结论「并不是给 app 提权,而是运行了一个由设立了权限的新程序」 + + 刚才的问题先放一边,我来问大家个新问题,怎样让 app 获取 root 权限?这个问题答案已经有不少了,网上一查便可知其实是获取「Runtime.getRuntime().exec」的流,在里面用su提权,然后就可以执行需要 root 权限的 shell 命令,比如挂载 system 读写,访问 data 分区,用 shell 命令静默安装,等等。话说回来,是不是和我们今天的主题有点像,如何使 app 获取 shell 权限?嗯,其实差不多,思路也类似,因为本来 root 啦, shell 啦,根本就不是 Android 应用层的名词呀,他们本来就是 Linux 里的名词,只不过是 Android 框架运行于 Linux 层之上, 我们可以调用 shell 命令,也可以在shell 里调用 su 来使shell 获取 root 权限,来绕过 Android 层做一些被限制的事。然而在 app 里调用 shell 命令,其进程还是 app 的,权限还是受限。所以就不能在 app 里运行 shell 命令,那么问题来了,不在 app 里运行在哪运行?答案是在 pc 上运行。当然不可能是 pc 一直连着手机啦,而是 pc 上在 shell 里运行独立的一个 java 程序,这个程序因为是在 shell 里启动的,所以具有 shell 权限。我们想一下,这个 Java 程序在 shell 里运行,建立本地 socket 服务器,和 app 通信,远程执行 app 下发的代码。因为即使拔掉了数据线,这个 Java 程序也不会停止,只要不重启他就一直活着,执行我们的命令,这不就是看起来 app 有了 shell 权限?现在真相大白,飞智和黑域用 usb 调试激活的那一下,其实是启动那个 Java 程序,飞智是执行模拟按键,黑域是监听系统事件,你想干啥就任你开发了。「注:黑域和飞智由于进程管理的需要,其实是先用 shell 启动一个 so ,然后再用 so 做跳板启动 Java 程序,而且 so 也充当守护进程,当 Java 意外停止可以重新启动,读着有兴趣可以自行研究,在此不多做说明」 + +## 2 好耶!是 app_process + + 那么如何具体用 shell 运行 Java 程序呢?肯定不是「java xxx.jar」啦,Android 能运行的格式是 dex 。没错,就是apk 里那个 dex 。然后我们可以通过「app_process」开启动 Java 。app_process 的参数如下 + +```shell +app_process [vm-options] cmd-dir [options] start-class-name [main-options] +``` + + 这个诡异又可怕的东西是没有 -help 的。我们要么看源码,要么看别人分析好的。本人水平有限,这里选择看别人分析好的: + +```shell +vm-options – VM 选项 +cmd-dir –父目录 (/system/bin) +options –运行的参数 : + –zygote + –start-system-server + –application (api>=14) + –nice-name=nice_proc_name (api>=14) +start-class-name –包含main方法的主类 (com.android.commands.am.Am) +main-options –启动时候传递到main方法中的参数 +``` + +## 3 实践 + +因为是 dex 我们就直接在 as 里写吧,提取 dex 也方便。新建个空白项目,初始结构是这样: + +![](http://article.gtf35.top/app_process/as%E9%BB%98%E8%AE%A4%E6%A6%82%E8%A7%88.JPG) + +我们新建个包,存放我们要在 shell 下运行的 Java 代码: + +![](http://article.gtf35.top/app_process/%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%B5%8B%E8%AF%95.JPG) + +这里我们补全 Main 方法,因为我们这个不是个 Android 程序,只是编译成 dex 的纯 Java 程序,所以我们这个的入口是 Main : + +```java +package shellService; + +public class Main { + public static void main(String[] args){ + System.out.println("我是在 shell 里运行的!!!"); + } +} +``` + +我们在代码里只是打印一行「我是在 shell 里运行的!!!」,因为这里是纯 Java 所以也用的 println。现在编译 apk: + +![](http://article.gtf35.top/app_process/%E7%BC%96%E8%AF%91%E5%87%BA%E6%9D%A5%E7%9A%84apk.JPG) + +因为 apk 就是 zip 所以我们直接解压出 apk 文件,然后执行 : + +```shell +adb push classes.dex /data/local/tmp +cd /data/local/tmp +app_process -Djava.class.path=/data/local/tmp/classes.dex /system/bin shellService.Main +``` + +这时就能看到已经成功运行啦: + +![](http://article.gtf35.top/app_process/%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C.JPG) + +这里因为 utf8 在 Windows shell 里有问题,所以乱码了,但是还是说明我们成功了。 + +##4 具有实用性 + +只能输出肯定是不行的,不具有实用性。我们之前说过,我们应该建立个本地 socket 服务器来接受命令并执行,这里的「Service」类实现了这个功能,因为如何建立 socket 不是文章的重点,所以大家只要知道这个类内部实现了一个「ServiceGetText」接口,在收到命令之后会把命令内容作为参数回掉 getText 方法,然后我们执行 shell 命令之后,吧结果作为字符串返回即可,具体实现可以看文末的 GitHub。 + +我们新建一个「ServiceThread」来运行「Service」服务和执行设立了命令: + +```java +public class ServiceThread extends Thread { + private static int ShellPORT = 4521; + + @Override + public void run() { + System.out.println(">>>>>>Shell服务端程序被调用<<<<<<"); + new Service(new Service.ServiceGetText() { + @Override + public String getText(String text) { + if (text.startsWith("###AreYouOK")){ + return "###IamOK#"; + } + try{ + ServiceShellUtils.ServiceShellCommandResult sr = ServiceShellUtils.execCommand(text, false); + if (sr.result == 0){ + return "###ShellOK#" + sr.successMsg; + } else { + return "###ShellError#" + sr.errorMsg; + } + }catch (Exception e){ + return "###CodeError#" + e.toString(); + } + } + }, ShellPORT); + } +} +``` + +其中 ServiceShellUtils 用到了开源项目 ShellUtils 在此感谢。这个类用来执行 shell 命令。 + +然后在 Main 中调用这个线程: + +```java +public class Main { + + public static void main(String[] args){ + new ServiceThread().start(); + while (true); + } + +} +``` + +这样,我们服务端就准备好了,我们来写控制服务端的 app 。我们新建类「SocketClient」用来和服务端进行通信,并在活动里调用他: + +```java + +``` + +然后重复 3 小节的操作,运行这个服务端: + +![](http://article.gtf35.top/app_process/%E6%9C%80%E5%90%8Edemo%E7%9A%84shell.JPG) + +然后安装 apk ,运行: + +```java + +``` + +![](http://article.gtf35.top/app_process/%E6%89%8B%E6%9C%BA%E8%BF%90%E8%A1%8C.gif) + +可以看到,在不 root 的情况下,成功的执行了需要 shell 权限的命令 + +##5 最可爱的人 + +最后,我真的是要由衷的感谢各种技术分析文章和开源项目,真的太感谢了,没有无条件的奉献就没有互联网这么快的进步。 + +我对 app_process 利用方法的研究离不开以下项目和前辈的汗水: + +[Brevent](https://github.com/brevent/Brevent) 最早利用app_process进程实现无 root 权限使用的开源应用(虽然已经闭源,仍然尊重并感谢 [liudongmiao](https://github.com/liudongmiao)) + +[Android system log viewer on Android phone without root.](https://github.com/Zane96/Fairy) 利用app_process进程实现无 root 权限使用的优秀开源应用 + +[Android上app_process启动java进程](https://blog.csdn.net/u010651541/article/details/53163542) 通俗易懂的教程 + +[使用 app_process 来调用高权限 API](https://haruue.moe/blog/2017/08/30/call-privileged-api-with-app-process/) 分析的很深刻的教程 \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..081dacd --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "top.gtf35.shellapplicatontest" + minSdkVersion 14 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:28.+' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/top/gtf35/shellapplicatontest/ExampleInstrumentedTest.java b/app/src/androidTest/java/top/gtf35/shellapplicatontest/ExampleInstrumentedTest.java new file mode 100644 index 0000000..963a58c --- /dev/null +++ b/app/src/androidTest/java/top/gtf35/shellapplicatontest/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package top.gtf35.shellapplicatontest; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("top.gtf35.shellapplicatontest", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..85c5f90 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/shellService/Main.java b/app/src/main/java/shellService/Main.java new file mode 100644 index 0000000..74f5c59 --- /dev/null +++ b/app/src/main/java/shellService/Main.java @@ -0,0 +1,10 @@ +package shellService; + +public class Main { + + public static void main(String[] args){ + new ServiceThread().start(); + while (true); + } + +} diff --git a/app/src/main/java/shellService/Service.java b/app/src/main/java/shellService/Service.java new file mode 100644 index 0000000..a5515c4 --- /dev/null +++ b/app/src/main/java/shellService/Service.java @@ -0,0 +1,79 @@ +package shellService; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; + +public class Service { + private ServiceGetText mServiceGetText; + + + public Service(ServiceGetText serviceGetText, int PORT) { + mServiceGetText = serviceGetText; + try { + /** 创建ServerSocket*/ + // 创建一个ServerSocket在端口4521监听客户请求 + ServerSocket serverSocket = new ServerSocket(PORT); + System.out.println("服务端运行在" + PORT + "端口"); + while (true) { + // 侦听并接受到此Socket的连接,请求到来则产生一个Socket对象,并继续执行 + Socket socket = serverSocket.accept(); + System.out.println("监听请求到来则产生一个Socket对象,并继续执行"); + new CreateServerThread(socket);//当有请求时,启一个线程处理 + } + } catch (Exception e) { + System.out.println("连接监听发生错误 Exception:" + e); + }finally{ +// serverSocket.close(); + } + } + + //线程类 + class CreateServerThread extends Thread { + Socket socket; + public CreateServerThread(Socket s) throws IOException { + System.out.println("创建了一个新的连接线程"); + socket = s; + start(); + } + + public void run() { + try { + /** 获取客户端传来的信息 */ + // 由Socket对象得到输入流,并构造相应的BufferedReader对象 + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + System.out.println("由Socket对象得到输入流,并构造相应的BufferedReader对象"); + String line = bufferedReader.readLine(); + /** 发送服务端准备传输的 */ + // 由Socket对象得到输出流,并构造PrintWriter对象 + PrintWriter printWriter = new PrintWriter(socket.getOutputStream()); + System.out.println("由Socket对象得到输出流,并构造PrintWriter对象"); + // 获取从客户端读入的字符串 + System.out.println("while循环:获取从客户端读入的字符串"); + System.out.println("while循环:客户端返回 : " + line); + String repeat = mServiceGetText.getText(line); + System.out.println("while循环:服务器将返回:" + repeat); + //printWriter.print("hello Client, I am Server!"); + printWriter.print(repeat); + System.out.println("while循环:准备刷新返回"); + printWriter.flush(); + System.out.println("while循环:已刷新返回"); + System.out.println("关闭Socket"); + /** 关闭Socket*/ + printWriter.close(); + bufferedReader.close(); + socket.close(); + } catch (IOException e) { + System.out.println("socket 连接线程发生错误:" + e.toString()); + } + } + } + + public interface ServiceGetText{ + String getText(String text); + } + +} \ No newline at end of file diff --git a/app/src/main/java/shellService/ServiceShellUtils.java b/app/src/main/java/shellService/ServiceShellUtils.java new file mode 100644 index 0000000..e679302 --- /dev/null +++ b/app/src/main/java/shellService/ServiceShellUtils.java @@ -0,0 +1,129 @@ +package shellService; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + + +public class ServiceShellUtils { + + public static final String COMMAND_SU = "su"; // 获取root权限的命令 + public static final String COMMAND_SH = "sh"; // 执行sh文件的命令 + public static final String COMMAND_EXIT = "exit\n"; // 退出的命令 + public static final String COMMAND_LINE_END = "\n"; // 执行命令必须加在末尾 + + private ServiceShellUtils() { + throw new AssertionError(); + } + //检测root状态 + public static boolean checkRootPermission() { + return execCommand("echo root", true, false).result == 0; + } + //执行单行命令,实际还是调用的执行多行 ,传入命令和是否需要root + public static ServiceShellUtils.ServiceShellCommandResult execCommand(String command, boolean isRoot) { + return execCommand(new String[]{command}, isRoot, true); + } + //执行List中的命令 , 传入List和是否需要root + public static ServiceShellUtils.ServiceShellCommandResult execCommand(List commands, boolean isRoot) { + return execCommand(commands == null ? null : commands.toArray(new String[]{}), isRoot, true); + } + //执行多行命令 + public static ServiceShellUtils.ServiceShellCommandResult execCommand(String[] commands, boolean isRoot) { + return execCommand(commands, isRoot, true); + } + + public static ServiceShellUtils.ServiceShellCommandResult execCommand(String command, boolean isRoot, boolean isNeedResultMsg) { + return execCommand(new String[]{command}, isRoot, isNeedResultMsg); + } + + public static ServiceShellUtils.ServiceShellCommandResult execCommand(List commands, boolean isRoot, boolean isNeedResultMsg) { + return execCommand(commands == null ? null : commands.toArray(new String[]{}), isRoot, isNeedResultMsg); + } + //执行命令,获得返回的信息 + public static ServiceShellUtils.ServiceShellCommandResult execCommand(String[] commands, boolean isRoot, boolean isNeedResultMsg) { + int result = -1; + if (commands == null || commands.length == 0) { + return new ServiceShellUtils.ServiceShellCommandResult(result, null, null); + } + + Process process = null; + BufferedReader successResult = null; + BufferedReader errorResult = null; + StringBuilder successMsg = null; + StringBuilder errorMsg = null; + DataOutputStream os = null; + try { + process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH); + os = new DataOutputStream(process.getOutputStream()); + for (String command : commands) { + if (command == null) { + continue; + } + os.write(command.getBytes()); + os.writeBytes(COMMAND_LINE_END); + os.flush(); + } + os.writeBytes(COMMAND_EXIT); + os.flush(); + + result = process.waitFor(); + // get command result + if (isNeedResultMsg) { + successMsg = new StringBuilder(); + errorMsg = new StringBuilder(); + successResult = new BufferedReader(new InputStreamReader(process.getInputStream())); + errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream())); + String s; + while ((s = successResult.readLine()) != null) { + successMsg.append(s); + } + while ((s = errorResult.readLine()) != null) { + errorMsg.append(s); + } + } + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (os != null) { + os.close(); + } + if (successResult != null) { + successResult.close(); + } + if (errorResult != null) { + errorResult.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (process != null) { + process.destroy(); + } + } + return new ServiceShellUtils.ServiceShellCommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null + : errorMsg.toString()); + } + //封装了返回信息 + public static class ServiceShellCommandResult { + + public int result; + public String successMsg; //成功信息 + public String errorMsg; // 错误信息 + + public ServiceShellCommandResult(int result) { + this.result = result; + } + + public ServiceShellCommandResult(int result, String successMsg, String errorMsg) { + this.result = result; + this.successMsg = successMsg; + this.errorMsg = errorMsg; + } + } +} diff --git a/app/src/main/java/shellService/ServiceThread.java b/app/src/main/java/shellService/ServiceThread.java new file mode 100644 index 0000000..f7577d8 --- /dev/null +++ b/app/src/main/java/shellService/ServiceThread.java @@ -0,0 +1,28 @@ +package shellService; + +public class ServiceThread extends Thread { + private static int ShellPORT = 4521; + + @Override + public void run() { + System.out.println(">>>>>>Shell服务端程序被调用<<<<<<"); + new Service(new Service.ServiceGetText() { + @Override + public String getText(String text) { + if (text.startsWith("###AreYouOK")){ + return "###IamOK#"; + } + try{ + ServiceShellUtils.ServiceShellCommandResult sr = ServiceShellUtils.execCommand(text, false); + if (sr.result == 0){ + return "###ShellOK#" + sr.successMsg; + } else { + return "###ShellError#" + sr.errorMsg; + } + }catch (Exception e){ + return "###CodeError#" + e.toString(); + } + } + }, ShellPORT); + } +} diff --git a/app/src/main/java/top/gtf35/shellapplicatontest/MainActivity.java b/app/src/main/java/top/gtf35/shellapplicatontest/MainActivity.java new file mode 100644 index 0000000..f3ebfe4 --- /dev/null +++ b/app/src/main/java/top/gtf35/shellapplicatontest/MainActivity.java @@ -0,0 +1,69 @@ +package top.gtf35.shellapplicatontest; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +public class MainActivity extends AppCompatActivity { + + private EditText mCmdInputEt; + private Button mRunShellBtn; + private TextView mOutputTv; + + private void initView(){ + mCmdInputEt = findViewById(R.id.et_cmd); + mRunShellBtn = findViewById(R.id.btn_runshell); + mOutputTv = findViewById(R.id.tv_output); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + initView(); + mRunShellBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String cmd = mCmdInputEt.getText().toString(); + if (TextUtils.isEmpty(cmd)) { + Toast.makeText(MainActivity.this, "输入内容为空", Toast.LENGTH_SHORT).show(); + return; + } + runShell(cmd); + } + }); + } + + private void runShell(final String cmd){ + if (TextUtils.isEmpty(cmd)) return; + new Thread(new Runnable() { + @Override + public void run() { + new SocketClient(cmd, new SocketClient.onServiceSend() { + @Override + public void getSend(String result) { + showTextOnTextView(result); + } + }); + } + }).start(); + } + + private void showTextOnTextView(final String text){ + MainActivity.this.runOnUiThread(new Runnable() { + @Override + public void run() { + if (TextUtils.isEmpty(mOutputTv.getText())) { + mOutputTv.setText(text); + } else { + mOutputTv.setText(mOutputTv.getText() + "\n" + text); + } + } + }); + } +} diff --git a/app/src/main/java/top/gtf35/shellapplicatontest/SocketClient.java b/app/src/main/java/top/gtf35/shellapplicatontest/SocketClient.java new file mode 100644 index 0000000..68d24b0 --- /dev/null +++ b/app/src/main/java/top/gtf35/shellapplicatontest/SocketClient.java @@ -0,0 +1,99 @@ +package top.gtf35.shellapplicatontest; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.Socket; + + +public class SocketClient { + + private String TAG = "SocketClient"; + + private String HOST = "127.0.0.1"; + PrintWriter printWriter;//发送用的 + onServiceSend mOnServiceSend; + String cmd; + BufferedReader bufferedReader; + int port = 4521; + + public SocketClient(String commod, onServiceSend onServiceSend) { + cmd = commod; + mOnServiceSend = onServiceSend; + try { + Log.d(TAG, "与service进行socket通讯,地址=" + HOST + ":" + port); + /** 创建Socket*/ + // 创建一个流套接字并将其连接到指定 IP 地址的指定端口号(本处是本机) + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(HOST, port), 3000);//设置连接请求超时时间3 s + // 接收3s超时 + socket.setSoTimeout(3000); + Log.d(TAG, "与service进行socket通讯,超时为:" + 3000); + /** 发送客户端准备传输的信息 */ + // 由Socket对象得到输出流,并构造PrintWriter对象 + printWriter = new PrintWriter(socket.getOutputStream(), true); + /** 用于获取服务端传输来的信息 */ + // 由Socket对象得到输入流,并构造相应的BufferedReader对象 + bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + new CreateServerThread(socket); + send(cmd); + } catch (Exception e) { + e.printStackTrace(); + Log.d(TAG, "与service进行socket通讯发生错误" + e); + mOnServiceSend.getSend("###ShellRunError:" + e.toString()); + } + } + + //线程类 + class CreateServerThread extends Thread { + Socket socket; + InputStreamReader inputStreamReader; + BufferedReader reader; + public CreateServerThread(Socket s) throws IOException { + Log.d(TAG, "创建了一个新的连接线程"); + socket = s; + start(); + } + + public void run() { + try { + // 打印读入一字符串并回调 + try { + inputStreamReader = new InputStreamReader(socket.getInputStream()); + reader = new BufferedReader(inputStreamReader); + String line = null; + while ((line = reader.readLine()) != null) { + if (line != null)mOnServiceSend.getSend(line); + } + Log.d(TAG, "客户端接收解析服务器的while循环结束"); + } catch (Exception e){ + e.printStackTrace(); + Log.d(TAG, "客户端接收解析服务器的Threadcatch块执行:" + e.toString()); + } + bufferedReader.close(); + printWriter.close(); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + Log.d(TAG, "socket 接收线程发生错误:" + e.toString()); + } + } + } + + + public void send(String cmd){ + printWriter.println(cmd ); + // 刷新输出流,使Server马上收到该字符串 + printWriter.flush(); + } + + public interface onServiceSend{ + void getSend(String result); + } +} + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..9a4d443 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,46 @@ + + + + + +