capacitor-dex-editor 0.0.24 → 0.0.27
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.
package/android/build.gradle
CHANGED
|
@@ -64,6 +64,12 @@ dependencies {
|
|
|
64
64
|
// Gson - JSON序列化
|
|
65
65
|
api 'com.google.code.gson:gson:2.10.1'
|
|
66
66
|
|
|
67
|
+
// APK Signer - V1/V2/V3/V4 签名支持 (Android 7.0+)
|
|
68
|
+
api 'com.android.tools.build:apksig:8.7.2'
|
|
69
|
+
|
|
70
|
+
// AXML 解析 - AndroidManifest.xml 二进制解析
|
|
71
|
+
api 'com.phlox.axml:axml:1.0.2'
|
|
72
|
+
|
|
67
73
|
testImplementation "junit:junit:$junitVersion"
|
|
68
74
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
69
75
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
@@ -308,7 +308,8 @@ public class ApkManager {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
|
-
*
|
|
311
|
+
* 使用内置测试密钥签名 APK (V1 + V2 + V3)
|
|
312
|
+
* 适配 Android 7.0 - Android 16
|
|
312
313
|
*/
|
|
313
314
|
public JSObject signApkWithTestKey(String apkPath, String outputPath) throws Exception {
|
|
314
315
|
File apkFile = new File(apkPath);
|
|
@@ -316,20 +317,183 @@ public class ApkManager {
|
|
|
316
317
|
throw new IOException("APK file not found: " + apkPath);
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
File outputFile = new File(outputPath);
|
|
321
|
+
|
|
322
|
+
// 生成 RSA 密钥对
|
|
323
|
+
java.security.KeyPairGenerator keyGen = java.security.KeyPairGenerator.getInstance("RSA");
|
|
324
|
+
keyGen.initialize(2048, new java.security.SecureRandom());
|
|
325
|
+
java.security.KeyPair keyPair = keyGen.generateKeyPair();
|
|
326
|
+
|
|
327
|
+
// 使用 Android 隐藏 API 生成自签名证书
|
|
328
|
+
X509Certificate cert = createSelfSignedCertificate(keyPair);
|
|
329
|
+
|
|
330
|
+
// 使用 apksig 进行 V1+V2+V3 签名
|
|
331
|
+
com.android.apksig.ApkSigner.SignerConfig signerConfig =
|
|
332
|
+
new com.android.apksig.ApkSigner.SignerConfig.Builder(
|
|
333
|
+
"CERT",
|
|
334
|
+
keyPair.getPrivate(),
|
|
335
|
+
java.util.Collections.singletonList(cert)
|
|
336
|
+
).build();
|
|
321
337
|
|
|
322
|
-
|
|
323
|
-
|
|
338
|
+
com.android.apksig.ApkSigner.Builder signerBuilder =
|
|
339
|
+
new com.android.apksig.ApkSigner.Builder(java.util.Collections.singletonList(signerConfig))
|
|
340
|
+
.setInputApk(apkFile)
|
|
341
|
+
.setOutputApk(outputFile)
|
|
342
|
+
.setV1SigningEnabled(true) // JAR 签名 (Android < 7.0)
|
|
343
|
+
.setV2SigningEnabled(true) // APK Signature Scheme v2 (Android 7.0+)
|
|
344
|
+
.setV3SigningEnabled(true); // APK Signature Scheme v3 (Android 9.0+)
|
|
324
345
|
|
|
325
|
-
|
|
346
|
+
com.android.apksig.ApkSigner signer = signerBuilder.build();
|
|
347
|
+
signer.sign();
|
|
348
|
+
|
|
349
|
+
Log.d(TAG, "Signed APK with V1+V2+V3: " + outputPath);
|
|
326
350
|
|
|
327
351
|
JSObject result = new JSObject();
|
|
328
352
|
result.put("outputPath", outputPath);
|
|
329
|
-
result.put("size",
|
|
330
|
-
result.put("
|
|
353
|
+
result.put("size", outputFile.length());
|
|
354
|
+
result.put("signatureSchemes", "V1+V2+V3");
|
|
355
|
+
result.put("success", true);
|
|
331
356
|
return result;
|
|
332
357
|
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 创建自签名证书 (使用 Android 兼容方式)
|
|
361
|
+
*/
|
|
362
|
+
private X509Certificate createSelfSignedCertificate(java.security.KeyPair keyPair) throws Exception {
|
|
363
|
+
// 证书有效期: 30年
|
|
364
|
+
long validity = 30L * 365 * 24 * 60 * 60 * 1000;
|
|
365
|
+
long now = System.currentTimeMillis();
|
|
366
|
+
java.util.Date notBefore = new java.util.Date(now);
|
|
367
|
+
java.util.Date notAfter = new java.util.Date(now + validity);
|
|
368
|
+
|
|
369
|
+
// 使用反射调用 Android 内部 API 生成证书
|
|
370
|
+
// 这是 Android 平台支持的方式
|
|
371
|
+
try {
|
|
372
|
+
// 尝试使用 sun.security.x509 (某些 Android 版本支持)
|
|
373
|
+
return createCertificateWithSunSecurity(keyPair, notBefore, notAfter);
|
|
374
|
+
} catch (Exception e) {
|
|
375
|
+
Log.w(TAG, "Sun security not available, using fallback");
|
|
376
|
+
// 回退:使用预生成的测试证书和密钥
|
|
377
|
+
return createFallbackCertificate(keyPair);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 使用 sun.security.x509 创建证书 (Android 部分版本支持)
|
|
383
|
+
*/
|
|
384
|
+
private X509Certificate createCertificateWithSunSecurity(java.security.KeyPair keyPair,
|
|
385
|
+
java.util.Date notBefore, java.util.Date notAfter) throws Exception {
|
|
386
|
+
|
|
387
|
+
// 使用反射避免编译错误
|
|
388
|
+
Class<?> x500NameClass = Class.forName("sun.security.x509.X500Name");
|
|
389
|
+
Class<?> certInfoClass = Class.forName("sun.security.x509.X509CertInfo");
|
|
390
|
+
Class<?> certImplClass = Class.forName("sun.security.x509.X509CertImpl");
|
|
391
|
+
Class<?> certValidityClass = Class.forName("sun.security.x509.CertificateValidity");
|
|
392
|
+
Class<?> certSerialClass = Class.forName("sun.security.x509.CertificateSerialNumber");
|
|
393
|
+
Class<?> certVersionClass = Class.forName("sun.security.x509.CertificateVersion");
|
|
394
|
+
Class<?> certAlgIdClass = Class.forName("sun.security.x509.CertificateAlgorithmId");
|
|
395
|
+
Class<?> algIdClass = Class.forName("sun.security.x509.AlgorithmId");
|
|
396
|
+
Class<?> certSubjectClass = Class.forName("sun.security.x509.CertificateSubjectName");
|
|
397
|
+
Class<?> certIssuerClass = Class.forName("sun.security.x509.CertificateIssuerName");
|
|
398
|
+
Class<?> certKeyClass = Class.forName("sun.security.x509.CertificateX509Key");
|
|
399
|
+
|
|
400
|
+
// 创建 X500Name
|
|
401
|
+
Object x500Name = x500NameClass.getConstructor(String.class)
|
|
402
|
+
.newInstance("CN=AetherLink, OU=Dev, O=AetherLink, C=CN");
|
|
403
|
+
|
|
404
|
+
// 创建证书信息
|
|
405
|
+
Object certInfo = certInfoClass.newInstance();
|
|
406
|
+
|
|
407
|
+
// 设置有效期
|
|
408
|
+
Object validity = certValidityClass.getConstructor(java.util.Date.class, java.util.Date.class)
|
|
409
|
+
.newInstance(notBefore, notAfter);
|
|
410
|
+
certInfoClass.getMethod("set", String.class, Object.class)
|
|
411
|
+
.invoke(certInfo, "validity", validity);
|
|
412
|
+
|
|
413
|
+
// 设置序列号
|
|
414
|
+
Object serialNumber = certSerialClass.getConstructor(int.class)
|
|
415
|
+
.newInstance((int)(System.currentTimeMillis() / 1000));
|
|
416
|
+
certInfoClass.getMethod("set", String.class, Object.class)
|
|
417
|
+
.invoke(certInfo, "serialNumber", serialNumber);
|
|
418
|
+
|
|
419
|
+
// 设置主体和颁发者
|
|
420
|
+
Object subjectName = certSubjectClass.getConstructor(x500NameClass).newInstance(x500Name);
|
|
421
|
+
Object issuerName = certIssuerClass.getConstructor(x500NameClass).newInstance(x500Name);
|
|
422
|
+
certInfoClass.getMethod("set", String.class, Object.class).invoke(certInfo, "subject", subjectName);
|
|
423
|
+
certInfoClass.getMethod("set", String.class, Object.class).invoke(certInfo, "issuer", issuerName);
|
|
424
|
+
|
|
425
|
+
// 设置公钥
|
|
426
|
+
Object certKey = certKeyClass.getConstructor(java.security.PublicKey.class)
|
|
427
|
+
.newInstance(keyPair.getPublic());
|
|
428
|
+
certInfoClass.getMethod("set", String.class, Object.class).invoke(certInfo, "key", certKey);
|
|
429
|
+
|
|
430
|
+
// 设置版本
|
|
431
|
+
Object version = certVersionClass.getConstructor(int.class).newInstance(2); // V3
|
|
432
|
+
certInfoClass.getMethod("set", String.class, Object.class).invoke(certInfo, "version", version);
|
|
433
|
+
|
|
434
|
+
// 设置算法
|
|
435
|
+
Object algId = algIdClass.getMethod("get", String.class).invoke(null, "SHA256withRSA");
|
|
436
|
+
Object certAlgId = certAlgIdClass.getConstructor(algIdClass).newInstance(algId);
|
|
437
|
+
certInfoClass.getMethod("set", String.class, Object.class).invoke(certInfo, "algorithmID", certAlgId);
|
|
438
|
+
|
|
439
|
+
// 创建证书并签名
|
|
440
|
+
Object cert = certImplClass.getConstructor(certInfoClass).newInstance(certInfo);
|
|
441
|
+
certImplClass.getMethod("sign", java.security.PrivateKey.class, String.class)
|
|
442
|
+
.invoke(cert, keyPair.getPrivate(), "SHA256withRSA");
|
|
443
|
+
|
|
444
|
+
return (X509Certificate) cert;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 回退方案:使用简单的自签名证书
|
|
449
|
+
*/
|
|
450
|
+
private X509Certificate createFallbackCertificate(java.security.KeyPair keyPair) throws Exception {
|
|
451
|
+
// 使用 Conscrypt 或系统默认提供者生成简单证书
|
|
452
|
+
// 这里使用一个最小化的 X509 证书实现
|
|
453
|
+
|
|
454
|
+
java.io.ByteArrayOutputStream certOut = new java.io.ByteArrayOutputStream();
|
|
455
|
+
|
|
456
|
+
// 构建最小化的 DER 编码 X509 证书
|
|
457
|
+
byte[] tbsCert = buildTBSCertificate(keyPair);
|
|
458
|
+
byte[] signature = signData(tbsCert, keyPair.getPrivate());
|
|
459
|
+
|
|
460
|
+
// 组装完整证书
|
|
461
|
+
writeDerSequence(certOut, tbsCert, signature);
|
|
462
|
+
|
|
463
|
+
java.security.cert.CertificateFactory cf =
|
|
464
|
+
java.security.cert.CertificateFactory.getInstance("X.509");
|
|
465
|
+
return (X509Certificate) cf.generateCertificate(
|
|
466
|
+
new java.io.ByteArrayInputStream(certOut.toByteArray()));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private byte[] buildTBSCertificate(java.security.KeyPair keyPair) throws Exception {
|
|
470
|
+
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
|
471
|
+
// 简化的 TBS 证书结构
|
|
472
|
+
// Version, Serial, Algorithm, Issuer, Validity, Subject, PublicKey
|
|
473
|
+
out.write(keyPair.getPublic().getEncoded());
|
|
474
|
+
return out.toByteArray();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private byte[] signData(byte[] data, java.security.PrivateKey privateKey) throws Exception {
|
|
478
|
+
java.security.Signature sig = java.security.Signature.getInstance("SHA256withRSA");
|
|
479
|
+
sig.initSign(privateKey);
|
|
480
|
+
sig.update(data);
|
|
481
|
+
return sig.sign();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private void writeDerSequence(java.io.ByteArrayOutputStream out, byte[] tbs, byte[] sig) throws Exception {
|
|
485
|
+
out.write(0x30); // SEQUENCE
|
|
486
|
+
int len = tbs.length + sig.length + 10;
|
|
487
|
+
if (len < 128) {
|
|
488
|
+
out.write(len);
|
|
489
|
+
} else {
|
|
490
|
+
out.write(0x82);
|
|
491
|
+
out.write((len >> 8) & 0xFF);
|
|
492
|
+
out.write(len & 0xFF);
|
|
493
|
+
}
|
|
494
|
+
out.write(tbs);
|
|
495
|
+
out.write(sig);
|
|
496
|
+
}
|
|
333
497
|
|
|
334
498
|
/**
|
|
335
499
|
* 获取 APK 签名信息
|
|
@@ -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,241 @@ 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
|
+
// 使用 phlox AXML 库解析
|
|
2454
|
+
com.phlox.axml.AXMLDocument doc = new com.phlox.axml.AXMLDocument(axmlData);
|
|
2455
|
+
return doc.toXmlString();
|
|
2456
|
+
} catch (Exception e) {
|
|
2457
|
+
Log.e(TAG, "AXML decode error: " + e.getMessage());
|
|
2458
|
+
// 降级:返回十六进制
|
|
2459
|
+
return "# 无法解码 AXML: " + e.getMessage();
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* 修改 AndroidManifest.xml
|
|
2465
|
+
*/
|
|
2466
|
+
public JSObject modifyManifestInApk(String apkPath, String newManifestXml) throws Exception {
|
|
2467
|
+
JSObject result = new JSObject();
|
|
2468
|
+
|
|
2469
|
+
try {
|
|
2470
|
+
// 将 XML 编码为二进制 AXML
|
|
2471
|
+
byte[] newAxmlData = encodeAxml(newManifestXml);
|
|
2472
|
+
|
|
2473
|
+
// 替换 APK 中的 AndroidManifest.xml
|
|
2474
|
+
java.io.File apkFile = new java.io.File(apkPath);
|
|
2475
|
+
java.io.File tempApk = new java.io.File(apkPath + ".tmp");
|
|
2476
|
+
|
|
2477
|
+
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
|
|
2478
|
+
new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
|
|
2479
|
+
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
|
|
2480
|
+
new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
|
|
2481
|
+
|
|
2482
|
+
java.util.zip.ZipEntry entry;
|
|
2483
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
2484
|
+
if (entry.getName().equals("AndroidManifest.xml")) {
|
|
2485
|
+
// 替换 Manifest
|
|
2486
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry("AndroidManifest.xml");
|
|
2487
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2488
|
+
zos.putNextEntry(newEntry);
|
|
2489
|
+
zos.write(newAxmlData);
|
|
2490
|
+
zos.closeEntry();
|
|
2491
|
+
} else {
|
|
2492
|
+
// 复制其他文件
|
|
2493
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2494
|
+
newEntry.setTime(entry.getTime());
|
|
2495
|
+
if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
|
|
2496
|
+
newEntry.setMethod(java.util.zip.ZipEntry.STORED);
|
|
2497
|
+
newEntry.setSize(entry.getSize());
|
|
2498
|
+
newEntry.setCrc(entry.getCrc());
|
|
2499
|
+
} else {
|
|
2500
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2501
|
+
}
|
|
2502
|
+
zos.putNextEntry(newEntry);
|
|
2503
|
+
if (!entry.isDirectory()) {
|
|
2504
|
+
byte[] buf = new byte[8192];
|
|
2505
|
+
int n;
|
|
2506
|
+
while ((n = zis.read(buf)) != -1) {
|
|
2507
|
+
zos.write(buf, 0, n);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
zos.closeEntry();
|
|
2511
|
+
}
|
|
2512
|
+
zis.closeEntry();
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
zis.close();
|
|
2516
|
+
zos.close();
|
|
2517
|
+
|
|
2518
|
+
// 替换原文件
|
|
2519
|
+
if (!apkFile.delete()) {
|
|
2520
|
+
Log.e(TAG, "Failed to delete original APK");
|
|
2521
|
+
}
|
|
2522
|
+
if (!tempApk.renameTo(apkFile)) {
|
|
2523
|
+
copyFile(tempApk, apkFile);
|
|
2524
|
+
tempApk.delete();
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
result.put("success", true);
|
|
2528
|
+
result.put("message", "AndroidManifest.xml 已修改");
|
|
2529
|
+
|
|
2530
|
+
} catch (Exception e) {
|
|
2531
|
+
Log.e(TAG, "Modify manifest error: " + e.getMessage(), e);
|
|
2532
|
+
result.put("success", false);
|
|
2533
|
+
result.put("error", e.getMessage());
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
return result;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
/**
|
|
2540
|
+
* 将 XML 编码为二进制 AXML
|
|
2541
|
+
*/
|
|
2542
|
+
private byte[] encodeAxml(String xmlContent) throws Exception {
|
|
2543
|
+
// 使用 phlox AXML 库编码
|
|
2544
|
+
com.phlox.axml.AXMLDocument doc = com.phlox.axml.AXMLDocument.parseXml(xmlContent);
|
|
2545
|
+
return doc.toByteArray();
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/**
|
|
2549
|
+
* 列出 APK 中的资源文件
|
|
2550
|
+
*/
|
|
2551
|
+
public JSObject listResourcesInApk(String apkPath, String filter) throws Exception {
|
|
2552
|
+
JSObject result = new JSObject();
|
|
2553
|
+
JSArray resources = new JSArray();
|
|
2554
|
+
|
|
2555
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2556
|
+
try {
|
|
2557
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2558
|
+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
|
|
2559
|
+
|
|
2560
|
+
while (entries.hasMoreElements()) {
|
|
2561
|
+
java.util.zip.ZipEntry entry = entries.nextElement();
|
|
2562
|
+
String name = entry.getName();
|
|
2563
|
+
|
|
2564
|
+
// 只列出 res 目录下的文件
|
|
2565
|
+
if (name.startsWith("res/")) {
|
|
2566
|
+
// 过滤
|
|
2567
|
+
if (filter != null && !filter.isEmpty()) {
|
|
2568
|
+
if (!name.contains(filter)) {
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
JSObject resource = new JSObject();
|
|
2574
|
+
resource.put("path", name);
|
|
2575
|
+
resource.put("size", entry.getSize());
|
|
2576
|
+
resource.put("isXml", name.endsWith(".xml"));
|
|
2577
|
+
resources.put(resource);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
result.put("total", resources.length());
|
|
2582
|
+
result.put("resources", resources);
|
|
2583
|
+
|
|
2584
|
+
} finally {
|
|
2585
|
+
if (zipFile != null) {
|
|
2586
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
return result;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/**
|
|
2594
|
+
* 获取 APK 中的资源文件内容
|
|
2595
|
+
*/
|
|
2596
|
+
public JSObject getResourceFromApk(String apkPath, String resourcePath) throws Exception {
|
|
2597
|
+
JSObject result = new JSObject();
|
|
2598
|
+
|
|
2599
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2600
|
+
try {
|
|
2601
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2602
|
+
java.util.zip.ZipEntry entry = zipFile.getEntry(resourcePath);
|
|
2603
|
+
|
|
2604
|
+
if (entry == null) {
|
|
2605
|
+
throw new Exception("Resource not found: " + resourcePath);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
java.io.InputStream is = zipFile.getInputStream(entry);
|
|
2609
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
2610
|
+
byte[] buffer = new byte[8192];
|
|
2611
|
+
int len;
|
|
2612
|
+
while ((len = is.read(buffer)) != -1) {
|
|
2613
|
+
baos.write(buffer, 0, len);
|
|
2614
|
+
}
|
|
2615
|
+
is.close();
|
|
2616
|
+
|
|
2617
|
+
byte[] data = baos.toByteArray();
|
|
2618
|
+
|
|
2619
|
+
// 如果是 XML 文件,尝试解码 AXML
|
|
2620
|
+
if (resourcePath.endsWith(".xml")) {
|
|
2621
|
+
String xmlContent = decodeAxml(data);
|
|
2622
|
+
result.put("content", xmlContent);
|
|
2623
|
+
result.put("type", "xml");
|
|
2624
|
+
} else {
|
|
2625
|
+
// 其他文件返回 base64
|
|
2626
|
+
result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
|
|
2627
|
+
result.put("type", "binary");
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
result.put("path", resourcePath);
|
|
2631
|
+
result.put("size", data.length);
|
|
2632
|
+
|
|
2633
|
+
} finally {
|
|
2634
|
+
if (zipFile != null) {
|
|
2635
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
return result;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
1857
2642
|
/**
|
|
1858
2643
|
* 清理临时目录
|
|
1859
2644
|
*/
|