init: ASW-37 初始化 device-api

This commit is contained in:
shizhendong
2024-08-22 09:22:52 +08:00
parent 94a42089e7
commit 3e306e1a8c
21 changed files with 2220 additions and 93 deletions

207
.gitignore vendored Normal file
View File

@@ -0,0 +1,207 @@
/target/
/.mvn/
#!.mvn/wrapper/maven-wrapper.jar
mvnw
mvnw.cmd
HELP.md
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
# Created by https://www.gitignore.io/api/git,java,maven,eclipse,windows
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# CDT- autotools
.autotools
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
### Eclipse Patch ###
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Annotation Processing
.apt_generated
.sts4-cache/
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Java ###
# Compiled class file
*.class
# Log file
*.log
/log/
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Some additional ignores (sort later)
*.DS_Store
*.sw?
.#*
*#
*~
.classpath
.project
.settings
bin
build
target
dependency-reduced-pom.xml
*.sublime-*
/scratch
.gradle
README.html
*.iml
.idea
.exercism

View File

@@ -1,93 +0,0 @@
# device-api
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://git.mesalab.cn/appsketch-works/device-api.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://git.mesalab.cn/appsketch-works/device-api/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

125
pom.xml Normal file
View File

@@ -0,0 +1,125 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<groupId>net.geedge</groupId>
<artifactId>device-api</artifactId>
<version>1.0</version>
<name>device-api</name>
<description>AppSketch Works Device API</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.6</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>**/application-*.yml</exclude>
<exclude>**/logback-spring.xml</exclude>
</excludes>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>
net.geedge.DeviceApiApplication
</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
<!-- 跳过单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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("*");
}
}

View File

@@ -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)));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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、ObjectIdMongoDB、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 {
}
/**
* URLUniform 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);
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
package net.geedge;
public class TestJ {
public static void main(String[] args) {
}
}