summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorshizhendong <[email protected]>2024-08-22 09:22:52 +0800
committershizhendong <[email protected]>2024-08-22 09:22:52 +0800
commit3e306e1a8c30655be7147751c51399a646cdef04 (patch)
tree0f2a00e36fcba23d02b20b729a813e5fbc3ff290 /src
parent94a42089e7c477e4783094f21437178b23cc9e97 (diff)
init: ASW-37 初始化 device-api
Diffstat (limited to 'src')
-rw-r--r--src/main/java/net/geedge/DeviceApiApplication.java48
-rw-r--r--src/main/java/net/geedge/api/config/VncProxyHandler.java84
-rw-r--r--src/main/java/net/geedge/api/config/WebSocketConfig.java21
-rw-r--r--src/main/java/net/geedge/api/controller/APIController.java166
-rw-r--r--src/main/java/net/geedge/api/entity/DeviceApiYml.java32
-rw-r--r--src/main/java/net/geedge/api/util/AdbCommandBuilder.java144
-rw-r--r--src/main/java/net/geedge/api/util/AdbDevice.java41
-rw-r--r--src/main/java/net/geedge/api/util/AdbUtil.java568
-rw-r--r--src/main/java/net/geedge/api/util/ApkInfo.java169
-rw-r--r--src/main/java/net/geedge/api/util/ApkUtil.java145
-rw-r--r--src/main/java/net/geedge/common/APIException.java47
-rw-r--r--src/main/java/net/geedge/common/APIExceptionHandler.java39
-rw-r--r--src/main/java/net/geedge/common/Constant.java17
-rw-r--r--src/main/java/net/geedge/common/R.java90
-rw-r--r--src/main/java/net/geedge/common/RCode.java42
-rw-r--r--src/main/java/net/geedge/common/T.java215
-rw-r--r--src/main/resources/application.yml12
-rw-r--r--src/test/java/net/geedge/TestJ.java8
18 files changed, 1888 insertions, 0 deletions
diff --git a/src/main/java/net/geedge/DeviceApiApplication.java b/src/main/java/net/geedge/DeviceApiApplication.java
new file mode 100644
index 0000000..8cb0821
--- /dev/null
+++ b/src/main/java/net/geedge/DeviceApiApplication.java
@@ -0,0 +1,48 @@
+package net.geedge;
+
+import cn.hutool.extra.spring.EnableSpringUtil;
+import net.geedge.api.entity.DeviceApiYml;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+
+import java.util.TimeZone;
+
+@EnableSpringUtil
+@SpringBootApplication
+public class DeviceApiApplication {
+
+ public static void main(String[] args) {
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+ SpringApplication.run(DeviceApiApplication.class, args);
+ }
+
+ @Autowired
+ private Environment env;
+
+ @Bean
+ public DeviceApiYml deviceProperties() {
+ DeviceApiYml apiYml = new DeviceApiYml();
+
+ DeviceApiYml.Device device = apiYml.new Device();
+ device.setRoot(env.getProperty("device.root"));
+ device.setType(env.getProperty("device.type"));
+ device.setPlatform(env.getProperty("device.platform"));
+
+ DeviceApiYml.Adb adb = apiYml.new Adb();
+ adb.setSerial(env.getProperty("adb.serial"));
+ adb.setHost(env.getProperty("adb.host"));
+ adb.setPort(env.getProperty("adb.port", Integer.class));
+
+ DeviceApiYml.Vnc vnc = apiYml.new Vnc();
+ vnc.setHost(env.getProperty("vnc.host"));
+ vnc.setPort(env.getProperty("vnc.port", Integer.class));
+
+ apiYml.setDevice(device);
+ apiYml.setAdb(adb);
+ apiYml.setVnc(vnc);
+ return apiYml;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/config/VncProxyHandler.java b/src/main/java/net/geedge/api/config/VncProxyHandler.java
new file mode 100644
index 0000000..a226568
--- /dev/null
+++ b/src/main/java/net/geedge/api/config/VncProxyHandler.java
@@ -0,0 +1,84 @@
+package net.geedge.api.config;
+
+import cn.hutool.log.Log;
+import net.geedge.api.entity.DeviceApiYml;
+import net.geedge.common.T;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+public class VncProxyHandler extends TextWebSocketHandler {
+
+ private static final Log log = Log.get();
+
+ private DeviceApiYml.Vnc vnc;
+
+ public VncProxyHandler(DeviceApiYml.Vnc vnc) {
+ this.vnc = vnc;
+ }
+
+ @Override
+ public synchronized void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ log.info("[afterConnectionEstablished] [WebSocket connection established] [websocket uri: {}]", session.getUri());
+ super.afterConnectionEstablished(session);
+
+ // connect to VNC Server
+ Socket vncSocket = new Socket(vnc.getHost(), vnc.getPort());
+ session.getAttributes().put("vncSocket", vncSocket);
+ log.info("[afterConnectionEstablished] [vnc server: {}] [isConnected: {}]", T.JSONUtil.toJsonStr(vnc), vncSocket.isConnected());
+
+ // vnc server -> web
+ T.ThreadUtil.execute(() -> {
+ this.forwardFromVncToWeb(session, vncSocket);
+ });
+ }
+
+ /**
+ * vnc server -> web
+ *
+ * @param session
+ * @param vncSocket
+ */
+ private void forwardFromVncToWeb(WebSocketSession session, Socket vncSocket) {
+ try (InputStream inputStream = vncSocket.getInputStream()) {
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ session.sendMessage(new BinaryMessage(buffer, 0, bytesRead, true));
+ }
+ } catch (IOException e) {
+ log.error(e, "[forwardFromVncToWeb] [error]");
+ }
+ }
+
+ @Override
+ protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
+ try {
+ // web -> vnc server
+ Socket vncSocket = (Socket) session.getAttributes().get("vncSocket");
+ if (vncSocket != null && !vncSocket.isClosed()) {
+ OutputStream outputStream = vncSocket.getOutputStream();
+ outputStream.write(message.getPayload().array());
+ outputStream.flush();
+ }
+ } catch (IOException e) {
+ log.error(e, "[handleBinaryMessage] [error]");
+ }
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+ log.info("[afterConnectionClosed] [WebSocket connection closed] [websocket uri: {}]", session.getUri());
+ Socket vncSocket = (Socket) session.getAttributes().get("vncSocket");
+ if (vncSocket != null && !vncSocket.isClosed()) {
+ vncSocket.close();
+ }
+ super.afterConnectionClosed(session, status);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/config/WebSocketConfig.java b/src/main/java/net/geedge/api/config/WebSocketConfig.java
new file mode 100644
index 0000000..453d972
--- /dev/null
+++ b/src/main/java/net/geedge/api/config/WebSocketConfig.java
@@ -0,0 +1,21 @@
+package net.geedge.api.config;
+
+import net.geedge.api.entity.DeviceApiYml;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig implements WebSocketConfigurer {
+
+ @Autowired
+ private DeviceApiYml deviceApiYml;
+
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ registry.addHandler(new VncProxyHandler(deviceApiYml.getVnc()), "/api/v1/device/websocket").setAllowedOrigins("*");
+ }
+}
diff --git a/src/main/java/net/geedge/api/controller/APIController.java b/src/main/java/net/geedge/api/controller/APIController.java
new file mode 100644
index 0000000..58e8c73
--- /dev/null
+++ b/src/main/java/net/geedge/api/controller/APIController.java
@@ -0,0 +1,166 @@
+package net.geedge.api.controller;
+
+import cn.hutool.core.codec.Base32Codec;
+import jakarta.servlet.http.HttpServletResponse;
+import net.geedge.api.entity.DeviceApiYml;
+import net.geedge.api.util.AdbUtil;
+import net.geedge.common.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v1/device")
+public class APIController {
+
+ private final AdbUtil adbUtil;
+
+ @Autowired
+ public APIController(DeviceApiYml deviceApiYml) {
+ this.adbUtil = AdbUtil.getInstance(deviceApiYml.getAdb());
+ }
+
+ @GetMapping("/status")
+ public R status() {
+ return R.ok(adbUtil.status());
+ }
+
+ @PostMapping("/file")
+ public R push(@RequestParam(value = "file") MultipartFile file, @RequestParam String path) throws IOException {
+ File tempFile = null;
+ try {
+ tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename());
+ file.transferTo(tempFile);
+
+ AdbUtil.CommandResult result = adbUtil.push(tempFile.getAbsolutePath(), path);
+ if (0 != result.exitCode()) {
+ return R.error(result.output());
+ }
+ } finally {
+ T.FileUtil.del(tempFile);
+ }
+ return R.ok();
+ }
+
+ @GetMapping("/file/{fileId}")
+ public void pull(@PathVariable String fileId, HttpServletResponse response) throws IOException {
+ byte[] decode = Base32Codec.Base32Decoder.DECODER.decode(fileId);
+ String filePath = T.StrUtil.str(decode, T.CharsetUtil.CHARSET_UTF_8);
+ String fileName = T.FileUtil.getName(filePath);
+
+ File tempFile = T.FileUtil.file(Constant.TEMP_PATH, fileName);
+ try {
+ AdbUtil.CommandResult result = adbUtil.pull(filePath, tempFile.getAbsolutePath());
+ if (0 != result.exitCode()) {
+ throw new APIException(result.output());
+ }
+ if (T.FileUtil.isDirectory(tempFile)) {
+ File zip = T.ZipUtil.zip(tempFile);
+ try {
+ T.ResponseUtil.downloadFile(response, zip.getName(), T.FileUtil.readBytes(zip));
+ } finally {
+ T.FileUtil.del(zip);
+ }
+ } else {
+ T.ResponseUtil.downloadFile(response, fileName, T.FileUtil.readBytes(tempFile));
+ }
+ } finally {
+ T.FileUtil.del(tempFile);
+ }
+ }
+
+ @GetMapping("/file")
+ public R listDir(@RequestParam(defaultValue = "/") String path) {
+ List<Map> listDir = adbUtil.listDir(path);
+ Map<Object, Object> data = T.MapUtil.builder()
+ .put("path", path)
+ .put("records", listDir)
+ .build();
+ return R.ok(data);
+ }
+
+ @GetMapping("/app")
+ public R listApp(@RequestParam(required = false) String arg) {
+ return R.ok().putData("records", adbUtil.listApp(arg));
+ }
+
+ @PostMapping("/app")
+ public R install(@RequestParam(value = "file", required = false) MultipartFile file,
+ @RequestParam(required = false) String path) throws IOException {
+ if (file != null) {
+ File tempFile = null;
+ try {
+ tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename());
+ file.transferTo(tempFile);
+
+ AdbUtil.CommandResult result = adbUtil.install(tempFile.getAbsolutePath(), true, true);
+ if (0 != result.exitCode()) {
+ throw new APIException(result.output());
+ }
+ return R.ok();
+ } finally {
+ T.FileUtil.del(tempFile);
+ }
+ }
+
+ if (T.StrUtil.isNotEmpty(path)) {
+ AdbUtil.CommandResult result = adbUtil.install(path, true, true);
+ if (0 != result.exitCode()) {
+ throw new APIException(result.output());
+ }
+ return R.ok();
+ }
+ return R.error(RCode.BAD_REQUEST);
+ }
+
+ @DeleteMapping("/app")
+ public R uninstall(@RequestParam String packageName) {
+ AdbUtil.CommandResult result = adbUtil.uninstall(packageName);
+ if (0 != result.exitCode()) {
+ throw new APIException(result.output());
+ }
+ return R.ok();
+ }
+
+ @PostMapping("/pcap")
+ public R startTcpdump(@RequestParam(required = false, defaultValue = "") String packageName) {
+ AdbUtil.CommandResult result = adbUtil.startTcpdump(packageName);
+ if (0 != result.exitCode()) {
+ throw new APIException("exec tcpdump error");
+ }
+ return R.ok().putData("id", result.output());
+ }
+
+ @DeleteMapping("/pcap")
+ public void stopTcpdump(@RequestParam String id,
+ @RequestParam(required = false, defaultValue = "false") Boolean returnFile,
+ HttpServletResponse response) throws IOException {
+ AdbUtil.CommandResult result = adbUtil.stopTcpdump(id);
+ if (0 != result.exitCode()) {
+ throw new APIException(result.output());
+ }
+
+ if (returnFile) {
+ // response pcap file
+ File tempFile = T.FileUtil.file(Constant.TEMP_PATH, id + ".pcap");
+ try {
+ String filePath = "/data/local/tmp/" + id + ".pcap";
+ AdbUtil.CommandResult pulled = adbUtil.pull(filePath, tempFile.getAbsolutePath());
+ if (0 != pulled.exitCode()) {
+ throw new APIException(pulled.output());
+ }
+ T.ResponseUtil.downloadFile(response, tempFile.getName(), T.FileUtil.readBytes(tempFile));
+ } finally {
+ T.FileUtil.del(tempFile);
+ }
+ } else {
+ // response taskid
+ response.getWriter().write(T.JSONUtil.toJsonStr(R.ok().putData("id", id)));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/entity/DeviceApiYml.java b/src/main/java/net/geedge/api/entity/DeviceApiYml.java
new file mode 100644
index 0000000..d939028
--- /dev/null
+++ b/src/main/java/net/geedge/api/entity/DeviceApiYml.java
@@ -0,0 +1,32 @@
+package net.geedge.api.entity;
+
+import lombok.Data;
+
+@Data
+public class DeviceApiYml {
+
+ private Device device;
+ private Adb adb;
+ private Vnc vnc;
+
+ @Data
+ public class Device {
+ String type;
+ String platform;
+ String root;
+ }
+
+ @Data
+ public class Adb {
+ String serial;
+ String host;
+ Integer port;
+ }
+
+ @Data
+ public class Vnc {
+ String host;
+ Integer port;
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/util/AdbCommandBuilder.java b/src/main/java/net/geedge/api/util/AdbCommandBuilder.java
new file mode 100644
index 0000000..5335d41
--- /dev/null
+++ b/src/main/java/net/geedge/api/util/AdbCommandBuilder.java
@@ -0,0 +1,144 @@
+package net.geedge.api.util;
+
+import net.geedge.common.T;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class AdbCommandBuilder {
+
+ private final String adbPath;
+ private final List<String> command;
+
+ private AdbCommandBuilder(String adbPath) {
+ this.adbPath = adbPath;
+ this.command = new LinkedList<>();
+ this.command.add(adbPath);
+ }
+
+ public static AdbCommandBuilder builder() {
+ return new AdbCommandBuilder("adb");
+ }
+
+ public static AdbCommandBuilder builder(String adbPath) {
+ return new AdbCommandBuilder(adbPath);
+ }
+
+ public AdbCommandBuilder serial(String serial) {
+ this.command.add("-s");
+ this.command.add(serial);
+ return this;
+ }
+
+ public AdbCommandBuilder buildConnectCommand(String host, Integer port) {
+ this.command.add("connect");
+ this.command.add(String.format("%s:%s", host, port));
+ return this;
+ }
+
+ public AdbCommandBuilder buildDevicesCommand() {
+ this.command.add("devices");
+ this.command.add("-l");
+ return this;
+ }
+
+ public AdbCommandBuilder buildRootCommand() {
+ this.command.add("root");
+ return this;
+ }
+
+ public AdbCommandBuilder buildGetpropCommand() {
+ this.command.add("shell");
+ this.command.add("getprop");
+ return this;
+ }
+
+ public AdbCommandBuilder buildWmSizeCommand() {
+ this.command.add("shell");
+ this.command.add("wm");
+ this.command.add("size");
+ return this;
+ }
+
+ public AdbCommandBuilder buildCheckRootCommand() {
+ this.command.add("shell");
+ this.command.add("ls");
+ this.command.add("/data");
+ return this;
+ }
+
+ /**
+ * 指定 String cmd 执行,解决命令过长阅读性较差问题
+ */
+ public AdbCommandBuilder buildShellCommand(String shellCmd) {
+ String[] strings = T.CommandLineUtil.translateCommandline(shellCmd);
+ for (String string : strings) {
+ this.command.add(string);
+ }
+ return this;
+ }
+
+ public AdbCommandBuilder buildPushCommand(String local, String remote) {
+ this.command.add("push");
+ this.command.add(local);
+ this.command.add(remote);
+ return this;
+ }
+
+ public AdbCommandBuilder buildPullCommand(String remote, String local) {
+ this.command.add("pull");
+ this.command.add(remote);
+ this.command.add(local);
+ return this;
+ }
+
+ public AdbCommandBuilder buildLsDirCommand(String path) {
+ this.command.add("shell");
+ this.command.add("ls");
+ this.command.add("-l");
+ this.command.add(path);
+ return this;
+ }
+
+ public AdbCommandBuilder buildPmListPackagesCommand(String arg) {
+ this.command.add("shell");
+ this.command.add("pm");
+ this.command.add("list");
+ this.command.add("packages");
+ if (T.StrUtil.isNotEmpty(arg)) {
+ this.command.add(arg);
+ }
+ return this;
+ }
+
+ public AdbCommandBuilder buildMd5sumCommand(String path) {
+ this.command.add("shell");
+ this.command.add("md5sum");
+ this.command.add(path);
+ return this;
+ }
+
+ public AdbCommandBuilder buildInstallCommand(String localFilePath, boolean isDebugApk, boolean isReInstall) {
+ this.command.add("install");
+ if (isDebugApk) {
+ this.command.add("-d");
+ }
+ if (isReInstall) {
+ this.command.add("-r");
+ }
+ this.command.add(localFilePath);
+ return this;
+ }
+
+ public AdbCommandBuilder buildUnInstallCommand(String packageName) {
+ this.command.add("uninstall");
+ this.command.add(packageName);
+ return this;
+ }
+
+
+ public List<String> build() {
+ return this.command;
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/util/AdbDevice.java b/src/main/java/net/geedge/api/util/AdbDevice.java
new file mode 100644
index 0000000..5dd1d01
--- /dev/null
+++ b/src/main/java/net/geedge/api/util/AdbDevice.java
@@ -0,0 +1,41 @@
+package net.geedge.api.util;
+
+import lombok.Data;
+import net.geedge.common.T;
+
+@Data
+public class AdbDevice implements Comparable<AdbDevice> {
+
+ private String serial;
+ private boolean available;
+
+
+ public AdbDevice(String line) {
+ String[] array = line.split(" ");
+ serial = array[0];
+
+ for (int i = 1; i < array.length; i++) {
+ if (!T.StrUtil.isEmpty(array[i])) {
+ available = "device".equals(array[i]);
+ break;
+ }
+ }
+ }
+
+ public boolean isAvailable() {
+ return available;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof AdbDevice)
+ return serial.equals(((AdbDevice) object).serial) && available == ((AdbDevice) object).available;
+ return false;
+ }
+
+ @Override
+ public int compareTo(AdbDevice device) {
+ return serial.compareTo(device.serial);
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/util/AdbUtil.java b/src/main/java/net/geedge/api/util/AdbUtil.java
new file mode 100644
index 0000000..ed901b2
--- /dev/null
+++ b/src/main/java/net/geedge/api/util/AdbUtil.java
@@ -0,0 +1,568 @@
+package net.geedge.api.util;
+
+import cn.hutool.core.codec.Base32Codec;
+import cn.hutool.core.thread.NamedThreadFactory;
+import cn.hutool.log.Log;
+import net.geedge.api.entity.DeviceApiYml;
+import net.geedge.common.APIException;
+import net.geedge.common.Constant;
+import net.geedge.common.RCode;
+import net.geedge.common.T;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AdbUtil {
+
+ private static final Log log = Log.get();
+
+ private static AdbUtil instance;
+
+ private String serial;
+ private String host;
+ private Integer port;
+
+ private ExecutorService threadPool;
+
+ public String getSerial() {
+ return T.StrUtil.isNotEmpty(this.serial) ? serial : String.format("%s:%s", this.host, this.port);
+ }
+
+ public record CommandResult(Integer exitCode, String output) {
+ }
+
+ private AdbUtil(DeviceApiYml.Adb adb) {
+ this.serial = T.StrUtil.emptyToDefault(adb.getSerial(), "");
+ this.host = adb.getHost();
+ this.port = adb.getPort();
+ this.connect();
+ }
+
+ public static AdbUtil getInstance(DeviceApiYml.Adb connInfo) {
+ if (instance == null) {
+ synchronized (AdbUtil.class) {
+ if (instance == null) {
+ instance = new AdbUtil(connInfo);
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * connect
+ */
+ private void connect() {
+ if (T.StrUtil.isNotEmpty(this.serial)) {
+ // local
+ AdbDevice adbDevice = this.getAdbDevice();
+ log.info("[connect] [result: {}]", T.JSONUtil.toJsonStr(adbDevice));
+ if (null == adbDevice || !adbDevice.isAvailable()) {
+ log.error("[device is not available, program exit]");
+ Runtime.getRuntime().halt(1);
+ }
+ } else {
+ // remote
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .buildConnectCommand(this.host, this.port)
+ .build());
+ log.info("[connect] [result: {}]", result);
+ if (!T.StrUtil.contains(result, "connected")) {
+ log.error("[connect error, program exit]");
+ Runtime.getRuntime().halt(1);
+ }
+ }
+ // adb root
+ CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildRootCommand()
+ .build()
+ );
+ }
+
+ /**
+ * status
+ *
+ * @return
+ */
+ public Map<Object, Object> status() {
+ Map<Object, Object> m = T.MapUtil.builder()
+ .put("platform", "android")
+ .build();
+ AdbDevice device = this.getAdbDevice();
+ m.put("status", device.isAvailable() ? "online" : "offline");
+
+ Map<String, String> prop = this.getProp();
+ m.put("name", T.MapUtil.getStr(prop, "ro.product.name", ""));
+ m.put("brand", T.MapUtil.getStr(prop, "ro.product.brand", ""));
+ m.put("model", T.MapUtil.getStr(prop, "ro.product.model", ""));
+ m.put("version", T.MapUtil.getStr(prop, "ro.build.version.release", ""));
+ m.put("resolution", T.MapUtil.getStr(prop, "wm.size", ""));
+
+ // 默认为真机
+ String type = "device";
+ for (Map.Entry<String, String> entry : prop.entrySet()) {
+ // 根据 ro.build.* 这一组配置值判定是否为模拟器,如果包含 emulator、sdk 则为模拟器
+ if (entry.getKey().contains("ro.build")) {
+ String value = entry.getValue();
+ if (T.StrUtil.containsAnyIgnoreCase(value, "emulator", "sdk")) {
+ type = "emulator";
+ break;
+ }
+ }
+ }
+ m.put("type", type);
+
+ // check root
+ String checkRootResult = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildCheckRootCommand()
+ .build()
+ );
+ m.put("root", !T.StrUtil.containsIgnoreCase(checkRootResult, "Permission denied"));
+ return m;
+ }
+
+ /**
+ * getAdbDevice
+ * adb devices -l
+ *
+ * @return
+ */
+ private AdbDevice getAdbDevice() {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .buildDevicesCommand()
+ .build()
+ );
+ List<AdbDevice> list = T.ListUtil.list(true);
+ String[] lines = result.split("\\n");
+ for (String line : lines) {
+ if (line.startsWith("*") || line.startsWith("List") || T.StrUtil.isEmpty(line))
+ continue;
+ list.add(new AdbDevice(line));
+ }
+ AdbDevice adbDevice = list.stream()
+ .filter(pojo -> T.StrUtil.equals(pojo.getSerial(), this.getSerial()))
+ .findFirst()
+ .orElse(null);
+ return adbDevice;
+ }
+
+ /**
+ * getProp
+ * adb shell getprop
+ *
+ * @return
+ */
+ private Map<String, String> getProp() {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildGetpropCommand()
+ .build()
+ );
+ Map<String, String> prop = new LinkedHashMap<>();
+
+ Pattern pattern = Pattern.compile("\\[(.*?)\\]: \\[(.*?)\\]");
+ Matcher matcher = pattern.matcher(result);
+
+ while (matcher.find()) {
+ String key = matcher.group(1).trim();
+ String value = matcher.group(2).trim();
+ prop.put(key, value);
+ }
+
+ // 分辨率 Physical size: 1440x3040
+ String wmSize = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildWmSizeCommand()
+ .build()
+ );
+ prop.put("wm.size", T.StrUtil.trim(wmSize.replaceAll("Physical size: ", "")));
+ return prop;
+ }
+
+
+ /**
+ * md5sum
+ */
+ private CommandResult md5sum(String path) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildMd5sumCommand(path)
+ .build()
+ );
+ log.info("[md5sum] [path: {}] [result: {}]", path, result);
+ if (T.StrUtil.isNotEmpty(result)) {
+ String md5 = result.split("\\s+")[0];
+ return new CommandResult(0, md5);
+ }
+ return new CommandResult(1, "");
+ }
+
+ /**
+ * push
+ * 0 success; !0 failed
+ */
+ public CommandResult push(String local, String remote) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildPushCommand(local, remote)
+ .build()
+ );
+ log.info("[push] [local: {}] [remote: {}] [result: {}]", local, remote, result);
+ return new CommandResult(T.StrUtil.contains(result, "failed") ? 1 : 0, result);
+ }
+
+ /**
+ * pull
+ * 0 success; !0 failed
+ */
+ public CommandResult pull(String remote, String local) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildPullCommand(remote, local)
+ .build()
+ );
+ log.info("[pull] [remote: {}] [local: {}] [result: {}]", remote, local, result);
+ return new CommandResult(T.StrUtil.containsAny(result, "file pulled", "files pulled") ? 0 : 1, result);
+ }
+
+ /**
+ * list dir
+ * ls -l
+ * stat filename
+ */
+ public List<Map> listDir(String path) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildLsDirCommand(path)
+ .build()
+ );
+ if (T.StrUtil.contains(result, "No such file or directory")) {
+ log.warn("[listDir] [path: {}] [result: {}]", path, result);
+ throw new APIException(RCode.NOT_EXISTS);
+ }
+
+ if (T.StrUtil.contains(result, "Permission denied")) {
+ log.warn("[listDir] [path: {}] [result: {}]", path, result);
+ throw new APIException(RCode.NOT_PERMISSION);
+ }
+
+ List<CompletableFuture<Map>> futureList = T.ListUtil.list(false);
+
+ List<Map> listDir = T.ListUtil.list(true);
+ String[] lines = result.split("\\n");
+ boolean isDir = false;
+ for (String line : lines) {
+ if (line.startsWith("total")) {
+ isDir = true;
+ continue;
+ }
+ String[] split = line.split("\\s+");
+ String name;
+ // link file|dir
+ if (10 == split.length) {
+ name = split[7];
+ } else {
+ name = split[split.length - 1];
+ }
+
+ String statFilePath = isDir ? Paths.get(path).resolve(name).toString() : path;
+ String statCommand = "shell stat -c \"'%N %a %X %Y'\" " + statFilePath;
+ futureList.add(
+ CompletableFuture.supplyAsync(() -> {
+ String statResult = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(statCommand.replaceAll("\\\\", "/"))
+ .build()
+ );
+ // reverse result
+ List<String> list = Arrays.asList(statResult.split("\\s+"));
+ Collections.reverse(list);
+
+ String fullName = list.get(3).replaceAll("'|`", "");
+ Map<String, Object> relMap = T.MapUtil.newHashMap();
+ relMap.put("name", name);
+ relMap.put("value", T.MapUtil.builder()
+ .put("id", Base32Codec.Base32Encoder.ENCODER.encode(fullName.getBytes()))
+ .put("fullName", fullName)
+ .put("permissions", Long.parseLong(list.get(2)))
+ .put("cts", Long.parseLong(list.get(1)))
+ .put("uts", Long.parseLong(list.get(0)))
+ .build());
+ return relMap;
+ }, getThreadPool())
+ );
+
+ Map<Object, Object> m = T.MapUtil.builder()
+ .put("name", name)
+ .build();
+ listDir.add(m);
+ }
+
+ try {
+ CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
+ futureList.forEach(f -> {
+ Map map = f.getNow(null);
+ if (T.MapUtil.isNotEmpty(map)) {
+ String name = T.MapUtil.getStr(map, "name");
+ Map fileAttr = listDir.stream().filter(m -> T.MapUtil.getStr(m, "name").equals(name)).findFirst().get();
+ fileAttr.putAll(T.MapUtil.get(map, "value", Map.class));
+ }
+ });
+ } catch (Exception e) {
+ log.warn(e);
+ }
+ return listDir;
+ }
+
+ /**
+ * listApp
+ * adb shell pm list packages
+ *
+ * @return
+ */
+ public List<Map> listApp(String arg) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildPmListPackagesCommand(arg)
+ .build()
+ );
+
+ List<Map> listApp = T.ListUtil.list(true);
+
+ List<CompletableFuture<Map>> futureList = T.ListUtil.list(false);
+
+ String prefix = "package:";
+ String[] lines = result.split("\\n");
+ for (String line : lines) {
+ String packageName = T.StrUtil.trim(line.substring(prefix.length()));
+
+ String dumpsysResult = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand("shell dumpsys package " + packageName)
+ .build()
+ );
+ String[] split = dumpsysResult.split("\\n");
+ String version = "", apkPath = "";
+ for (String s : split) {
+ if (s.contains("versionName=")) {
+ version = T.StrUtil.trim(s).replaceAll("versionName=", "");
+ }
+ if (s.contains("path: ")) {
+ apkPath = T.StrUtil.trim(s).replaceAll("path: ", "");
+ }
+ }
+ if (T.StrUtil.isNotEmpty(apkPath)) {
+ String finalApkPath = apkPath;
+ futureList.add(
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ CommandResult md5sumRes = this.md5sum(finalApkPath);
+ String md5Value = md5sumRes.output();
+ File localApk = T.FileUtil.file(Constant.TEMP_PATH, md5Value + ".apk");
+ if (!T.FileUtil.exist(localApk)) {
+ CommandResult pulled = this.pull(finalApkPath, localApk.getAbsolutePath());
+ if (0 != pulled.exitCode()) {
+ log.warn("[listApp] [pull apk error] [pkg: {}]", packageName);
+ return null;
+ }
+ }
+ ApkUtil apkUtil = new ApkUtil();
+ ApkInfo apkInfo = apkUtil.parseApk(localApk.getAbsolutePath());
+ String appName = apkInfo.getLabel();
+ String iconFilename = apkInfo.getIcon();
+ String base64IconDate = apkUtil.extractFileFromApk(localApk.getAbsolutePath(), iconFilename);
+
+ Map<String, Object> relMap = T.MapUtil.newHashMap();
+ relMap.put("pkg", packageName);
+ relMap.put("value", T.MapUtil.builder()
+ .put("name", appName)
+ .put("icon", base64IconDate)
+ .build());
+ return relMap;
+ } catch (Exception e) {
+ log.error(e, "[listApp] [parse apk] [pkg: {}]", packageName);
+ }
+ return null;
+ }, getThreadPool())
+ );
+ }
+
+ Map<Object, Object> m = T.MapUtil.builder()
+ .put("packageName", packageName)
+ .put("version", version)
+ .build();
+ listApp.add(m);
+ }
+
+ try {
+ CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
+ futureList.forEach(f -> {
+ Map map = f.getNow(null);
+ if (T.MapUtil.isNotEmpty(map)) {
+ String pkg = T.MapUtil.getStr(map, "pkg");
+ Map appAttr = listApp.stream().filter(m -> T.MapUtil.getStr(m, "packageName").equals(pkg)).findFirst().get();
+ appAttr.putAll(T.MapUtil.get(map, "value", Map.class));
+ }
+ });
+ } catch (Exception e) {
+ log.warn(e);
+ }
+ return listApp;
+ }
+
+ /**
+ * install app
+ * adb install apk
+ */
+ public CommandResult install(String localFilePath, boolean isDebugApk, boolean isReInstall) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildInstallCommand(localFilePath, isDebugApk, isReInstall)
+ .build()
+ );
+ log.info("[install] [localFilePath: {}] [isDebugApk: {}] [isReInstall: {}] [result: {}]", localFilePath, isDebugApk, isReInstall, result);
+ return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
+ }
+
+ /**
+ * uninstall app
+ * adb uninstall package_name
+ */
+ public CommandResult uninstall(String packageName) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildUnInstallCommand(packageName)
+ .build()
+ );
+ log.info("[uninstall] [packageName: {}] [result: {}]", packageName, result);
+ return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
+ }
+
+ /**
+ * iptables -F
+ * iptables -X
+ */
+ private void cleanIptables() {
+ // Delete all rules in chain or all chains
+ CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand("shell iptables -F")
+ .build()
+ );
+ // Delete user-defined chain
+ CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand("shell iptables -X")
+ .build()
+ );
+ }
+
+ /**
+ * start Tcpdump
+ * iptables option
+ * tcpdump pcap
+ */
+ public CommandResult startTcpdump(String packageName) {
+ // clean iptables conf
+ this.cleanIptables();
+
+ String taskId = T.IdUtil.fastSimpleUUID();
+ String pcapFilePath = "/data/local/tmp/" + taskId + ".pcap";
+ if (T.StrUtil.isNotEmpty(packageName)) {
+ log.info("[startTcpdump] [capture app package] [pkg: {}]", packageName);
+ String dumpsysResult = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand("shell dumpsys package " + packageName)
+ .build()
+ );
+ String[] lines = dumpsysResult.split("\\n");
+ String userId = Arrays.stream(lines)
+ .filter(s -> T.StrUtil.contains(s, "userId="))
+ .findFirst()
+ .map(s -> T.StrUtil.trim(s).replaceAll("userId=", ""))
+ .orElseThrow(() -> new APIException("Not found userId by package name. package name: " + packageName));
+
+ CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell iptables -A OUTPUT -m owner --uid-owner %s -j CONNMARK --set-mark %s", userId, userId))
+ .build());
+ CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell iptables -A INPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
+ .build());
+ CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell iptables -A OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
+ .build());
+
+ String ruleList = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand("shell iptables -L")
+ .build());
+ log.info("[startTcpdump] [iptables -L] [result: {}]", ruleList);
+
+ CommandExec.execForProcess(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell tcpdump -i nflog:%s -w %s &", userId, pcapFilePath))
+ .build());
+ } else {
+ log.info("[startTcpdump] [capture all package]");
+ CommandExec.execForProcess(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell tcpdump -w %s &", pcapFilePath))
+ .build());
+ }
+
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' \"", taskId))
+ .build());
+ log.info("[startTcpdump] [taskId: {}] [tcpdump pid: {}]", taskId, result);
+ return new CommandResult(T.StrUtil.isNotEmpty(result) ? 0 : 1, taskId);
+ }
+
+ /**
+ * stop tcpdump
+ * kill -INT {pid}
+ */
+ public CommandResult stopTcpdump(String id) {
+ String result = CommandExec.exec(AdbCommandBuilder.builder()
+ .serial(this.getSerial())
+ .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' | xargs kill -INT \"", id))
+ .build());
+ log.info("[stopTcpdump] [id: {}] [result: {}]", id, result);
+ return new CommandResult(T.StrUtil.isEmpty(result) ? 0 : 1, result);
+ }
+
+
+ private synchronized ExecutorService getThreadPool() {
+ if (threadPool == null) {
+ threadPool = new ThreadPoolExecutor(
+ 5,
+ 10,
+ 30,
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue<Runnable>(10000),
+ new NamedThreadFactory("API-", true));
+ }
+ return threadPool;
+ }
+
+ class CommandExec {
+ public static String exec(List<String> command) {
+ String str = T.RuntimeUtil.execForStr(T.CharsetUtil.CHARSET_UTF_8, command.stream().toArray(String[]::new));
+ return str.stripTrailing();
+ }
+
+ public static Process execForProcess(List<String> command) {
+ Process process = T.RuntimeUtil.exec(command.stream().toArray(String[]::new));
+ return process;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/util/ApkInfo.java b/src/main/java/net/geedge/api/util/ApkInfo.java
new file mode 100644
index 0000000..8806d4e
--- /dev/null
+++ b/src/main/java/net/geedge/api/util/ApkInfo.java
@@ -0,0 +1,169 @@
+package net.geedge.api.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ApkInfo {
+
+ public static final String APPLICATION_ICON_120 = "application-icon-120";
+ public static final String APPLICATION_ICON_160 = "application-icon-160";
+ public static final String APPLICATION_ICON_240 = "application-icon-240";
+ public static final String APPLICATION_ICON_320 = "application-icon-320";
+
+ // 所需设备属性
+ private List<String> features;
+ // 图标
+ private String icon;
+ // 各分辨率下图标路径
+ private Map<String, String> icons;
+ // 应用程序名
+ private String label;
+ // 入口Activity
+ private String launchableActivity;
+ // 支持的Android平台最低版本号
+ private String minSdkVersion;
+ // 主包名
+ private String packageName;
+ // 支持的SDK版本
+ private String sdkVersion;
+ // Apk文件大小(字节)
+ private long size;
+ // 目标SDK版本
+ private String targetSdkVersion;
+ // 所需权限
+ private List<String> usesPermissions;
+ // 内部版本号
+ private String versionCode;
+ // 外部版本号
+ private String versionName;
+
+ public ApkInfo() {
+ this.features = new ArrayList<>();
+ this.icons = new HashMap<>();
+ this.usesPermissions = new ArrayList<>();
+ }
+
+ public List<String> getFeatures() {
+ return features;
+ }
+
+ public void setFeatures(List<String> features) {
+ this.features = features;
+ }
+
+ public void addToFeatures(String feature) {
+ this.features.add(feature);
+ }
+
+ public String getIcon() {
+ return icon;
+ }
+
+ public void setIcon(String icon) {
+ this.icon = icon;
+ }
+
+ public Map<String, String> getIcons() {
+ return icons;
+ }
+
+ public void setIcons(Map<String, String> icons) {
+ this.icons = icons;
+ }
+
+ public void addToIcons(String key, String value) {
+ this.icons.put(key, value);
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public String getLaunchableActivity() {
+ return launchableActivity;
+ }
+
+ public void setLaunchableActivity(String launchableActivity) {
+ this.launchableActivity = launchableActivity;
+ }
+
+ public String getMinSdkVersion() {
+ return minSdkVersion;
+ }
+
+ public void setMinSdkVersion(String minSdkVersion) {
+ this.minSdkVersion = minSdkVersion;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public String getSdkVersion() {
+ return sdkVersion;
+ }
+
+ public void setSdkVersion(String sdkVersion) {
+ this.sdkVersion = sdkVersion;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public String getTargetSdkVersion() {
+ return targetSdkVersion;
+ }
+
+ public void setTargetSdkVersion(String targetSdkVersion) {
+ this.targetSdkVersion = targetSdkVersion;
+ }
+
+ public List<String> getUsesPermissions() {
+ return usesPermissions;
+ }
+
+ public void setUsesPermissions(List<String> usesPermissions) {
+ this.usesPermissions = usesPermissions;
+ }
+
+ public void addToUsesPermissions(String usesPermission) {
+ this.usesPermissions.add(usesPermission);
+ }
+
+ public String getVersionCode() {
+ return versionCode;
+ }
+
+ public void setVersionCode(String versionCode) {
+ this.versionCode = versionCode;
+ }
+
+ public String getVersionName() {
+ return versionName;
+ }
+
+ public void setVersionName(String versionName) {
+ this.versionName = versionName;
+ }
+
+ @Override
+ public String toString() {
+ return "ApkInfo [features=" + features + ", icon=" + icon + ", icons=" + icons + ", label=" + label + ", launchableActivity=" + launchableActivity + ", minSdkVersion=" + minSdkVersion + ", packageName=" + packageName + ", sdkVersion=" + sdkVersion + ", size=" + size + ", targetSdkVersion=" + targetSdkVersion + ", usesPermissions=" + usesPermissions + ", versionCode=" + versionCode + ", versionName=" + versionName + "]";
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/api/util/ApkUtil.java b/src/main/java/net/geedge/api/util/ApkUtil.java
new file mode 100644
index 0000000..572b058
--- /dev/null
+++ b/src/main/java/net/geedge/api/util/ApkUtil.java
@@ -0,0 +1,145 @@
+package net.geedge.api.util;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.log.Log;
+import net.geedge.common.Constant;
+import net.geedge.common.T;
+
+import java.io.*;
+import java.util.Base64;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class ApkUtil {
+
+ private static final Log log = Log.get();
+
+ public static final String APPLICATION = "application:";
+ public static final String APPLICATION_ICON = "application-icon";
+ public static final String APPLICATION_LABEL = "application-label";
+ public static final String APPLICATION_LABEL_N = "application: label";
+ public static final String DENSITIES = "densities";
+ public static final String LAUNCHABLE_ACTIVITY = "launchable";
+ public static final String PACKAGE = "package";
+ public static final String SDK_VERSION = "sdkVersion";
+ public static final String SUPPORTS_ANY_DENSITY = "support-any-density";
+ public static final String SUPPORTS_SCREENS = "support-screens";
+ public static final String TARGET_SDK_VERSION = "targetSdkVersion";
+ public static final String VERSION_CODE = "versionCode";
+ public static final String VERSION_NAME = "versionName";
+ public static final String USES_FEATURE = "uses-feature";
+ public static final String USES_IMPLIED_FEATURE = "uses-implied-feature";
+ public static final String USES_PERMISSION = "uses-permission";
+
+ private static final String SPLIT_REGEX = "(: )|(=')|(' )|'";
+
+ private ProcessBuilder builder;
+ // aapt 所在目录
+ private String aaptToolPath = "aapt";
+
+ public ApkUtil() {
+ builder = new ProcessBuilder();
+ builder.redirectErrorStream(true);
+ }
+
+ public String getAaptToolPath() {
+ return aaptToolPath;
+ }
+
+ public void setAaptToolPath(String aaptToolPath) {
+ this.aaptToolPath = aaptToolPath;
+ }
+
+ public ApkInfo parseApk(String apkPath) {
+ String aaptTool = aaptToolPath;
+ Process process = null;
+ InputStream inputStream = null;
+ BufferedReader bufferedReader = null;
+ try {
+ process = builder.command(aaptTool, "d", "badging", apkPath).start();
+ inputStream = process.getInputStream();
+ bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
+ ApkInfo apkInfo = new ApkInfo();
+ apkInfo.setSize(new File(apkPath).length());
+ String temp = null;
+ while ((temp = bufferedReader.readLine()) != null) {
+ setApkInfoProperty(apkInfo, temp);
+ }
+ return apkInfo;
+ } catch (IOException e) {
+ log.error(e, "[parseApk] [error]");
+ return null;
+ } finally {
+ if (process != null) {
+ process.destroy();
+ }
+ T.IoUtil.close(inputStream);
+ T.IoUtil.close(bufferedReader);
+ }
+ }
+
+ private void setApkInfoProperty(ApkInfo apkInfo, String source) {
+ if (source.startsWith(APPLICATION)) {
+ String[] rs = source.split("( icon=')|'");
+ apkInfo.setIcon(rs[rs.length - 1]);
+ } else if (source.startsWith(APPLICATION_ICON)) {
+ apkInfo.addToIcons(getKeyBeforeColon(source), getPropertyInQuote(source));
+ } else if (source.startsWith(APPLICATION_LABEL)) {
+ apkInfo.setLabel(getPropertyInQuote(source));
+ } else if (source.startsWith(LAUNCHABLE_ACTIVITY)) {
+ apkInfo.setLaunchableActivity(getPropertyInQuote(source));
+ } else if (source.startsWith(PACKAGE)) {
+ String[] packageInfo = source.split(SPLIT_REGEX);
+ apkInfo.setPackageName(packageInfo[2]);
+ apkInfo.setVersionCode(packageInfo[4]);
+ apkInfo.setVersionName(packageInfo[6]);
+ } else if (source.startsWith(SDK_VERSION)) {
+ apkInfo.setSdkVersion(getPropertyInQuote(source));
+ } else if (source.startsWith(TARGET_SDK_VERSION)) {
+ apkInfo.setTargetSdkVersion(getPropertyInQuote(source));
+ } else if (source.startsWith(USES_PERMISSION)) {
+ apkInfo.addToUsesPermissions(getPropertyInQuote(source));
+ } else if (source.startsWith(USES_FEATURE)) {
+ apkInfo.addToFeatures(getPropertyInQuote(source));
+ }
+ }
+
+ private String getKeyBeforeColon(String source) {
+ return source.substring(0, source.indexOf(':'));
+ }
+
+ private String getPropertyInQuote(String source) {
+ int index = source.indexOf("'") + 1;
+ return source.substring(index, source.indexOf('\'', index));
+ }
+
+ public String extractFileFromApk(String apkPath, String fileName) {
+ ZipFile zipFile = null;
+ File tempIconFile = T.FileUtil.file(Constant.TEMP_PATH, T.IdUtil.fastSimpleUUID());
+ try {
+ zipFile = new ZipFile(apkPath);
+ ZipEntry entry = zipFile.getEntry(fileName);
+ InputStream inputStream = zipFile.getInputStream(entry);
+ BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempIconFile), 1024);
+ byte[] b = new byte[1024];
+ BufferedInputStream bis = new BufferedInputStream(inputStream, 1024);
+ while (bis.read(b) != -1) {
+ bos.write(b);
+ }
+ IoUtil.flush(bos);
+ T.IoUtil.close(bos);
+ T.IoUtil.close(bis);
+ T.IoUtil.close(inputStream);
+ T.IoUtil.close(zipFile);
+
+ String base64Str = Base64.getEncoder().encodeToString(T.FileUtil.readBytes(tempIconFile));
+ return base64Str;
+ } catch (IOException e) {
+ log.error(e, "[extractFileFromApk] [error]");
+ } finally {
+ T.FileUtil.del(tempIconFile);
+ T.IoUtil.close(zipFile);
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/common/APIException.java b/src/main/java/net/geedge/common/APIException.java
new file mode 100644
index 0000000..d3ff76a
--- /dev/null
+++ b/src/main/java/net/geedge/common/APIException.java
@@ -0,0 +1,47 @@
+package net.geedge.common;
+
+import lombok.Data;
+
+/**
+ * 自定义异常
+ */
+@Data
+public class APIException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ private String msg;
+ private int code = RCode.ERROR.getCode();
+ private Object[] param = new Object[]{};
+ private RCode rCode;
+
+ public APIException(RCode rCode) {
+ super(rCode.getMsg());
+ this.code = rCode.getCode();
+ this.msg = rCode.getMsg();
+ this.param = rCode.getParam();
+ this.rCode = rCode;
+ }
+
+ public APIException(String msg) {
+ super(msg);
+ this.msg = msg;
+ }
+
+ public APIException(String msg, Throwable e) {
+ super(msg, e);
+ this.msg = msg;
+ }
+
+ public APIException(String msg, int code) {
+ super(msg);
+ this.msg = msg;
+ this.code = code;
+ }
+
+ public APIException(String msg, int code, Throwable e) {
+ super(msg, e);
+ this.msg = msg;
+ this.code = code;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/common/APIExceptionHandler.java b/src/main/java/net/geedge/common/APIExceptionHandler.java
new file mode 100644
index 0000000..89f25cc
--- /dev/null
+++ b/src/main/java/net/geedge/common/APIExceptionHandler.java
@@ -0,0 +1,39 @@
+package net.geedge.common;
+
+import cn.hutool.log.Log;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.catalina.connector.ClientAbortException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class APIExceptionHandler {
+
+ private static final Log log = Log.get();
+
+ @ExceptionHandler(APIException.class)
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST)
+ public R handleNZException(APIException e) {
+ R r = new R();
+ r.put("code", e.getCode());
+ r.put("msg", e.getMsg());
+ return r;
+ }
+
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleException(Exception e, HttpServletRequest request) {
+ if (e instanceof ClientAbortException) {
+ return null;
+ }
+ log.error(e, "Request uri: {}", request.getRequestURI());
+ return R.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/common/Constant.java b/src/main/java/net/geedge/common/Constant.java
new file mode 100644
index 0000000..b1b4f6b
--- /dev/null
+++ b/src/main/java/net/geedge/common/Constant.java
@@ -0,0 +1,17 @@
+package net.geedge.common;
+
+import java.io.File;
+
+public class Constant {
+ /**
+ * 临时目录
+ */
+ public static final String TEMP_PATH = System.getProperty("user.dir") + File.separator + "tmp";
+
+ static {
+ File tempPath = T.FileUtil.file(TEMP_PATH);
+ // 程序启动清空临时目录
+ T.FileUtil.del(tempPath);
+ T.FileUtil.mkdir(tempPath);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/common/R.java b/src/main/java/net/geedge/common/R.java
new file mode 100644
index 0000000..7468ab5
--- /dev/null
+++ b/src/main/java/net/geedge/common/R.java
@@ -0,0 +1,90 @@
+package net.geedge.common;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 返回数据
+ * <p>
+ * 错误码、错误内容统一在枚举类RCode中定义, 错误码格式见RCode注释,错误码内容必须用英文,作为国际化的code 自定义的错误类型必须加注释
+ */
+public class R extends HashMap<String, Object> {
+
+ private static final long serialVersionUID = 1L;
+
+ public R() {
+ put("code", RCode.SUCCESS.getCode());
+ put("msg", RCode.SUCCESS.getMsg());
+ put("timestamp", T.DateUtil.current());
+ }
+
+ public static R ok() {
+ return new R();
+ }
+
+ public static R ok(String msg) {
+ R r = new R();
+ r.put("msg", msg);
+ return r;
+ }
+
+ public static R ok(Object data) {
+ R r = new R();
+ r.put("data", data);
+ return r;
+ }
+
+ public static R error() {
+ return error(RCode.ERROR.getCode(), RCode.ERROR.getMsg());
+ }
+
+ public static R error(RCode rCode) {
+ R r = new R();
+ r.put("code", rCode.getCode());
+ r.put("msg", rCode.getMsg());
+ return r;
+ }
+
+ public static R error(String msg) {
+ R r = new R();
+ r.put("code", RCode.ERROR.getCode());
+ r.put("msg", msg);
+ return r;
+ }
+
+ public static R error(Integer code, String msg) {
+ R r = new R();
+ r.put("code", code);
+ r.put("msg", msg);
+ return r;
+ }
+
+ @Override
+ public R put(String key, Object value) {
+ super.put(key, value);
+ return this;
+ }
+
+ public R putData(Object value) {
+ this.put("data", value);
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public R putData(String key, Object value) {
+ Object data = super.getOrDefault("data", new LinkedHashMap<String, Object>());
+ if (!(data instanceof Map)) {
+ throw new APIException("data put error");
+ }
+ ((Map<String, Object>) data).put(key, value);
+ super.put("data", data);
+ return this;
+ }
+
+ @SuppressWarnings("all")
+ public R putAllData(Map m) {
+ super.putAll(m);
+ return this;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/common/RCode.java b/src/main/java/net/geedge/common/RCode.java
new file mode 100644
index 0000000..a7e6b97
--- /dev/null
+++ b/src/main/java/net/geedge/common/RCode.java
@@ -0,0 +1,42 @@
+package net.geedge.common;
+
+import java.text.MessageFormat;
+
+public enum RCode {
+
+
+ BAD_REQUEST(400, "Bad Request "),
+
+ NOT_EXISTS(404, "No such file or directory"),
+ NOT_PERMISSION(401 , "Permission denied"),
+
+ ERROR(999, "error"), // 通用错误/未知错误
+
+ SUCCESS(200, "success"); // 成功
+
+ RCode(Integer code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ private Integer code;
+ private String msg;
+ private Object[] param;
+
+ public RCode setParam(Object... param) {
+ this.param = param;
+ return this;
+ }
+
+ public Object[] getParam() {
+ return param;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getMsg() {
+ return MessageFormat.format(msg, param);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/net/geedge/common/T.java b/src/main/java/net/geedge/common/T.java
new file mode 100644
index 0000000..2545cfb
--- /dev/null
+++ b/src/main/java/net/geedge/common/T.java
@@ -0,0 +1,215 @@
+package net.geedge.common;
+
+import cn.hutool.core.io.IORuntimeException;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+
+public class T {
+ /**
+ * 时间工具类
+ */
+ public static class DateUtil extends cn.hutool.core.date.DateUtil {
+ }
+
+ /**
+ * 字符串工具类
+ */
+ public static class StrUtil extends cn.hutool.core.util.StrUtil {
+ }
+ /**
+ * 反射工具类
+ *
+ * @author Looly
+ * @since 3.0.9
+ */
+ public static class ReflectUtil extends cn.hutool.core.util.ReflectUtil {
+ }
+
+ /**
+ * Map相关工具类
+ */
+ public static class MapUtil extends cn.hutool.core.map.MapUtil {
+ }
+
+ /**
+ * 集合工具类
+ */
+ public static class ListUtil extends cn.hutool.core.collection.ListUtil {
+ }
+ /**
+ * 字符集工具类
+ *
+ * @author xiaoleilu
+ */
+ public static class CharsetUtil extends cn.hutool.core.util.CharsetUtil {
+ }
+ /**
+ * ID生成器工具类,此工具类中主要封装:
+ *
+ * <pre>
+ * 1. 唯一性ID生成器:UUID、ObjectId(MongoDB)、Snowflake
+ * </pre>
+ *
+ * <p>
+ * ID相关文章见:http://calvin1978.blogcn.com/articles/uuid.html
+ *
+ * @author looly
+ * @since 4.1.13
+ */
+ public static class IdUtil extends cn.hutool.core.util.IdUtil {
+ }
+ /**
+ * 线程池工具
+ *
+ * @author luxiaolei
+ */
+ public static class ThreadUtil extends cn.hutool.core.thread.ThreadUtil {
+ }
+ /**
+ * json 工具类
+ */
+ public static class JSONUtil extends cn.hutool.json.JSONUtil {
+ }
+ /**
+ * 系统运行时工具类,用于执行系统命令的工具
+ *
+ * @author Looly
+ * @since 3.1.1
+ */
+ public static class RuntimeUtil extends cn.hutool.core.util.RuntimeUtil {
+ }
+
+ /**
+ * 文件工具类
+ *
+ * @author looly
+ */
+ public static class FileUtil extends cn.hutool.core.io.FileUtil {
+ }
+ /**
+ * 压缩工具类
+ *
+ * @author Looly
+ */
+ public static class ZipUtil extends cn.hutool.core.util.ZipUtil {
+ }
+ /**
+ * IO工具类<br>
+ * IO工具类只是辅助流的读写,并不负责关闭流。原因是流可能被多次读写,读写关闭后容易造成问题。
+ *
+ * @author xiaoleilu
+ */
+ public static class IoUtil extends cn.hutool.core.io.IoUtil {
+ }
+ /**
+ * URL(Uniform Resource Locator)统一资源定位符相关工具类
+ *
+ * <p>
+ * 统一资源定位符,描述了一台特定服务器上某资源的特定位置。
+ * </p>
+ * URL组成:
+ *
+ * <pre>
+ * 协议://主机名[:端口]/ 路径/[:参数] [?查询]#Fragment
+ * protocol :// hostname[:port] / path / [:parameters][?query]#fragment
+ * </pre>
+ *
+ * @author xiaoleilu
+ */
+ public static class URLUtil extends cn.hutool.core.util.URLUtil {
+ }
+
+ /**
+ * CommandLineUtil
+ *
+ * @version org.apache.commons.commons-exec:1.3
+ * @apiNote copy from rg.apache.commons.exec.CommandLine.translateCommandline
+ */
+ public static class CommandLineUtil {
+ /**
+ * translateCommandline
+ *
+ * @param toProcess
+ * @return
+ */
+ public static String[] translateCommandline(String toProcess) {
+ if (toProcess != null && toProcess.length() != 0) {
+ int state = 0;
+ StringTokenizer tok = new StringTokenizer(toProcess, "\"' ", true);
+ ArrayList<String> list = new ArrayList();
+ StringBuilder current = new StringBuilder();
+ boolean lastTokenHasBeenQuoted = false;
+
+ while (true) {
+ while (tok.hasMoreTokens()) {
+ String nextTok = tok.nextToken();
+ switch (state) {
+ case 1:
+ if ("'".equals(nextTok)) {
+ lastTokenHasBeenQuoted = true;
+ state = 0;
+ } else {
+ current.append(nextTok);
+ }
+ continue;
+ case 2:
+ if ("\"".equals(nextTok)) {
+ lastTokenHasBeenQuoted = true;
+ state = 0;
+ } else {
+ current.append(nextTok);
+ }
+ continue;
+ }
+
+ if ("'".equals(nextTok)) {
+ state = 1;
+ } else if ("\"".equals(nextTok)) {
+ state = 2;
+ } else if (" ".equals(nextTok)) {
+ if (lastTokenHasBeenQuoted || current.length() != 0) {
+ list.add(current.toString());
+ current = new StringBuilder();
+ }
+ } else {
+ current.append(nextTok);
+ }
+
+ lastTokenHasBeenQuoted = false;
+ }
+
+ if (lastTokenHasBeenQuoted || current.length() != 0) {
+ list.add(current.toString());
+ }
+
+ if (state != 1 && state != 2) {
+ String[] args = new String[list.size()];
+ return (String[]) list.toArray(args);
+ }
+
+ throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
+ }
+ } else {
+ return new String[0];
+ }
+ }
+ }
+
+ public static class ResponseUtil {
+ /**
+ * reponse 下载 byte数据
+ */
+ public static void downloadFile(HttpServletResponse response, String filename, byte[] data) throws IORuntimeException, IOException {
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ String fileName = T.URLUtil.encode(filename, T.CharsetUtil.CHARSET_UTF_8);
+ cn.hutool.core.util.ReflectUtil.invoke(response, "addHeader", "Content-Disposition", "attachment; filename=" + fileName);
+ cn.hutool.core.util.ReflectUtil.invoke(response, "addHeader", "Content-Length", "" + data.length);
+ cn.hutool.core.util.ReflectUtil.invoke(response, "setHeader", "Access-Control-Expose-Headers", "Content-Disposition");
+ T.IoUtil.write(response.getOutputStream(), false, data);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..e86d68f
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,12 @@
+spring:
+ profiles:
+ active: prod
+ servlet:
+ context-path: /
+ multipart:
+ max-file-size: 500MB
+ max-request-size: 500MB
+ enabled: true
+
+logging:
+ config:./config/logback-spring.xml
diff --git a/src/test/java/net/geedge/TestJ.java b/src/test/java/net/geedge/TestJ.java
new file mode 100644
index 0000000..3d2f79f
--- /dev/null
+++ b/src/test/java/net/geedge/TestJ.java
@@ -0,0 +1,8 @@
+package net.geedge;
+
+public class TestJ {
+
+ public static void main(String[] args) {
+
+ }
+} \ No newline at end of file