project: 增加web和adb控制,未测试

This commit is contained in:
2026-07-17 19:17:10 +08:00
parent 5fb7b58fc3
commit b8edd5eff5
49 changed files with 5061 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.MaterialComponents.DayNight">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,89 @@
package com.ttstd.adbloopback;
import com.ttstd.adbloopback.adb.AdbConnection;
import com.ttstd.adbloopback.adb.AdbKeyPair;
import com.ttstd.adbloopback.adb.AdbPairing;
import com.ttstd.adbloopback.adb.AdbStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
/**
* ADB 管理器:封装密钥持久化、配对、连接及 Shell 指令执行。
* 它是上层 UI 与底层 ADB 协议库之间的桥梁。
*/
public class AdbManager {
private final File privateKeyFile;
private final File publicKeyFile;
private AdbKeyPair keyPair;
private AdbConnection connection;
public AdbManager(File filesDir) {
this.privateKeyFile = new File(filesDir, "adb_private.key");
this.publicKeyFile = new File(filesDir, "adb_public.key");
loadOrCreateKeyPair();
}
private void loadOrCreateKeyPair() {
try {
if (privateKeyFile.exists() && publicKeyFile.exists()) {
keyPair = AdbKeyPair.read(privateKeyFile, publicKeyFile);
} else {
keyPair = AdbKeyPair.generate();
keyPair.write(privateKeyFile, publicKeyFile);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/** 发起无线调试配对TLS-PSK */
public void pair(int port, String pairingCode) throws Exception {
AdbPairing.pair("127.0.0.1", port, pairingCode, keyPair);
}
/** 连接无线调试主端口 */
public void connect(int port) throws Exception {
if (connection != null) {
try { connection.close(); } catch (Exception ignored) {}
}
connection = AdbConnection.connect("127.0.0.1", port, keyPair);
}
/** 断开连接 */
public void disconnect() {
if (connection != null) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
connection = null;
}
}
/** 执行 Shell 命令并获取全部输出 */
public String execShell(String command) throws Exception {
if (connection == null || !connection.isConnected()) {
throw new IOException("ADB 未连接");
}
AdbStream stream = connection.open("shell:" + command);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] data;
while ((data = stream.read()) != null) {
bos.write(data);
}
stream.close();
return bos.toString("UTF-8");
}
/** 快速发送点击指令 */
public void tap(int x, int y) throws Exception {
execShell("input tap " + x + " " + y);
}
public boolean isConnected() {
return connection != null && connection.isConnected();
}
}

View File

@@ -0,0 +1,154 @@
package com.ttstd.adbloopback;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputEditText;
public class MainActivity extends AppCompatActivity {
private AdbManager adbManager;
private TextInputEditText etPairPort;
private TextInputEditText etPairCode;
private TextInputEditText etMainPort;
private TextInputEditText etTapX;
private TextInputEditText etTapY;
private TextInputEditText etCmd;
private TextView tvStatus;
private TextView tvLog;
private ScrollView scrollLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
adbManager = new AdbManager(getFilesDir());
etPairPort = findViewById(R.id.et_pair_port);
etPairCode = findViewById(R.id.et_pair_code);
etMainPort = findViewById(R.id.et_main_port);
etTapX = findViewById(R.id.et_tap_x);
etTapY = findViewById(R.id.et_tap_y);
etCmd = findViewById(R.id.et_cmd);
tvStatus = findViewById(R.id.tv_status);
tvLog = findViewById(R.id.tv_log);
scrollLog = findViewById(R.id.scroll_root);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
log("⚠️ 当前系统低于 Android 11API 30无线调试不可用。");
}
findViewById(R.id.btn_open_settings).setOnClickListener(v -> openDeveloperOptions());
findViewById(R.id.btn_pair).setOnClickListener(v -> doPair());
findViewById(R.id.btn_connect).setOnClickListener(v -> doConnect());
findViewById(R.id.btn_disconnect).setOnClickListener(v -> doDisconnect());
findViewById(R.id.btn_tap).setOnClickListener(v -> doTap());
findViewById(R.id.btn_exec).setOnClickListener(v -> doExec());
}
private void openDeveloperOptions() {
try {
startActivity(new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS));
Toast.makeText(this, "请开启「无线调试」,并记录配对端口/码与主端口", Toast.LENGTH_LONG).show();
} catch (Exception e) {
Toast.makeText(this, "无法打开开发者选项,请手动进入", Toast.LENGTH_LONG).show();
}
}
private void doPair() {
String portStr = etPairPort.getText().toString().trim();
String code = etPairCode.getText().toString().trim();
if (portStr.isEmpty() || code.isEmpty()) {
Toast.makeText(this, "请填写配对端口与配对码", Toast.LENGTH_SHORT).show();
return;
}
int port = Integer.parseInt(portStr);
runBg("配对", () -> {
adbManager.pair(port, code);
log("✅ 配对成功TLS-PSK 完成,公钥已加入 adbd 授权列表)");
});
}
private void doConnect() {
String portStr = etMainPort.getText().toString().trim();
if (portStr.isEmpty()) {
Toast.makeText(this, "请填写无线调试主端口", Toast.LENGTH_SHORT).show();
return;
}
int port = Integer.parseInt(portStr);
runBg("连接", () -> {
adbManager.connect(port);
log("✅ 已连接无线调试主端口 " + port + ",获得 ADB 权限管道");
runOnUiThread(() -> tvStatus.setText("状态已连接ADB 权限)"));
});
}
private void doDisconnect() {
runBg("断开", () -> {
adbManager.disconnect();
runOnUiThread(() -> tvStatus.setText("状态:未连接"));
log("🔌 已断开连接");
});
}
private void doTap() {
String xStr = etTapX.getText().toString().trim();
String yStr = etTapY.getText().toString().trim();
if (xStr.isEmpty() || yStr.isEmpty()) {
Toast.makeText(this, "请填写 X/Y 坐标", Toast.LENGTH_SHORT).show();
return;
}
int x = Integer.parseInt(xStr);
int y = Integer.parseInt(yStr);
runBg("input tap", () -> {
adbManager.tap(x, y);
log("👆 已发送 input tap " + x + " " + y);
});
}
private void doExec() {
String cmd = etCmd.getText().toString().trim();
if (cmd.isEmpty()) {
Toast.makeText(this, "请输入 shell 命令", Toast.LENGTH_SHORT).show();
return;
}
runBg("exec: " + cmd, () -> {
String out = adbManager.execShell(cmd);
log("" + cmd + "\n" + (out.isEmpty() ? "(无输出)" : out));
});
}
private interface ThrowingRunnable {
void run() throws Exception;
}
private void runBg(String name, ThrowingRunnable task) {
log("⏳ 执行 " + name + "");
new Thread(() -> {
try {
task.run();
} catch (Exception e) {
log("" + name + " 失败:" + e.getMessage());
e.printStackTrace();
}
}).start();
}
private void log(String line) {
runOnUiThread(() -> {
tvLog.append(line + "\n");
scrollLog.post(() -> scrollLog.fullScroll(View.FOCUS_DOWN));
});
}
}

View File

@@ -0,0 +1,110 @@
package com.ttstd.adbloopback.adb;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 自建 ADB 客户端连接:与系统 adbd无线调试守护进程建立明文 TCP 连接,
* 完成 CNXN / AUTH 握手,然后可开启 shell 流执行指令。
*/
public class AdbConnection {
private final Socket socket;
private final AdbKeyPair keyPair;
private InputStream in;
private OutputStream out;
private boolean connected = false;
private int lastLocalId = 1;
public AdbConnection(Socket socket, AdbKeyPair keyPair) {
this.socket = socket;
this.keyPair = keyPair;
}
public static AdbConnection connect(String host, int port, AdbKeyPair keyPair) throws Exception {
Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), 5000);
s.setSoTimeout(2000); // 读超时,供 shell 读取在无输出时及时返回
s.setTcpNoDelay(true);
AdbConnection c = new AdbConnection(s, keyPair);
c.connect();
return c;
}
private void connect() throws Exception {
in = socket.getInputStream();
out = socket.getOutputStream();
// 1) 发送 CNXN
AdbMessage.write(out, new AdbMessage(AdbConstants.CNXN, AdbConstants.VERSION,
AdbConstants.MAX_PAYLOAD, "device::adb-loopback-demo".getBytes("UTF-8")));
// 2) 处理 AUTH 挑战,直到收到 CNXN 确认
while (true) {
AdbMessage resp = readMessage();
if (resp.command == AdbConstants.AUTH) {
int sub = resp.arg0;
if (sub == AdbConstants.AUTH_TOKEN) {
byte[] signature = keyPair.sign(resp.data);
AdbMessage.write(out, new AdbMessage(AdbConstants.AUTH,
AdbConstants.AUTH_SIGNATURE, 0, signature));
} else if (sub == AdbConstants.AUTH_RSAPUBLICKEY) {
// 某些场景下服务端主动索要公钥,一并提交
AdbMessage.write(out, new AdbMessage(AdbConstants.AUTH,
AdbConstants.AUTH_RSAPUBLICKEY, 0,
keyPair.getAdbPublicKey().getBytes("UTF-8")));
} else {
throw new IOException("ADB AUTH 失败sub=" + sub);
}
} else if (resp.command == AdbConstants.CNXN) {
connected = true;
break;
} else {
throw new IOException("ADB 握手收到未知消息: 0x"
+ Integer.toHexString(resp.command));
}
}
if (!connected) {
throw new IOException("ADB 连接建立失败");
}
}
public AdbStream open(String destination) throws Exception {
int localId = lastLocalId++;
AdbMessage.write(out, new AdbMessage(AdbConstants.OPEN, localId, 0,
(destination + "\0").getBytes("UTF-8")));
AdbMessage resp = readMessage();
if (resp.command == AdbConstants.OKAY) {
AdbStream stream = new AdbStream(this, localId, destination);
stream.setRemoteId(resp.arg0);
return stream;
} else if (resp.command == AdbConstants.CLSE) {
throw new IOException("ADB 流打开被拒绝");
} else {
throw new IOException("ADB 打开流收到未知响应: 0x"
+ Integer.toHexString(resp.command));
}
}
synchronized void sendInternal(int command, int arg0, int arg1, byte[] data) throws IOException {
AdbMessage.write(out, new AdbMessage(command, arg0, arg1, data));
}
synchronized AdbMessage readMessage() throws IOException {
return AdbMessage.read(in);
}
public void close() throws IOException {
socket.close();
connected = false;
}
public boolean isConnected() {
return connected;
}
}

View File

@@ -0,0 +1,24 @@
package com.ttstd.adbloopback.adb;
/** ADB 网络协议常量(协议字段均为小端序 / little-endian。 */
public final class AdbConstants {
// 消息命令4 字节 ASCII按小端读取
public static final int CNXN = 0x4e584e43; // "CNXN"
public static final int AUTH = 0x48545541; // "AUTH"
public static final int OPEN = 0x4e45504f; // "OPEN"
public static final int OKAY = 0x59414b4f; // "OKAY"
public static final int CLSE = 0x45534c43; // "CLSE"
public static final int WRTE = 0x45545257; // "WRTE"
// AUTH 子类型
public static final int AUTH_TOKEN = 1; // 服务端下发的随机挑战
public static final int AUTH_SIGNATURE = 2; // 客户端对挑战的签名
public static final int AUTH_RSAPUBLICKEY = 3; // 客户端公钥
public static final int VERSION = 0x01000000; // ADB 协议版本 1.0(小端)
public static final int MAX_PAYLOAD = 4096;
public static final String TAG = "AdbLoopback";
private AdbConstants() {}
}

View File

@@ -0,0 +1,144 @@
package com.ttstd.adbloopback.adb;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.interfaces.RSAPublicKey;
/**
* ADB 密钥对:负责生成 RSA 密钥、对 AUTH 挑战做 SHA1withRSA 签名,
* 以及把公钥编码成 adbd 可识别的 adb 公钥格式base64(PKCS#1 RSAPublicKey) + 注释)。
*/
public class AdbKeyPair {
private final PrivateKey privateKey;
private final PublicKey publicKey;
public AdbKeyPair(PrivateKey privateKey, PublicKey publicKey) {
this.privateKey = privateKey;
this.publicKey = publicKey;
}
public static AdbKeyPair generate() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048, new SecureRandom());
KeyPair kp = gen.generateKeyPair();
return new AdbKeyPair(kp.getPrivate(), kp.getPublic());
}
public static AdbKeyPair read(File privateFile, File publicFile) throws Exception {
byte[] pk = readAllBytes(privateFile);
byte[] pb = readAllBytes(publicFile);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey priv = kf.generatePrivate(new PKCS8EncodedKeySpec(pk));
PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(pb));
return new AdbKeyPair(priv, pub);
}
public void write(File privateFile, File publicFile) throws Exception {
KeyFactory kf = KeyFactory.getInstance("RSA");
byte[] pk = kf.getKeySpec(privateKey, PKCS8EncodedKeySpec.class).getEncoded();
byte[] pb = kf.getKeySpec(publicKey, X509EncodedKeySpec.class).getEncoded();
writeAllBytes(privateFile, pk);
writeAllBytes(publicFile, pb);
}
public PrivateKey getPrivateKey() {
return privateKey;
}
public PublicKey getPublicKey() {
return publicKey;
}
/** 对服务端下发的 AUTH token 做签名SHA1(token) 再用 RSA PKCS#1 v1.5 签名。 */
public byte[] sign(byte[] token) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] hash = md.digest(token);
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initSign(privateKey);
sig.update(hash);
return sig.sign();
}
/**
* 生成 adb 格式公钥字符串base64( DER(PKCS#1 RSAPublicKey) ) + " " + 注释 + "\n"
* adbd 解析时只取 base64 部分,按 (modulus || exponent) 还原 RSA 公钥。
*/
public String getAdbPublicKey() throws Exception {
RSAPublicKey rsa = (RSAPublicKey) publicKey;
byte[] modulus = toDerInteger(rsa.getModulus());
byte[] exponent = toDerInteger(rsa.getPublicExponent());
byte[] pkcs1 = encodeSequence(modulus, exponent);
String b64 = Base64.encodeToString(pkcs1, Base64.NO_WRAP);
return b64 + " unknown@unknown\n";
}
// ---- DER 编码辅助 ----
private static byte[] toDerInteger(BigInteger v) {
return derTlv((byte) 0x02, v.toByteArray());
}
private static byte[] encodeSequence(byte[]... parts) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
for (byte[] p : parts) bos.write(p);
return derTlv((byte) 0x30, bos.toByteArray());
}
private static byte[] derTlv(byte tag, byte[] content) {
return concat(new byte[]{tag}, derLen(content.length), content);
}
private static byte[] derLen(int len) {
if (len < 0x80) {
return new byte[]{(byte) len};
} else if (len < 0x100) {
return new byte[]{(byte) 0x81, (byte) len};
} else {
return new byte[]{(byte) 0x82, (byte) (len >> 8), (byte) len};
}
}
private static byte[] concat(byte[] a, byte[] b) {
byte[] r = new byte[a.length + b.length];
System.arraycopy(a, 0, r, 0, a.length);
System.arraycopy(b, 0, r, a.length, b.length);
return r;
}
private static byte[] concat(byte[] a, byte[] b, byte[] c) {
return concat(concat(a, b), c);
}
private static byte[] readAllBytes(File f) throws IOException {
try (FileInputStream in = new FileInputStream(f)) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) > 0) bos.write(buf, 0, n);
return bos.toByteArray();
}
}
private static void writeAllBytes(File f, byte[] data) throws IOException {
try (FileOutputStream out = new FileOutputStream(f)) {
out.write(data);
}
}
}

View File

@@ -0,0 +1,84 @@
package com.ttstd.adbloopback.adb;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.CRC32;
/**
* ADB 消息封装与读写。
*
* <pre>
* struct message {
* unsigned command; // 命令
* unsigned arg0; // 第一参数(本地 id
* unsigned arg1; // 第二参数(远端 id
* unsigned data_length; // 负载长度
* unsigned data_crc32; // 负载 CRC32
* unsigned magic; // command ^ 0xffffffff
* };
* </pre>
* 所有整型字段均为小端序little-endian
*/
public class AdbMessage {
public int command;
public int arg0;
public int arg1;
public byte[] data;
public AdbMessage(int command, int arg0, int arg1, byte[] data) {
this.command = command;
this.arg0 = arg0;
this.arg1 = arg1;
this.data = data != null ? data : new byte[0];
}
public static void write(OutputStream out, AdbMessage msg) throws IOException {
byte[] payload = msg.data;
int crc = crc32(payload);
ByteBuffer buf = ByteBuffer.allocate(24 + payload.length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(msg.command);
buf.putInt(msg.arg0);
buf.putInt(msg.arg1);
buf.putInt(payload.length);
buf.putInt(crc);
buf.putInt(msg.command ^ 0xffffffff);
buf.put(payload);
out.write(buf.array());
out.flush();
}
public static AdbMessage read(InputStream in) throws IOException {
byte[] header = readExactly(in, 24);
ByteBuffer buf = ByteBuffer.wrap(header);
buf.order(ByteOrder.LITTLE_ENDIAN);
int command = buf.getInt();
int arg0 = buf.getInt();
int arg1 = buf.getInt();
int len = buf.getInt();
/* int crc = */ buf.getInt();
/* int magic = */ buf.getInt();
byte[] data = len > 0 ? readExactly(in, len) : new byte[0];
return new AdbMessage(command, arg0, arg1, data);
}
public static byte[] readExactly(InputStream in, int n) throws IOException {
byte[] b = new byte[n];
int off = 0;
while (off < n) {
int r = in.read(b, off, n - off);
if (r < 0) throw new IOException("ADB 连接意外断开 (EOF)");
off += r;
}
return b;
}
public static int crc32(byte[] data) {
CRC32 c = new CRC32();
c.update(data);
return (int) c.getValue();
}
}

View File

@@ -0,0 +1,88 @@
package com.ttstd.adbloopback.adb;
import com.ttstd.adbloopback.tls.TlsPskClient;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 无线调试配对:在 TLS-PSK 通道之上,与 adbd pairing 守护进程交换 RSA 公钥。
*
* <p>配对成功后,本 App 的公钥被加入 adbd 的授权列表,之后即可用同一密钥对
* 无线调试主端口发起 ADB 连接(无需电脑 / Shizuku
*
* <p>线规AOSP pairing 协议):每条消息 = 4 字节头 [type(1)][reserved(1)][len(2, 大端)] + payload。
*/
public class AdbPairing {
// AOSP pairing.h MsgType
private static final byte MSG_PING = 0;
private static final byte MSG_PONG = 1;
private static final byte MSG_PAIRING_REQ = 2;
private static final byte MSG_PAIRING_RESULT = 3;
private static final byte MSG_PAIRING_ACK = 4;
public static void pair(String host, int port, String pairingCode, AdbKeyPair keyPair) throws Exception {
// 配对码同时作为 TLS-PSK 的 PSK 与 identity与 AOSP adb pair 一致)
byte[] psk = pairingCode.getBytes("UTF-8");
TlsPskClient tls = new TlsPskClient(host, port, pairingCode, psk);
tls.handshake();
InputStream in = tls.getInputStream();
OutputStream out = tls.getOutputStream();
// 向服务端提交本 App 的 ADB 公钥
byte[] pubKey = keyPair.getAdbPublicKey().getBytes("UTF-8");
writePairingMsg(out, MSG_PAIRING_REQ, pubKey);
// 读取服务端回应(可能先有 Ping然后 PairingResult
boolean paired = false;
for (int i = 0; i < 4; i++) {
PairingMsg msg = readPairingMsg(in);
if (msg.type == MSG_PING) {
writePairingMsg(out, MSG_PONG, new byte[0]);
} else if (msg.type == MSG_PAIRING_RESULT) {
paired = true;
break;
} else {
throw new IOException("无线调试配对收到意外消息类型: " + msg.type);
}
}
if (!paired) {
throw new IOException("无线调试配对未完成");
}
// 回 ACK部分实现需要
try {
writePairingMsg(out, MSG_PAIRING_ACK, new byte[0]);
} catch (IOException ignored) {
}
}
private static void writePairingMsg(OutputStream out, byte type, byte[] payload) throws IOException {
byte[] header = {type, 0x00, (byte) (payload.length >> 8), (byte) payload.length};
out.write(header);
out.write(payload);
out.flush();
}
private static PairingMsg readPairingMsg(InputStream in) throws IOException {
byte[] header = AdbMessage.readExactly(in, 4);
byte type = header[0];
int len = ((header[2] & 0xff) << 8) | (header[3] & 0xff);
byte[] payload = len > 0 ? AdbMessage.readExactly(in, len) : new byte[0];
return new PairingMsg(type, payload);
}
private static class PairingMsg {
final byte type;
@SuppressWarnings("unused")
final byte[] payload;
PairingMsg(byte type, byte[] payload) {
this.type = type;
this.payload = payload;
}
}
}

View File

@@ -0,0 +1,96 @@
package com.ttstd.adbloopback.adb;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;
/**
* ADB 流(对应 ADB 协议里的 OPEN/WRTE/OKAY/CLSE 多路复用)。
* 本 demo 仅在单流场景下使用,内部通过 connection 的同步读写防止消息交错。
*/
public class AdbStream {
private final AdbConnection connection;
private final int localId;
private final String destination;
private int remoteId = -1;
private boolean closed = false;
private final Queue<byte[]> incoming = new LinkedList<>();
private final Object lock = new Object();
AdbStream(AdbConnection connection, int localId, String destination) {
this.connection = connection;
this.localId = localId;
this.destination = destination;
}
void setRemoteId(int remoteId) {
this.remoteId = remoteId;
}
public int write(byte[] data) throws IOException {
synchronized (lock) {
connection.sendInternal(AdbConstants.WRTE, localId, remoteId, data);
// 等待对端 OKAY其间若收到 WRTE 数据则先缓存起来
while (true) {
AdbMessage msg = connection.readMessage();
if (msg.command == AdbConstants.OKAY) {
break;
} else if (msg.command == AdbConstants.WRTE) {
incoming.add(msg.data);
} else if (msg.command == AdbConstants.CLSE) {
closed = true;
break;
} else {
break;
}
}
}
return data.length;
}
/** 读取对端发来的数据;无数据且对端未关闭时按 socket 超时返回 null。 */
public byte[] read() throws IOException {
synchronized (lock) {
if (!incoming.isEmpty()) {
return incoming.poll();
}
while (!closed) {
try {
AdbMessage msg = connection.readMessage();
if (msg.command == AdbConstants.WRTE) {
incoming.add(msg.data);
return incoming.poll();
} else if (msg.command == AdbConstants.OKAY) {
// 暂无数据,继续等待
} else if (msg.command == AdbConstants.CLSE) {
closed = true;
break;
}
} catch (IOException e) {
// 多为 socket 读超时,返回当前缓存(可能为 null
break;
}
}
return incoming.poll();
}
}
public void close() throws IOException {
synchronized (lock) {
if (closed) return;
try {
connection.sendInternal(AdbConstants.CLSE, localId, remoteId, new byte[0]);
} catch (IOException ignored) {
}
closed = true;
}
}
public boolean isClosed() {
return closed;
}
public String getDestination() {
return destination;
}
}

View File

@@ -0,0 +1,72 @@
package com.ttstd.adbloopback.tls;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
/** TLS 1.2 所需的密码学原语SHA-256、HMAC-SHA256、TLS PRF、AES-128-GCM。 */
public final class TlsCrypto {
public static byte[] sha256(byte[] data) throws Exception {
return MessageDigest.getInstance("SHA-256").digest(data);
}
public static byte[] hmacSha256(byte[] key, byte[] data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
/**
* TLS 1.2 PRF使用 SHA-256
* PRF(secret, label, seed) = P_SHA256(secret, label + seed)
*/
public static byte[] prf(byte[] secret, String label, byte[] seed, int outLen) throws Exception {
byte[] labelSeed = concat(label.getBytes("ASCII"), seed);
byte[] result = new byte[outLen];
byte[] prev = hmacSha256(secret, labelSeed); // A(1)
int generated = 0;
while (generated < outLen) {
byte[] block = hmacSha256(secret, concat(prev, labelSeed));
int take = Math.min(block.length, outLen - generated);
System.arraycopy(block, 0, result, generated, take);
generated += take;
prev = hmacSha256(secret, prev); // A(i+1)
}
return result;
}
public static byte[] aesGcmEncrypt(byte[] key, byte[] nonce12, byte[] plaintext, byte[] aad) throws Exception {
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce12));
c.updateAAD(aad);
return c.doFinal(plaintext);
}
public static byte[] aesGcmDecrypt(byte[] key, byte[] nonce12, byte[] ciphertextWithTag, byte[] aad) throws Exception {
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce12));
c.updateAAD(aad);
return c.doFinal(ciphertextWithTag);
}
public static byte[] concat(byte[]... arrays) {
int len = 0;
for (byte[] a : arrays) {
if (a != null) len += a.length;
}
byte[] r = new byte[len];
int off = 0;
for (byte[] a : arrays) {
if (a != null) {
System.arraycopy(a, 0, r, off, a.length);
off += a.length;
}
}
return r;
}
private TlsCrypto() {}
}

View File

@@ -0,0 +1,332 @@
package com.ttstd.adbloopback.tls;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* 最小化 TLS 1.2 PSK 客户端,仅支持 {@code TLS_PSK_WITH_AES_128_GCM_SHA256}。
*
* <p>Android 11+ 的「无线调试 → 使用配对码配对设备」走的就是这条 TLS-PSK 通道:
* 配对码同时作为 PSK 与 PSK identity。标准 Android (JSSE / Conscrypt) 不暴露 PSK 接口,
* 故这里用 Android 自带密码学原语AES-GCM / HMAC / SHA-256手搓实现。
*
* <p>握手完成后,通过 {@link #getInputStream()}/{@link #getOutputStream()} 获得透明加密的流,
* 上层即可在其上跑 adb 的配对公钥交换协议。
*/
public class TlsPskClient {
// 仅支持这一个密码套件(与 AOSP adbd pairing 一致)
private static final int CIPHER_PSK_AES128_GCM_SHA256 = 0x00A8;
private final Socket socket;
private InputStream in;
private OutputStream out;
private final byte[] psk;
private final byte[] identity;
private final byte[] clientRandom = new byte[32];
private final byte[] serverRandom = new byte[32];
private byte[] clientWriteKey, serverWriteKey, clientWriteIV, serverWriteIV;
private byte[] masterSecret;
private long clientSeq = 0;
private long serverSeq = 0;
private final ByteArrayOutputStream handshakeBuf = new ByteArrayOutputStream();
public TlsPskClient(String host, int port, String identityStr, byte[] psk) throws IOException {
this.psk = psk;
this.identity = identityStr.getBytes("UTF-8");
this.socket = new Socket();
this.socket.connect(new InetSocketAddress(host, port), 8000);
this.socket.setSoTimeout(8000);
this.socket.setTcpNoDelay(true);
this.in = socket.getInputStream();
this.out = socket.getOutputStream();
new SecureRandom().nextBytes(clientRandom);
}
public InputStream getInputStream() {
return new TlsInputStream();
}
public OutputStream getOutputStream() {
return new TlsOutputStream();
}
public void handshake() throws Exception {
sendClientHello();
readServerHelloAndDone();
deriveKeys();
sendClientKeyExchange();
sendChangeCipherSpec();
sendFinished();
readServerFinished();
}
// ---------- 握手各阶段 ----------
private void sendClientHello() throws Exception {
byte[] sigAlgs = {
0x00, 0x0c, // 签名算法列表长度 = 12
0x04, 0x03, // sha256 + ecdsa
0x05, 0x03, // sha384 + ecdsa
0x06, 0x03, // sha512 + ecdsa
0x04, 0x01, // sha256 + rsa
0x05, 0x01, // sha384 + rsa
0x06, 0x01, // sha512 + rsa
0x02, 0x01 // sha1 + rsa
};
byte[] ext = concat(shortBytes(0x000d), shortBytes(sigAlgs.length), sigAlgs);
byte[] extensions = concat(shortBytes(ext.length), ext);
byte[] cipherSuites = concat(shortBytes(2), new byte[]{(byte) 0x00, (byte) 0xA8});
byte[] clientHello = concat(
new byte[]{0x03, 0x03}, // client_version TLS 1.2
clientRandom, // 32 字节
new byte[]{0x00}, // session_id 长度 0
cipherSuites, // 4 字节
new byte[]{0x01, 0x00}, // compression: 1 个方法, null(0x00)
extensions
);
byte[] body = concat(new byte[]{0x01}, int24(clientHello.length), clientHello);
out.write(buildPlainRecord((byte) 22, body));
out.flush();
handshakeBuf.write(body);
}
private void readServerHelloAndDone() throws Exception {
boolean gotServerHello = false;
boolean gotDone = false;
while (!gotDone) {
Record rec = readRecordAny();
if (rec.type == 21) {
throw new IOException("TLS 握手收到 alert: "
+ (rec.payload.length > 1 ? (rec.payload[1] & 0xff) : 0));
}
if (rec.type != 22) {
throw new IOException("期望握手记录,实际类型: " + rec.type);
}
int off = 0;
byte[] p = rec.payload;
while (off < p.length) {
int htype = p[off] & 0xff;
int hlen = ((p[off + 1] & 0xff) << 16) | ((p[off + 2] & 0xff) << 8) | (p[off + 3] & 0xff);
byte[] hbody = Arrays.copyOfRange(p, off + 4, off + 4 + hlen);
handshakeBuf.write(p, off, 4 + hlen);
if (htype == 2) {
parseServerHello(hbody);
gotServerHello = true;
} else if (htype == 14) {
gotDone = true;
}
off += 4 + hlen;
}
}
if (!gotServerHello) {
throw new IOException("未收到 ServerHello");
}
}
private void parseServerHello(byte[] b) throws IOException {
System.arraycopy(b, 2, serverRandom, 0, 32); // version(2) + random(32)
int sidLen = b[34] & 0xff;
int pos = 35 + sidLen;
int cs = ((b[pos] & 0xff) << 8) | (b[pos + 1] & 0xff);
if (cs != CIPHER_PSK_AES128_GCM_SHA256) {
throw new IOException("服务端未选择 PSK_AES_128_GCM 密码套件,实际: 0x"
+ Integer.toHexString(cs));
}
}
private void deriveKeys() throws Exception {
// PSK 预主密钥other_secret(空) || psk
byte[] premaster = concat(new byte[]{0x00, 0x00}, shortBytes(psk.length), psk);
masterSecret = TlsCrypto.prf(premaster, "master secret",
concat(clientRandom, serverRandom), 48);
byte[] keyBlock = TlsCrypto.prf(masterSecret, "key expansion",
concat(serverRandom, clientRandom), 40);
clientWriteKey = Arrays.copyOfRange(keyBlock, 0, 16);
serverWriteKey = Arrays.copyOfRange(keyBlock, 16, 32);
clientWriteIV = Arrays.copyOfRange(keyBlock, 32, 36);
serverWriteIV = Arrays.copyOfRange(keyBlock, 36, 40);
}
private void sendClientKeyExchange() throws Exception {
byte[] body = concat(new byte[]{0x10}, int24(2 + identity.length),
shortBytes(identity.length), identity);
out.write(buildPlainRecord((byte) 22, body));
out.flush();
handshakeBuf.write(body);
}
private void sendChangeCipherSpec() throws IOException {
out.write(new byte[]{20, 3, 3, 0, 1, 1});
out.flush();
}
private void sendFinished() throws Exception {
byte[] hash = TlsCrypto.sha256(handshakeBuf.toByteArray());
byte[] verify = TlsCrypto.prf(masterSecret, "client finished", hash, 12);
byte[] body = concat(new byte[]{0x14}, int24(12), verify);
handshakeBuf.write(body); // 计入,供服务端 Finished 校验
byte[] record = buildEncryptedRecord((byte) 23, body);
out.write(record);
out.flush();
}
private void readServerFinished() throws Exception {
Record r = readRecordAny();
if (r.type == 20) { // 服务端 ChangeCipherSpec明文
r = readRecordAny();
}
if (r.type != 23) {
throw new IOException("期望加密 Finished实际类型: " + r.type);
}
byte[] plaintext = decryptRecord(r);
byte[] expected = TlsCrypto.prf(masterSecret, "server finished",
TlsCrypto.sha256(handshakeBuf.toByteArray()), 12);
byte[] actual = Arrays.copyOfRange(plaintext, 4, 16);
if (!Arrays.equals(expected, actual)) {
throw new IOException("TLS Finished 校验失败");
}
}
// ---------- 记录层 ----------
private static class Record {
int type;
byte[] payload;
Record(int type, byte[] payload) {
this.type = type;
this.payload = payload;
}
}
private Record readRecordAny() throws IOException {
byte[] header = readExactly(5);
int type = header[0] & 0xff;
int len = ((header[3] & 0xff) << 8) | (header[4] & 0xff);
byte[] payload = readExactly(len);
return new Record(type, payload);
}
private byte[] buildPlainRecord(byte contentType, byte[] body) {
byte[] header = {contentType, 3, 3, (byte) (body.length >> 8), (byte) body.length};
return concat(header, body);
}
private byte[] buildEncryptedRecord(byte contentType, byte[] plaintext) throws Exception {
long seq = clientSeq++;
byte[] explicit = longTo8(seq);
byte[] nonce = concat(clientWriteIV, explicit);
byte[] aad = {contentType, 3, 3, (byte) (plaintext.length >> 8), (byte) plaintext.length};
byte[] ct = TlsCrypto.aesGcmEncrypt(clientWriteKey, nonce, plaintext, aad);
byte[] payload = concat(explicit, ct);
byte[] header = {contentType, 3, 3, (byte) (payload.length >> 8), (byte) payload.length};
return concat(header, payload);
}
private byte[] decryptRecord(Record r) throws Exception {
byte[] payload = r.payload;
byte[] explicit = Arrays.copyOfRange(payload, 0, 8);
byte[] ct = Arrays.copyOfRange(payload, 8, payload.length);
byte[] nonce = concat(serverWriteIV, explicit);
int plainLen = payload.length - 8 - 16;
byte[] aad = {23, 3, 3, (byte) (plainLen >> 8), (byte) plainLen};
return TlsCrypto.aesGcmDecrypt(serverWriteKey, nonce, ct, aad);
}
private byte[] readExactly(int n) throws IOException {
byte[] b = new byte[n];
int off = 0;
while (off < n) {
int r = in.read(b, off, n - off);
if (r < 0) throw new IOException("TLS 连接断开 (EOF)");
off += r;
}
return b;
}
// ---------- 透明加密流 ----------
private class TlsInputStream extends InputStream {
private byte[] buf = new byte[0];
private int bufPos = 0;
@Override
public int read() throws IOException {
byte[] b = new byte[1];
int n = read(b, 0, 1);
return n <= 0 ? -1 : (b[0] & 0xff);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (bufPos >= buf.length) {
Record r = readRecordAny();
if (r.type != 23) {
throw new IOException("TLS 流期望应用数据记录,实际: " + r.type);
}
try {
buf = decryptRecord(r);
} catch (Exception e) {
throw new IOException("TLS 解密失败", e);
}
bufPos = 0;
}
int avail = buf.length - bufPos;
int n = Math.min(len, avail);
System.arraycopy(buf, bufPos, b, off, n);
bufPos += n;
return n;
}
}
private class TlsOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
write(new byte[]{(byte) b}, 0, 1);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
byte[] slice = Arrays.copyOfRange(b, off, off + len);
try {
byte[] record = buildEncryptedRecord((byte) 23, slice);
out.write(record);
out.flush();
} catch (Exception e) {
throw new IOException("TLS 加密失败", e);
}
}
}
// ---------- 字节工具 ----------
private static byte[] shortBytes(int v) {
return new byte[]{(byte) (v >> 8), (byte) v};
}
private static byte[] int24(int v) {
return new byte[]{(byte) (v >> 16), (byte) (v >> 8), (byte) v};
}
private static byte[] longTo8(long v) {
byte[] b = new byte[8];
for (int i = 7; i >= 0; i--) {
b[i] = (byte) (v & 0xff);
v >>= 8;
}
return b;
}
private static byte[] concat(byte[]... arrays) {
return TlsCrypto.concat(arrays);
}
}

View File

@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/scroll_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="App 内部利用无线调试进行环回连接(自建 ADB 客户端)"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tv_hint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="需要 Android 11+。请先到「开发者选项 → 无线调试」开启无线调试,并记录「使用配对码配对设备」的端口/配对码,以及「无线调试」主端口。"
android:textSize="12sp"
android:textColor="#888888"
app:layout_constraintTop_toBottomOf="@id/tv_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btn_open_settings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="打开开发者选项"
app:layout_constraintTop_toBottomOf="@id/tv_hint"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 配对 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_pair_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="配对端口 (Pairing Port)"
app:layout_constraintTop_toBottomOf="@id/btn_open_settings"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_pair_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="37019" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_pair_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="配对码 (Pairing Code)"
app:layout_constraintTop_toBottomOf="@id/til_pair_port"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_pair_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="123456" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_pair"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="① 配对 (TLS 握手提交配对码)"
app:layout_constraintTop_toBottomOf="@id/til_pair_code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 主端口连接 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_main_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="无线调试主端口 (IP Address &amp; Port 下方)"
app:layout_constraintTop_toBottomOf="@id/btn_pair"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_main_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="12345" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_connect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="② 连接无线调试主端口"
app:layout_constraintTop_toBottomOf="@id/til_main_port"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btn_disconnect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="断开连接"
app:layout_constraintTop_toBottomOf="@id/btn_connect"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Shell 指令 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_tap_x"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="X 坐标"
app:layout_constraintTop_toBottomOf="@id/btn_disconnect"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/til_tap_y">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_tap_x"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="500" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_tap_y"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="8dp"
android:hint="Y 坐标"
app:layout_constraintTop_toBottomOf="@id/btn_disconnect"
app:layout_constraintStart_toEndOf="@id/til_tap_x"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_tap_y"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="1000" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_tap"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="③ 执行 input tap (点击)"
app:layout_constraintTop_toBottomOf="@id/til_tap_y"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_cmd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="自定义 shell 命令 (如 getprop ro.build.version.sdk)"
app:layout_constraintTop_toBottomOf="@id/btn_tap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_cmd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionSend"
android:text="getprop ro.build.version.sdk" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_exec"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="执行自定义命令"
app:layout_constraintTop_toBottomOf="@id/til_cmd"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tv_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="状态:未连接"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/btn_exec"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tv_log"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#111111"
android:textColor="#00FF00"
android:textSize="11sp"
android:padding="8dp"
android:typeface="monospace"
android:text="日志输出将显示在这里…"
app:layout_constraintTop_toBottomOf="@id/tv_status"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ADB 环回控制端</string>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>