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 status() { Map m = T.MapUtil.builder() .put("platform", "android") .build(); AdbDevice device = this.getAdbDevice(); m.put("status", device.isAvailable() ? "online" : "offline"); Map 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 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 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 getProp() { String result = CommandExec.exec(AdbCommandBuilder.builder() .serial(this.getSerial()) .buildGetpropCommand() .build() ); Map 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 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> futureList = T.ListUtil.list(false); List 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 list = Arrays.asList(statResult.split("\\s+")); Collections.reverse(list); String fullName = list.get(3).replaceAll("'|`", ""); Map 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 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 listApp(String arg) { String result = CommandExec.exec(AdbCommandBuilder.builder() .serial(this.getSerial()) .buildPmListPackagesCommand(arg) .build() ); List listApp = T.ListUtil.list(true); List> 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 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 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(10000), new NamedThreadFactory("API-", true)); } return threadPool; } class CommandExec { public static String exec(List command) { String str = T.RuntimeUtil.execForStr(T.CharsetUtil.CHARSET_UTF_8, command.stream().toArray(String[]::new)); return str.stripTrailing(); } public static Process execForProcess(List command) { Process process = T.RuntimeUtil.exec(command.stream().toArray(String[]::new)); return process; } } }