capacitor-dex-editor 0.0.25 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
package com.aetherlink.dexeditor;
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayInputStream;
|
|
4
|
+
import java.nio.ByteBuffer;
|
|
5
|
+
import java.nio.ByteOrder;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 简单的 AXML (Android Binary XML) 解析器
|
|
9
|
+
* 用于解码 APK 中的二进制 XML 文件
|
|
10
|
+
*/
|
|
11
|
+
public class AxmlParser {
|
|
12
|
+
|
|
13
|
+
// AXML 文件头魔数
|
|
14
|
+
private static final int AXML_MAGIC = 0x00080003;
|
|
15
|
+
|
|
16
|
+
// Chunk 类型
|
|
17
|
+
private static final int CHUNK_STRING_POOL = 0x001C0001;
|
|
18
|
+
private static final int CHUNK_RESOURCE_IDS = 0x00080180;
|
|
19
|
+
private static final int CHUNK_START_NAMESPACE = 0x00100100;
|
|
20
|
+
private static final int CHUNK_END_NAMESPACE = 0x00100101;
|
|
21
|
+
private static final int CHUNK_START_TAG = 0x00100102;
|
|
22
|
+
private static final int CHUNK_END_TAG = 0x00100103;
|
|
23
|
+
private static final int CHUNK_TEXT = 0x00100104;
|
|
24
|
+
|
|
25
|
+
private ByteBuffer buffer;
|
|
26
|
+
private String[] stringPool;
|
|
27
|
+
private StringBuilder xml;
|
|
28
|
+
private int indent = 0;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 解码 AXML 数据为可读 XML 字符串
|
|
32
|
+
*/
|
|
33
|
+
public static String decode(byte[] data) throws Exception {
|
|
34
|
+
AxmlParser parser = new AxmlParser();
|
|
35
|
+
return parser.parse(data);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private String parse(byte[] data) throws Exception {
|
|
39
|
+
buffer = ByteBuffer.wrap(data);
|
|
40
|
+
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
41
|
+
xml = new StringBuilder();
|
|
42
|
+
|
|
43
|
+
// 读取文件头
|
|
44
|
+
int magic = buffer.getInt();
|
|
45
|
+
if (magic != AXML_MAGIC) {
|
|
46
|
+
throw new Exception("Invalid AXML magic: " + Integer.toHexString(magic));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
int fileSize = buffer.getInt();
|
|
50
|
+
|
|
51
|
+
// 解析所有 chunks
|
|
52
|
+
while (buffer.position() < data.length) {
|
|
53
|
+
int chunkType = buffer.getInt();
|
|
54
|
+
int chunkSize = buffer.getInt();
|
|
55
|
+
int startPos = buffer.position() - 8;
|
|
56
|
+
|
|
57
|
+
switch (chunkType) {
|
|
58
|
+
case CHUNK_STRING_POOL:
|
|
59
|
+
parseStringPool(chunkSize);
|
|
60
|
+
break;
|
|
61
|
+
case CHUNK_RESOURCE_IDS:
|
|
62
|
+
// 跳过资源 ID
|
|
63
|
+
buffer.position(startPos + chunkSize);
|
|
64
|
+
break;
|
|
65
|
+
case CHUNK_START_NAMESPACE:
|
|
66
|
+
parseStartNamespace();
|
|
67
|
+
break;
|
|
68
|
+
case CHUNK_END_NAMESPACE:
|
|
69
|
+
parseEndNamespace();
|
|
70
|
+
break;
|
|
71
|
+
case CHUNK_START_TAG:
|
|
72
|
+
parseStartTag();
|
|
73
|
+
break;
|
|
74
|
+
case CHUNK_END_TAG:
|
|
75
|
+
parseEndTag();
|
|
76
|
+
break;
|
|
77
|
+
case CHUNK_TEXT:
|
|
78
|
+
parseText();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
// 跳过未知 chunk
|
|
82
|
+
buffer.position(startPos + chunkSize);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return xml.toString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private void parseStringPool(int chunkSize) {
|
|
91
|
+
int stringCount = buffer.getInt();
|
|
92
|
+
int styleCount = buffer.getInt();
|
|
93
|
+
int flags = buffer.getInt();
|
|
94
|
+
int stringsOffset = buffer.getInt();
|
|
95
|
+
int stylesOffset = buffer.getInt();
|
|
96
|
+
|
|
97
|
+
// 读取字符串偏移表
|
|
98
|
+
int[] stringOffsets = new int[stringCount];
|
|
99
|
+
for (int i = 0; i < stringCount; i++) {
|
|
100
|
+
stringOffsets[i] = buffer.getInt();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 跳过样式偏移表
|
|
104
|
+
for (int i = 0; i < styleCount; i++) {
|
|
105
|
+
buffer.getInt();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 读取字符串
|
|
109
|
+
int stringsStart = buffer.position();
|
|
110
|
+
stringPool = new String[stringCount];
|
|
111
|
+
|
|
112
|
+
boolean isUtf8 = (flags & 0x100) != 0;
|
|
113
|
+
|
|
114
|
+
for (int i = 0; i < stringCount; i++) {
|
|
115
|
+
buffer.position(stringsStart + stringOffsets[i]);
|
|
116
|
+
stringPool[i] = readString(isUtf8);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private String readString(boolean isUtf8) {
|
|
121
|
+
if (isUtf8) {
|
|
122
|
+
// UTF-8 编码
|
|
123
|
+
int charLen = buffer.get() & 0xFF;
|
|
124
|
+
if ((charLen & 0x80) != 0) {
|
|
125
|
+
charLen = ((charLen & 0x7F) << 8) | (buffer.get() & 0xFF);
|
|
126
|
+
}
|
|
127
|
+
int byteLen = buffer.get() & 0xFF;
|
|
128
|
+
if ((byteLen & 0x80) != 0) {
|
|
129
|
+
byteLen = ((byteLen & 0x7F) << 8) | (buffer.get() & 0xFF);
|
|
130
|
+
}
|
|
131
|
+
byte[] bytes = new byte[byteLen];
|
|
132
|
+
buffer.get(bytes);
|
|
133
|
+
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
|
134
|
+
} else {
|
|
135
|
+
// UTF-16 编码
|
|
136
|
+
int charLen = buffer.getShort() & 0xFFFF;
|
|
137
|
+
if ((charLen & 0x8000) != 0) {
|
|
138
|
+
charLen = ((charLen & 0x7FFF) << 16) | (buffer.getShort() & 0xFFFF);
|
|
139
|
+
}
|
|
140
|
+
char[] chars = new char[charLen];
|
|
141
|
+
for (int i = 0; i < charLen; i++) {
|
|
142
|
+
chars[i] = buffer.getChar();
|
|
143
|
+
}
|
|
144
|
+
return new String(chars);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private void parseStartNamespace() {
|
|
149
|
+
int lineNumber = buffer.getInt();
|
|
150
|
+
int comment = buffer.getInt();
|
|
151
|
+
int prefix = buffer.getInt();
|
|
152
|
+
int uri = buffer.getInt();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private void parseEndNamespace() {
|
|
156
|
+
int lineNumber = buffer.getInt();
|
|
157
|
+
int comment = buffer.getInt();
|
|
158
|
+
int prefix = buffer.getInt();
|
|
159
|
+
int uri = buffer.getInt();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private void parseStartTag() {
|
|
163
|
+
int lineNumber = buffer.getInt();
|
|
164
|
+
int comment = buffer.getInt();
|
|
165
|
+
int namespaceUri = buffer.getInt();
|
|
166
|
+
int name = buffer.getInt();
|
|
167
|
+
int flags = buffer.getShort() & 0xFFFF;
|
|
168
|
+
int attributeCount = buffer.getShort() & 0xFFFF;
|
|
169
|
+
int classAttribute = buffer.getShort() & 0xFFFF;
|
|
170
|
+
int idAttribute = buffer.getShort() & 0xFFFF;
|
|
171
|
+
int styleAttribute = buffer.getShort() & 0xFFFF;
|
|
172
|
+
|
|
173
|
+
// 添加缩进
|
|
174
|
+
for (int i = 0; i < indent; i++) {
|
|
175
|
+
xml.append(" ");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
xml.append("<");
|
|
179
|
+
xml.append(getString(name));
|
|
180
|
+
|
|
181
|
+
// 解析属性
|
|
182
|
+
for (int i = 0; i < attributeCount; i++) {
|
|
183
|
+
int attrNs = buffer.getInt();
|
|
184
|
+
int attrName = buffer.getInt();
|
|
185
|
+
int attrRawValue = buffer.getInt();
|
|
186
|
+
int attrType = buffer.getShort() & 0xFFFF;
|
|
187
|
+
buffer.getShort(); // size
|
|
188
|
+
int attrData = buffer.getInt();
|
|
189
|
+
|
|
190
|
+
xml.append("\n");
|
|
191
|
+
for (int j = 0; j <= indent; j++) {
|
|
192
|
+
xml.append(" ");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 命名空间前缀
|
|
196
|
+
if (attrNs >= 0 && attrNs < stringPool.length) {
|
|
197
|
+
String ns = stringPool[attrNs];
|
|
198
|
+
if (ns.contains("android")) {
|
|
199
|
+
xml.append("android:");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
xml.append(getString(attrName));
|
|
204
|
+
xml.append("=\"");
|
|
205
|
+
xml.append(getAttributeValue(attrRawValue, attrType, attrData));
|
|
206
|
+
xml.append("\"");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
xml.append(">\n");
|
|
210
|
+
indent++;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private void parseEndTag() {
|
|
214
|
+
int lineNumber = buffer.getInt();
|
|
215
|
+
int comment = buffer.getInt();
|
|
216
|
+
int namespaceUri = buffer.getInt();
|
|
217
|
+
int name = buffer.getInt();
|
|
218
|
+
|
|
219
|
+
indent--;
|
|
220
|
+
for (int i = 0; i < indent; i++) {
|
|
221
|
+
xml.append(" ");
|
|
222
|
+
}
|
|
223
|
+
xml.append("</");
|
|
224
|
+
xml.append(getString(name));
|
|
225
|
+
xml.append(">\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private void parseText() {
|
|
229
|
+
int lineNumber = buffer.getInt();
|
|
230
|
+
int comment = buffer.getInt();
|
|
231
|
+
int text = buffer.getInt();
|
|
232
|
+
buffer.getInt(); // unknown
|
|
233
|
+
buffer.getInt(); // unknown
|
|
234
|
+
|
|
235
|
+
for (int i = 0; i < indent; i++) {
|
|
236
|
+
xml.append(" ");
|
|
237
|
+
}
|
|
238
|
+
xml.append(getString(text));
|
|
239
|
+
xml.append("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private String getString(int index) {
|
|
243
|
+
if (index >= 0 && index < stringPool.length) {
|
|
244
|
+
return escapeXml(stringPool[index]);
|
|
245
|
+
}
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private String getAttributeValue(int rawValue, int type, int data) {
|
|
250
|
+
// 如果有原始字符串值
|
|
251
|
+
if (rawValue >= 0 && rawValue < stringPool.length) {
|
|
252
|
+
return escapeXml(stringPool[rawValue]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 根据类型解析数据
|
|
256
|
+
switch (type >> 8) {
|
|
257
|
+
case 0x01: // 引用
|
|
258
|
+
return "@" + Integer.toHexString(data);
|
|
259
|
+
case 0x02: // 属性引用
|
|
260
|
+
return "?" + Integer.toHexString(data);
|
|
261
|
+
case 0x03: // 字符串
|
|
262
|
+
if (data >= 0 && data < stringPool.length) {
|
|
263
|
+
return escapeXml(stringPool[data]);
|
|
264
|
+
}
|
|
265
|
+
return "";
|
|
266
|
+
case 0x10: // 整数
|
|
267
|
+
return String.valueOf(data);
|
|
268
|
+
case 0x11: // 十六进制
|
|
269
|
+
return "0x" + Integer.toHexString(data);
|
|
270
|
+
case 0x12: // 布尔
|
|
271
|
+
return data != 0 ? "true" : "false";
|
|
272
|
+
case 0x1C: // 颜色 ARGB8
|
|
273
|
+
return String.format("#%08X", data);
|
|
274
|
+
case 0x1D: // 颜色 RGB8
|
|
275
|
+
return String.format("#%06X", data & 0xFFFFFF);
|
|
276
|
+
default:
|
|
277
|
+
return "0x" + Integer.toHexString(data);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private String escapeXml(String s) {
|
|
282
|
+
if (s == null) return "";
|
|
283
|
+
return s.replace("&", "&")
|
|
284
|
+
.replace("<", "<")
|
|
285
|
+
.replace(">", ">")
|
|
286
|
+
.replace("\"", """)
|
|
287
|
+
.replace("'", "'");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -61,6 +61,9 @@ public class DexManager {
|
|
|
61
61
|
|
|
62
62
|
// 存储活跃的 DEX 会话
|
|
63
63
|
private final Map<String, DexSession> sessions = new HashMap<>();
|
|
64
|
+
|
|
65
|
+
// 存储多 DEX 会话(MCP 工作流)
|
|
66
|
+
private final Map<String, MultiDexSession> multiDexSessions = new HashMap<>();
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* DEX 会话 - 存储加载的 DEX 文件及其修改状态
|
|
@@ -82,6 +85,28 @@ public class DexManager {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 多 DEX 会话 - 用于 MCP 工作流,支持同时编辑多个 DEX 文件
|
|
90
|
+
*/
|
|
91
|
+
private static class MultiDexSession {
|
|
92
|
+
String sessionId;
|
|
93
|
+
String apkPath;
|
|
94
|
+
Map<String, DexBackedDexFile> dexFiles;
|
|
95
|
+
Map<String, ClassDef> modifiedClasses;
|
|
96
|
+
boolean modified = false;
|
|
97
|
+
|
|
98
|
+
MultiDexSession(String sessionId, String apkPath) {
|
|
99
|
+
this.sessionId = sessionId;
|
|
100
|
+
this.apkPath = apkPath;
|
|
101
|
+
this.dexFiles = new HashMap<>();
|
|
102
|
+
this.modifiedClasses = new HashMap<>();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
void addDex(String dexName, DexBackedDexFile dexFile) {
|
|
106
|
+
this.dexFiles.put(dexName, dexFile);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
// ==================== DEX 文件操作 ====================
|
|
86
111
|
|
|
87
112
|
/**
|
|
@@ -1500,6 +1525,531 @@ public class DexManager {
|
|
|
1500
1525
|
return result;
|
|
1501
1526
|
}
|
|
1502
1527
|
|
|
1528
|
+
// ==================== MCP 工作流支持方法 ====================
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* 列出 APK 中的所有 DEX 文件
|
|
1532
|
+
*/
|
|
1533
|
+
public JSObject listDexFilesInApk(String apkPath) throws Exception {
|
|
1534
|
+
JSObject result = new JSObject();
|
|
1535
|
+
JSArray dexFiles = new JSArray();
|
|
1536
|
+
|
|
1537
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1538
|
+
try {
|
|
1539
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1540
|
+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
|
|
1541
|
+
|
|
1542
|
+
while (entries.hasMoreElements()) {
|
|
1543
|
+
java.util.zip.ZipEntry entry = entries.nextElement();
|
|
1544
|
+
String name = entry.getName();
|
|
1545
|
+
if (name.endsWith(".dex") && !name.contains("/")) {
|
|
1546
|
+
JSObject dexInfo = new JSObject();
|
|
1547
|
+
dexInfo.put("name", name);
|
|
1548
|
+
dexInfo.put("size", entry.getSize());
|
|
1549
|
+
dexFiles.put(dexInfo);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
result.put("apkPath", apkPath);
|
|
1554
|
+
result.put("dexFiles", dexFiles);
|
|
1555
|
+
result.put("count", dexFiles.length());
|
|
1556
|
+
|
|
1557
|
+
} finally {
|
|
1558
|
+
if (zipFile != null) {
|
|
1559
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return result;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* 打开多个 DEX 文件创建会话(MCP 工作流)
|
|
1568
|
+
*/
|
|
1569
|
+
public JSObject openMultipleDex(String apkPath, JSONArray dexFiles) throws Exception {
|
|
1570
|
+
JSObject result = new JSObject();
|
|
1571
|
+
String sessionId = UUID.randomUUID().toString();
|
|
1572
|
+
|
|
1573
|
+
// 创建复合会话
|
|
1574
|
+
MultiDexSession multiSession = new MultiDexSession(sessionId, apkPath);
|
|
1575
|
+
|
|
1576
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1577
|
+
int totalClasses = 0;
|
|
1578
|
+
|
|
1579
|
+
try {
|
|
1580
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1581
|
+
|
|
1582
|
+
for (int i = 0; i < dexFiles.length(); i++) {
|
|
1583
|
+
String dexName = dexFiles.getString(i);
|
|
1584
|
+
java.util.zip.ZipEntry dexEntry = zipFile.getEntry(dexName);
|
|
1585
|
+
|
|
1586
|
+
if (dexEntry == null) {
|
|
1587
|
+
Log.w(TAG, "DEX not found: " + dexName);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// 读取 DEX 到内存
|
|
1592
|
+
java.io.InputStream is = zipFile.getInputStream(dexEntry);
|
|
1593
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
1594
|
+
byte[] buffer = new byte[8192];
|
|
1595
|
+
int len;
|
|
1596
|
+
while ((len = is.read(buffer)) != -1) {
|
|
1597
|
+
baos.write(buffer, 0, len);
|
|
1598
|
+
}
|
|
1599
|
+
is.close();
|
|
1600
|
+
|
|
1601
|
+
// 解析 DEX
|
|
1602
|
+
DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), baos.toByteArray());
|
|
1603
|
+
multiSession.addDex(dexName, dexFile);
|
|
1604
|
+
totalClasses += dexFile.getClasses().size();
|
|
1605
|
+
|
|
1606
|
+
Log.d(TAG, "Loaded DEX: " + dexName + " with " + dexFile.getClasses().size() + " classes");
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
} finally {
|
|
1610
|
+
if (zipFile != null) {
|
|
1611
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
multiDexSessions.put(sessionId, multiSession);
|
|
1616
|
+
|
|
1617
|
+
result.put("sessionId", sessionId);
|
|
1618
|
+
result.put("apkPath", apkPath);
|
|
1619
|
+
result.put("dexCount", multiSession.dexFiles.size());
|
|
1620
|
+
result.put("classCount", totalClasses);
|
|
1621
|
+
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* 列出所有打开的会话
|
|
1627
|
+
*/
|
|
1628
|
+
public JSArray listAllSessions() {
|
|
1629
|
+
JSArray result = new JSArray();
|
|
1630
|
+
|
|
1631
|
+
// 单 DEX 会话
|
|
1632
|
+
for (Map.Entry<String, DexSession> entry : sessions.entrySet()) {
|
|
1633
|
+
JSObject session = new JSObject();
|
|
1634
|
+
session.put("sessionId", entry.getKey());
|
|
1635
|
+
session.put("type", "single");
|
|
1636
|
+
session.put("filePath", entry.getValue().filePath);
|
|
1637
|
+
session.put("modified", entry.getValue().modified);
|
|
1638
|
+
result.put(session);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// 多 DEX 会话
|
|
1642
|
+
for (Map.Entry<String, MultiDexSession> entry : multiDexSessions.entrySet()) {
|
|
1643
|
+
JSObject session = new JSObject();
|
|
1644
|
+
session.put("sessionId", entry.getKey());
|
|
1645
|
+
session.put("type", "multi");
|
|
1646
|
+
session.put("apkPath", entry.getValue().apkPath);
|
|
1647
|
+
session.put("dexCount", entry.getValue().dexFiles.size());
|
|
1648
|
+
session.put("modified", entry.getValue().modified);
|
|
1649
|
+
result.put(session);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* 关闭多 DEX 会话
|
|
1657
|
+
*/
|
|
1658
|
+
public void closeMultiDexSession(String sessionId) {
|
|
1659
|
+
multiDexSessions.remove(sessionId);
|
|
1660
|
+
Log.d(TAG, "Closed multi-dex session: " + sessionId);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* 获取多 DEX 会话中的类列表(支持分页和过滤)
|
|
1665
|
+
*/
|
|
1666
|
+
public JSObject getClassesFromMultiSession(String sessionId, String packageFilter, int offset, int limit) throws Exception {
|
|
1667
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1668
|
+
if (session == null) {
|
|
1669
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
JSObject result = new JSObject();
|
|
1673
|
+
JSArray classes = new JSArray();
|
|
1674
|
+
List<String> allClasses = new ArrayList<>();
|
|
1675
|
+
|
|
1676
|
+
// 收集所有类
|
|
1677
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1678
|
+
String dexName = entry.getKey();
|
|
1679
|
+
DexBackedDexFile dexFile = entry.getValue();
|
|
1680
|
+
|
|
1681
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1682
|
+
String className = convertTypeToClassName(classDef.getType());
|
|
1683
|
+
|
|
1684
|
+
// 包名过滤
|
|
1685
|
+
if (packageFilter != null && !packageFilter.isEmpty()) {
|
|
1686
|
+
if (!className.startsWith(packageFilter)) {
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
allClasses.add(className + "|" + dexName);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 排序
|
|
1696
|
+
java.util.Collections.sort(allClasses);
|
|
1697
|
+
|
|
1698
|
+
// 分页
|
|
1699
|
+
int total = allClasses.size();
|
|
1700
|
+
int end = Math.min(offset + limit, total);
|
|
1701
|
+
|
|
1702
|
+
for (int i = offset; i < end; i++) {
|
|
1703
|
+
String[] parts = allClasses.get(i).split("\\|");
|
|
1704
|
+
JSObject classInfo = new JSObject();
|
|
1705
|
+
classInfo.put("className", parts[0]);
|
|
1706
|
+
classInfo.put("dexFile", parts[1]);
|
|
1707
|
+
classes.put(classInfo);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
result.put("total", total);
|
|
1711
|
+
result.put("offset", offset);
|
|
1712
|
+
result.put("limit", limit);
|
|
1713
|
+
result.put("classes", classes);
|
|
1714
|
+
result.put("hasMore", end < total);
|
|
1715
|
+
|
|
1716
|
+
return result;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* 在多 DEX 会话中搜索
|
|
1721
|
+
*/
|
|
1722
|
+
public JSObject searchInMultiSession(String sessionId, String query, String searchType,
|
|
1723
|
+
boolean caseSensitive, int maxResults) throws Exception {
|
|
1724
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1725
|
+
if (session == null) {
|
|
1726
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
JSObject result = new JSObject();
|
|
1730
|
+
JSArray results = new JSArray();
|
|
1731
|
+
String queryMatch = caseSensitive ? query : query.toLowerCase();
|
|
1732
|
+
|
|
1733
|
+
outerLoop:
|
|
1734
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1735
|
+
String dexName = entry.getKey();
|
|
1736
|
+
DexBackedDexFile dexFile = entry.getValue();
|
|
1737
|
+
|
|
1738
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1739
|
+
if (results.length() >= maxResults) break outerLoop;
|
|
1740
|
+
|
|
1741
|
+
String className = convertTypeToClassName(classDef.getType());
|
|
1742
|
+
String classNameMatch = caseSensitive ? className : className.toLowerCase();
|
|
1743
|
+
|
|
1744
|
+
switch (searchType) {
|
|
1745
|
+
case "class":
|
|
1746
|
+
if (classNameMatch.contains(queryMatch)) {
|
|
1747
|
+
JSObject item = new JSObject();
|
|
1748
|
+
item.put("type", "class");
|
|
1749
|
+
item.put("className", className);
|
|
1750
|
+
item.put("dexFile", dexName);
|
|
1751
|
+
results.put(item);
|
|
1752
|
+
}
|
|
1753
|
+
break;
|
|
1754
|
+
|
|
1755
|
+
case "package":
|
|
1756
|
+
if (classNameMatch.startsWith(queryMatch)) {
|
|
1757
|
+
JSObject item = new JSObject();
|
|
1758
|
+
item.put("type", "package");
|
|
1759
|
+
item.put("className", className);
|
|
1760
|
+
item.put("dexFile", dexName);
|
|
1761
|
+
results.put(item);
|
|
1762
|
+
}
|
|
1763
|
+
break;
|
|
1764
|
+
|
|
1765
|
+
case "method":
|
|
1766
|
+
for (Method method : classDef.getMethods()) {
|
|
1767
|
+
if (results.length() >= maxResults) break outerLoop;
|
|
1768
|
+
String methodName = method.getName();
|
|
1769
|
+
String methodMatch = caseSensitive ? methodName : methodName.toLowerCase();
|
|
1770
|
+
if (methodMatch.contains(queryMatch)) {
|
|
1771
|
+
JSObject item = new JSObject();
|
|
1772
|
+
item.put("type", "method");
|
|
1773
|
+
item.put("className", className);
|
|
1774
|
+
item.put("methodName", methodName);
|
|
1775
|
+
item.put("dexFile", dexName);
|
|
1776
|
+
results.put(item);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
break;
|
|
1780
|
+
|
|
1781
|
+
case "field":
|
|
1782
|
+
for (Field field : classDef.getFields()) {
|
|
1783
|
+
if (results.length() >= maxResults) break outerLoop;
|
|
1784
|
+
String fieldName = field.getName();
|
|
1785
|
+
String fieldMatch = caseSensitive ? fieldName : fieldName.toLowerCase();
|
|
1786
|
+
if (fieldMatch.contains(queryMatch)) {
|
|
1787
|
+
JSObject item = new JSObject();
|
|
1788
|
+
item.put("type", "field");
|
|
1789
|
+
item.put("className", className);
|
|
1790
|
+
item.put("fieldName", fieldName);
|
|
1791
|
+
item.put("dexFile", dexName);
|
|
1792
|
+
results.put(item);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
break;
|
|
1796
|
+
|
|
1797
|
+
case "string":
|
|
1798
|
+
case "code":
|
|
1799
|
+
// 需要反编译 smali 搜索
|
|
1800
|
+
String smali = getSmaliForClass(dexFile, classDef);
|
|
1801
|
+
String smaliMatch = caseSensitive ? smali : smali.toLowerCase();
|
|
1802
|
+
if (smaliMatch.contains(queryMatch)) {
|
|
1803
|
+
JSObject item = new JSObject();
|
|
1804
|
+
item.put("type", searchType);
|
|
1805
|
+
item.put("className", className);
|
|
1806
|
+
item.put("dexFile", dexName);
|
|
1807
|
+
// 找到匹配的行
|
|
1808
|
+
String[] lines = smali.split("\n");
|
|
1809
|
+
for (int i = 0; i < lines.length; i++) {
|
|
1810
|
+
String lineMatch = caseSensitive ? lines[i] : lines[i].toLowerCase();
|
|
1811
|
+
if (lineMatch.contains(queryMatch)) {
|
|
1812
|
+
item.put("line", i + 1);
|
|
1813
|
+
item.put("content", lines[i].trim());
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
results.put(item);
|
|
1818
|
+
}
|
|
1819
|
+
break;
|
|
1820
|
+
|
|
1821
|
+
case "int":
|
|
1822
|
+
// 搜索整数常量
|
|
1823
|
+
String smaliForInt = getSmaliForClass(dexFile, classDef);
|
|
1824
|
+
if (smaliForInt.contains("0x" + query) || smaliForInt.contains(" " + query + "\n") ||
|
|
1825
|
+
smaliForInt.contains(" " + query + " ")) {
|
|
1826
|
+
JSObject item = new JSObject();
|
|
1827
|
+
item.put("type", "int");
|
|
1828
|
+
item.put("className", className);
|
|
1829
|
+
item.put("dexFile", dexName);
|
|
1830
|
+
results.put(item);
|
|
1831
|
+
}
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
result.put("query", query);
|
|
1838
|
+
result.put("searchType", searchType);
|
|
1839
|
+
result.put("total", results.length());
|
|
1840
|
+
result.put("results", results);
|
|
1841
|
+
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* 获取类的 Smali 代码(内部方法)
|
|
1847
|
+
*/
|
|
1848
|
+
private String getSmaliForClass(DexBackedDexFile dexFile, ClassDef classDef) {
|
|
1849
|
+
try {
|
|
1850
|
+
BaksmaliOptions options = new BaksmaliOptions();
|
|
1851
|
+
ClassDefinition classDefinition = new ClassDefinition(options, classDef);
|
|
1852
|
+
java.io.StringWriter stringWriter = new java.io.StringWriter();
|
|
1853
|
+
BaksmaliWriter writer = new BaksmaliWriter(stringWriter, null);
|
|
1854
|
+
classDefinition.writeTo(writer);
|
|
1855
|
+
writer.close();
|
|
1856
|
+
return stringWriter.toString();
|
|
1857
|
+
} catch (Exception e) {
|
|
1858
|
+
return "";
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* 从多 DEX 会话获取类的 Smali 代码
|
|
1864
|
+
*/
|
|
1865
|
+
public JSObject getClassSmaliFromSession(String sessionId, String className) throws Exception {
|
|
1866
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1867
|
+
if (session == null) {
|
|
1868
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
String targetType = convertClassNameToType(className);
|
|
1872
|
+
|
|
1873
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1874
|
+
String dexName = entry.getKey();
|
|
1875
|
+
DexBackedDexFile dexFile = entry.getValue();
|
|
1876
|
+
|
|
1877
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1878
|
+
if (classDef.getType().equals(targetType)) {
|
|
1879
|
+
JSObject result = new JSObject();
|
|
1880
|
+
result.put("className", className);
|
|
1881
|
+
result.put("dexFile", dexName);
|
|
1882
|
+
result.put("smaliContent", getSmaliForClass(dexFile, classDef));
|
|
1883
|
+
return result;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
throw new IllegalArgumentException("Class not found: " + className);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* 修改类并保存到多 DEX 会话
|
|
1893
|
+
*/
|
|
1894
|
+
public void modifyClassInSession(String sessionId, String className, String smaliContent) throws Exception {
|
|
1895
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1896
|
+
if (session == null) {
|
|
1897
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
String targetType = convertClassNameToType(className);
|
|
1901
|
+
|
|
1902
|
+
// 找到类所在的 DEX
|
|
1903
|
+
String targetDex = null;
|
|
1904
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1905
|
+
for (ClassDef classDef : entry.getValue().getClasses()) {
|
|
1906
|
+
if (classDef.getType().equals(targetType)) {
|
|
1907
|
+
targetDex = entry.getKey();
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
if (targetDex != null) break;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (targetDex == null) {
|
|
1915
|
+
throw new IllegalArgumentException("Class not found: " + className);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// 编译新的 Smali
|
|
1919
|
+
ClassDef newClassDef = compileSmaliToClass(smaliContent, Opcodes.getDefault());
|
|
1920
|
+
|
|
1921
|
+
// 记录修改
|
|
1922
|
+
session.modifiedClasses.put(targetDex + "|" + className, newClassDef);
|
|
1923
|
+
session.modified = true;
|
|
1924
|
+
|
|
1925
|
+
Log.d(TAG, "Modified class in session: " + className);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* 保存多 DEX 会话的修改到 APK
|
|
1930
|
+
*/
|
|
1931
|
+
public JSObject saveMultiDexSessionToApk(String sessionId) throws Exception {
|
|
1932
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1933
|
+
if (session == null) {
|
|
1934
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (!session.modified || session.modifiedClasses.isEmpty()) {
|
|
1938
|
+
JSObject result = new JSObject();
|
|
1939
|
+
result.put("success", true);
|
|
1940
|
+
result.put("message", "没有需要保存的修改");
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// 按 DEX 文件分组修改
|
|
1945
|
+
Map<String, List<ClassDef>> modifiedByDex = new HashMap<>();
|
|
1946
|
+
for (Map.Entry<String, ClassDef> entry : session.modifiedClasses.entrySet()) {
|
|
1947
|
+
String[] parts = entry.getKey().split("\\|");
|
|
1948
|
+
String dexName = parts[0];
|
|
1949
|
+
modifiedByDex.computeIfAbsent(dexName, k -> new ArrayList<>()).add(entry.getValue());
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// 为每个修改的 DEX 创建新版本
|
|
1953
|
+
Map<String, byte[]> newDexData = new HashMap<>();
|
|
1954
|
+
|
|
1955
|
+
for (Map.Entry<String, List<ClassDef>> entry : modifiedByDex.entrySet()) {
|
|
1956
|
+
String dexName = entry.getKey();
|
|
1957
|
+
List<ClassDef> modifiedClasses = entry.getValue();
|
|
1958
|
+
DexBackedDexFile originalDex = session.dexFiles.get(dexName);
|
|
1959
|
+
|
|
1960
|
+
// 合并类
|
|
1961
|
+
Set<String> modifiedTypes = new HashSet<>();
|
|
1962
|
+
for (ClassDef c : modifiedClasses) {
|
|
1963
|
+
modifiedTypes.add(c.getType());
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
List<ClassDef> allClasses = new ArrayList<>(modifiedClasses);
|
|
1967
|
+
for (ClassDef c : originalDex.getClasses()) {
|
|
1968
|
+
if (!modifiedTypes.contains(c.getType())) {
|
|
1969
|
+
allClasses.add(c);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// 创建新 DEX
|
|
1974
|
+
java.io.File tempDex = java.io.File.createTempFile("dex_", ".dex");
|
|
1975
|
+
DexPool dexPool = new DexPool(Opcodes.getDefault());
|
|
1976
|
+
for (ClassDef c : allClasses) {
|
|
1977
|
+
dexPool.internClass(c);
|
|
1978
|
+
}
|
|
1979
|
+
dexPool.writeTo(new FileDataStore(tempDex));
|
|
1980
|
+
|
|
1981
|
+
newDexData.put(dexName, readFileBytes(tempDex));
|
|
1982
|
+
tempDex.delete();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// 替换 APK 中的 DEX
|
|
1986
|
+
java.io.File apkFile = new java.io.File(session.apkPath);
|
|
1987
|
+
java.io.File tempApk = new java.io.File(session.apkPath + ".tmp");
|
|
1988
|
+
|
|
1989
|
+
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
|
|
1990
|
+
new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
|
|
1991
|
+
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
|
|
1992
|
+
new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
|
|
1993
|
+
|
|
1994
|
+
java.util.zip.ZipEntry entry;
|
|
1995
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
1996
|
+
if (newDexData.containsKey(entry.getName())) {
|
|
1997
|
+
// 替换 DEX
|
|
1998
|
+
byte[] dexBytes = newDexData.get(entry.getName());
|
|
1999
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2000
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2001
|
+
zos.putNextEntry(newEntry);
|
|
2002
|
+
zos.write(dexBytes);
|
|
2003
|
+
zos.closeEntry();
|
|
2004
|
+
} else {
|
|
2005
|
+
// 复制原条目
|
|
2006
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2007
|
+
newEntry.setTime(entry.getTime());
|
|
2008
|
+
if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
|
|
2009
|
+
newEntry.setMethod(java.util.zip.ZipEntry.STORED);
|
|
2010
|
+
newEntry.setSize(entry.getSize());
|
|
2011
|
+
newEntry.setCrc(entry.getCrc());
|
|
2012
|
+
} else {
|
|
2013
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2014
|
+
}
|
|
2015
|
+
zos.putNextEntry(newEntry);
|
|
2016
|
+
if (!entry.isDirectory()) {
|
|
2017
|
+
byte[] buf = new byte[8192];
|
|
2018
|
+
int n;
|
|
2019
|
+
while ((n = zis.read(buf)) != -1) {
|
|
2020
|
+
zos.write(buf, 0, n);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
zos.closeEntry();
|
|
2024
|
+
}
|
|
2025
|
+
zis.closeEntry();
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
zis.close();
|
|
2029
|
+
zos.close();
|
|
2030
|
+
|
|
2031
|
+
// 替换原文件
|
|
2032
|
+
if (!apkFile.delete()) {
|
|
2033
|
+
Log.e(TAG, "Failed to delete original APK");
|
|
2034
|
+
}
|
|
2035
|
+
if (!tempApk.renameTo(apkFile)) {
|
|
2036
|
+
copyFile(tempApk, apkFile);
|
|
2037
|
+
tempApk.delete();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// 清除修改状态
|
|
2041
|
+
session.modifiedClasses.clear();
|
|
2042
|
+
session.modified = false;
|
|
2043
|
+
|
|
2044
|
+
JSObject result = new JSObject();
|
|
2045
|
+
result.put("success", true);
|
|
2046
|
+
result.put("message", "DEX 已保存到 APK");
|
|
2047
|
+
result.put("apkPath", session.apkPath);
|
|
2048
|
+
result.put("needSign", true);
|
|
2049
|
+
|
|
2050
|
+
return result;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1503
2053
|
/**
|
|
1504
2054
|
* 将 DEX 类型格式转换为 Java 类名格式
|
|
1505
2055
|
* 例如: Lcom/example/Class; -> com.example.Class
|
|
@@ -1854,6 +2404,237 @@ public class DexManager {
|
|
|
1854
2404
|
fos.close();
|
|
1855
2405
|
}
|
|
1856
2406
|
|
|
2407
|
+
// ==================== XML/资源操作方法 ====================
|
|
2408
|
+
|
|
2409
|
+
/**
|
|
2410
|
+
* 获取 APK 的 AndroidManifest.xml(解码为可读 XML)
|
|
2411
|
+
*/
|
|
2412
|
+
public JSObject getManifestFromApk(String apkPath) throws Exception {
|
|
2413
|
+
JSObject result = new JSObject();
|
|
2414
|
+
|
|
2415
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2416
|
+
try {
|
|
2417
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2418
|
+
java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
|
|
2419
|
+
|
|
2420
|
+
if (manifestEntry == null) {
|
|
2421
|
+
throw new Exception("AndroidManifest.xml not found in APK");
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
java.io.InputStream is = zipFile.getInputStream(manifestEntry);
|
|
2425
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
2426
|
+
byte[] buffer = new byte[8192];
|
|
2427
|
+
int len;
|
|
2428
|
+
while ((len = is.read(buffer)) != -1) {
|
|
2429
|
+
baos.write(buffer, 0, len);
|
|
2430
|
+
}
|
|
2431
|
+
is.close();
|
|
2432
|
+
|
|
2433
|
+
// 解析二进制 AXML
|
|
2434
|
+
byte[] axmlData = baos.toByteArray();
|
|
2435
|
+
String xmlContent = decodeAxml(axmlData);
|
|
2436
|
+
|
|
2437
|
+
result.put("manifest", xmlContent);
|
|
2438
|
+
|
|
2439
|
+
} finally {
|
|
2440
|
+
if (zipFile != null) {
|
|
2441
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
return result;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* 解码二进制 AXML 为可读 XML(使用内置解析器)
|
|
2450
|
+
*/
|
|
2451
|
+
private String decodeAxml(byte[] axmlData) {
|
|
2452
|
+
try {
|
|
2453
|
+
return AxmlParser.decode(axmlData);
|
|
2454
|
+
} catch (Exception e) {
|
|
2455
|
+
Log.e(TAG, "AXML decode error: " + e.getMessage());
|
|
2456
|
+
return "# 无法解码 AXML: " + e.getMessage();
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/**
|
|
2461
|
+
* 修改 AndroidManifest.xml
|
|
2462
|
+
*/
|
|
2463
|
+
public JSObject modifyManifestInApk(String apkPath, String newManifestXml) throws Exception {
|
|
2464
|
+
JSObject result = new JSObject();
|
|
2465
|
+
|
|
2466
|
+
try {
|
|
2467
|
+
// 将 XML 编码为二进制 AXML
|
|
2468
|
+
byte[] newAxmlData = encodeAxml(newManifestXml);
|
|
2469
|
+
|
|
2470
|
+
// 替换 APK 中的 AndroidManifest.xml
|
|
2471
|
+
java.io.File apkFile = new java.io.File(apkPath);
|
|
2472
|
+
java.io.File tempApk = new java.io.File(apkPath + ".tmp");
|
|
2473
|
+
|
|
2474
|
+
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
|
|
2475
|
+
new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
|
|
2476
|
+
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
|
|
2477
|
+
new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
|
|
2478
|
+
|
|
2479
|
+
java.util.zip.ZipEntry entry;
|
|
2480
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
2481
|
+
if (entry.getName().equals("AndroidManifest.xml")) {
|
|
2482
|
+
// 替换 Manifest
|
|
2483
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry("AndroidManifest.xml");
|
|
2484
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2485
|
+
zos.putNextEntry(newEntry);
|
|
2486
|
+
zos.write(newAxmlData);
|
|
2487
|
+
zos.closeEntry();
|
|
2488
|
+
} else {
|
|
2489
|
+
// 复制其他文件
|
|
2490
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2491
|
+
newEntry.setTime(entry.getTime());
|
|
2492
|
+
if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
|
|
2493
|
+
newEntry.setMethod(java.util.zip.ZipEntry.STORED);
|
|
2494
|
+
newEntry.setSize(entry.getSize());
|
|
2495
|
+
newEntry.setCrc(entry.getCrc());
|
|
2496
|
+
} else {
|
|
2497
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2498
|
+
}
|
|
2499
|
+
zos.putNextEntry(newEntry);
|
|
2500
|
+
if (!entry.isDirectory()) {
|
|
2501
|
+
byte[] buf = new byte[8192];
|
|
2502
|
+
int n;
|
|
2503
|
+
while ((n = zis.read(buf)) != -1) {
|
|
2504
|
+
zos.write(buf, 0, n);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
zos.closeEntry();
|
|
2508
|
+
}
|
|
2509
|
+
zis.closeEntry();
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
zis.close();
|
|
2513
|
+
zos.close();
|
|
2514
|
+
|
|
2515
|
+
// 替换原文件
|
|
2516
|
+
if (!apkFile.delete()) {
|
|
2517
|
+
Log.e(TAG, "Failed to delete original APK");
|
|
2518
|
+
}
|
|
2519
|
+
if (!tempApk.renameTo(apkFile)) {
|
|
2520
|
+
copyFile(tempApk, apkFile);
|
|
2521
|
+
tempApk.delete();
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
result.put("success", true);
|
|
2525
|
+
result.put("message", "AndroidManifest.xml 已修改");
|
|
2526
|
+
|
|
2527
|
+
} catch (Exception e) {
|
|
2528
|
+
Log.e(TAG, "Modify manifest error: " + e.getMessage(), e);
|
|
2529
|
+
result.put("success", false);
|
|
2530
|
+
result.put("error", e.getMessage());
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
return result;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/**
|
|
2537
|
+
* 将 XML 编码为二进制 AXML
|
|
2538
|
+
* 注意:AXML 编码比较复杂,暂时不支持修改功能
|
|
2539
|
+
*/
|
|
2540
|
+
private byte[] encodeAxml(String xmlContent) throws Exception {
|
|
2541
|
+
throw new UnsupportedOperationException("AXML 编码功能暂不支持,请使用 APKTool 进行 Manifest 修改");
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* 列出 APK 中的资源文件
|
|
2546
|
+
*/
|
|
2547
|
+
public JSObject listResourcesInApk(String apkPath, String filter) throws Exception {
|
|
2548
|
+
JSObject result = new JSObject();
|
|
2549
|
+
JSArray resources = new JSArray();
|
|
2550
|
+
|
|
2551
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2552
|
+
try {
|
|
2553
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2554
|
+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
|
|
2555
|
+
|
|
2556
|
+
while (entries.hasMoreElements()) {
|
|
2557
|
+
java.util.zip.ZipEntry entry = entries.nextElement();
|
|
2558
|
+
String name = entry.getName();
|
|
2559
|
+
|
|
2560
|
+
// 只列出 res 目录下的文件
|
|
2561
|
+
if (name.startsWith("res/")) {
|
|
2562
|
+
// 过滤
|
|
2563
|
+
if (filter != null && !filter.isEmpty()) {
|
|
2564
|
+
if (!name.contains(filter)) {
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
JSObject resource = new JSObject();
|
|
2570
|
+
resource.put("path", name);
|
|
2571
|
+
resource.put("size", entry.getSize());
|
|
2572
|
+
resource.put("isXml", name.endsWith(".xml"));
|
|
2573
|
+
resources.put(resource);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
result.put("total", resources.length());
|
|
2578
|
+
result.put("resources", resources);
|
|
2579
|
+
|
|
2580
|
+
} finally {
|
|
2581
|
+
if (zipFile != null) {
|
|
2582
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
return result;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/**
|
|
2590
|
+
* 获取 APK 中的资源文件内容
|
|
2591
|
+
*/
|
|
2592
|
+
public JSObject getResourceFromApk(String apkPath, String resourcePath) throws Exception {
|
|
2593
|
+
JSObject result = new JSObject();
|
|
2594
|
+
|
|
2595
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2596
|
+
try {
|
|
2597
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2598
|
+
java.util.zip.ZipEntry entry = zipFile.getEntry(resourcePath);
|
|
2599
|
+
|
|
2600
|
+
if (entry == null) {
|
|
2601
|
+
throw new Exception("Resource not found: " + resourcePath);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
java.io.InputStream is = zipFile.getInputStream(entry);
|
|
2605
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
2606
|
+
byte[] buffer = new byte[8192];
|
|
2607
|
+
int len;
|
|
2608
|
+
while ((len = is.read(buffer)) != -1) {
|
|
2609
|
+
baos.write(buffer, 0, len);
|
|
2610
|
+
}
|
|
2611
|
+
is.close();
|
|
2612
|
+
|
|
2613
|
+
byte[] data = baos.toByteArray();
|
|
2614
|
+
|
|
2615
|
+
// 如果是 XML 文件,尝试解码 AXML
|
|
2616
|
+
if (resourcePath.endsWith(".xml")) {
|
|
2617
|
+
String xmlContent = decodeAxml(data);
|
|
2618
|
+
result.put("content", xmlContent);
|
|
2619
|
+
result.put("type", "xml");
|
|
2620
|
+
} else {
|
|
2621
|
+
// 其他文件返回 base64
|
|
2622
|
+
result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
|
|
2623
|
+
result.put("type", "binary");
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
result.put("path", resourcePath);
|
|
2627
|
+
result.put("size", data.length);
|
|
2628
|
+
|
|
2629
|
+
} finally {
|
|
2630
|
+
if (zipFile != null) {
|
|
2631
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
return result;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
1857
2638
|
/**
|
|
1858
2639
|
* 清理临时目录
|
|
1859
2640
|
*/
|