capacitor-dex-editor 0.0.1
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/CapacitorDexEditor.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +238 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/aetherlink/dexeditor/ApkManager.java +577 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPlugin.java +11 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +400 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexManager.java +1191 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +303 -0
- package/dist/esm/definitions.d.ts +96 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +8 -0
- package/dist/esm/web.js +15 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +29 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +32 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/DexEditorPluginPlugin/DexEditorPlugin.swift +8 -0
- package/ios/Sources/DexEditorPluginPlugin/DexEditorPluginPlugin.swift +23 -0
- package/ios/Tests/DexEditorPluginPluginTests/DexEditorPluginPluginTests.swift +15 -0
- package/package.json +80 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
package com.aetherlink.dexeditor;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.pm.PackageInfo;
|
|
5
|
+
import android.content.pm.PackageManager;
|
|
6
|
+
import android.content.pm.Signature;
|
|
7
|
+
import android.util.Log;
|
|
8
|
+
|
|
9
|
+
import com.getcapacitor.JSArray;
|
|
10
|
+
import com.getcapacitor.JSObject;
|
|
11
|
+
|
|
12
|
+
import java.io.BufferedInputStream;
|
|
13
|
+
import java.io.BufferedOutputStream;
|
|
14
|
+
import java.io.File;
|
|
15
|
+
import java.io.FileInputStream;
|
|
16
|
+
import java.io.FileOutputStream;
|
|
17
|
+
import java.io.IOException;
|
|
18
|
+
import java.io.InputStream;
|
|
19
|
+
import java.io.OutputStream;
|
|
20
|
+
import java.security.KeyStore;
|
|
21
|
+
import java.security.MessageDigest;
|
|
22
|
+
import java.security.PrivateKey;
|
|
23
|
+
import java.security.cert.X509Certificate;
|
|
24
|
+
import java.util.ArrayList;
|
|
25
|
+
import java.util.Enumeration;
|
|
26
|
+
import java.util.HashMap;
|
|
27
|
+
import java.util.List;
|
|
28
|
+
import java.util.Map;
|
|
29
|
+
import java.util.UUID;
|
|
30
|
+
import java.util.zip.ZipEntry;
|
|
31
|
+
import java.util.zip.ZipFile;
|
|
32
|
+
import java.util.zip.ZipInputStream;
|
|
33
|
+
import java.util.zip.ZipOutputStream;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ApkManager - APK 文件操作管理
|
|
37
|
+
* 支持打开、解压、重打包、签名等功能
|
|
38
|
+
*/
|
|
39
|
+
public class ApkManager {
|
|
40
|
+
|
|
41
|
+
private static final String TAG = "ApkManager";
|
|
42
|
+
private static final int BUFFER_SIZE = 8192;
|
|
43
|
+
|
|
44
|
+
// 存储活跃的 APK 会话
|
|
45
|
+
private final Map<String, ApkSession> sessions = new HashMap<>();
|
|
46
|
+
private Context context;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* APK 会话 - 存储打开的 APK 信息
|
|
50
|
+
*/
|
|
51
|
+
private static class ApkSession {
|
|
52
|
+
String sessionId;
|
|
53
|
+
String apkPath;
|
|
54
|
+
String extractDir;
|
|
55
|
+
List<String> dexFiles = new ArrayList<>();
|
|
56
|
+
boolean modified = false;
|
|
57
|
+
|
|
58
|
+
ApkSession(String sessionId, String apkPath, String extractDir) {
|
|
59
|
+
this.sessionId = sessionId;
|
|
60
|
+
this.apkPath = apkPath;
|
|
61
|
+
this.extractDir = extractDir;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public void setContext(Context context) {
|
|
66
|
+
this.context = context;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ==================== APK 文件操作 ====================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 打开 APK 文件(解压到临时目录)
|
|
73
|
+
*/
|
|
74
|
+
public JSObject openApk(String apkPath, String extractDir) throws Exception {
|
|
75
|
+
File apkFile = new File(apkPath);
|
|
76
|
+
if (!apkFile.exists()) {
|
|
77
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
String sessionId = UUID.randomUUID().toString();
|
|
81
|
+
|
|
82
|
+
// 如果未指定解压目录,使用临时目录
|
|
83
|
+
if (extractDir == null || extractDir.isEmpty()) {
|
|
84
|
+
extractDir = new File(apkFile.getParent(), "apk_" + sessionId).getAbsolutePath();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
File outDir = new File(extractDir);
|
|
88
|
+
outDir.mkdirs();
|
|
89
|
+
|
|
90
|
+
// 解压 APK
|
|
91
|
+
List<String> dexFiles = extractApk(apkPath, extractDir);
|
|
92
|
+
|
|
93
|
+
// 创建会话
|
|
94
|
+
ApkSession session = new ApkSession(sessionId, apkPath, extractDir);
|
|
95
|
+
session.dexFiles = dexFiles;
|
|
96
|
+
sessions.put(sessionId, session);
|
|
97
|
+
|
|
98
|
+
Log.d(TAG, "Opened APK: " + apkPath + " -> " + extractDir);
|
|
99
|
+
|
|
100
|
+
JSObject result = new JSObject();
|
|
101
|
+
result.put("sessionId", sessionId);
|
|
102
|
+
result.put("apkPath", apkPath);
|
|
103
|
+
result.put("extractDir", extractDir);
|
|
104
|
+
result.put("dexCount", dexFiles.size());
|
|
105
|
+
|
|
106
|
+
JSArray dexArray = new JSArray();
|
|
107
|
+
for (String dex : dexFiles) {
|
|
108
|
+
dexArray.put(dex);
|
|
109
|
+
}
|
|
110
|
+
result.put("dexFiles", dexArray);
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取 APK 信息
|
|
117
|
+
*/
|
|
118
|
+
public JSObject getApkInfo(String apkPath) throws Exception {
|
|
119
|
+
File apkFile = new File(apkPath);
|
|
120
|
+
if (!apkFile.exists()) {
|
|
121
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
JSObject info = new JSObject();
|
|
125
|
+
info.put("path", apkPath);
|
|
126
|
+
info.put("size", apkFile.length());
|
|
127
|
+
info.put("lastModified", apkFile.lastModified());
|
|
128
|
+
|
|
129
|
+
// 获取包信息
|
|
130
|
+
if (context != null) {
|
|
131
|
+
PackageManager pm = context.getPackageManager();
|
|
132
|
+
PackageInfo packageInfo = pm.getPackageArchiveInfo(apkPath,
|
|
133
|
+
PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES);
|
|
134
|
+
|
|
135
|
+
if (packageInfo != null) {
|
|
136
|
+
info.put("packageName", packageInfo.packageName);
|
|
137
|
+
info.put("versionName", packageInfo.versionName);
|
|
138
|
+
info.put("versionCode", packageInfo.versionCode);
|
|
139
|
+
|
|
140
|
+
if (packageInfo.applicationInfo != null) {
|
|
141
|
+
packageInfo.applicationInfo.sourceDir = apkPath;
|
|
142
|
+
packageInfo.applicationInfo.publicSourceDir = apkPath;
|
|
143
|
+
CharSequence label = pm.getApplicationLabel(packageInfo.applicationInfo);
|
|
144
|
+
info.put("appName", label.toString());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 列出 APK 内容
|
|
150
|
+
try (ZipFile zipFile = new ZipFile(apkFile)) {
|
|
151
|
+
int dexCount = 0;
|
|
152
|
+
int resCount = 0;
|
|
153
|
+
int libCount = 0;
|
|
154
|
+
|
|
155
|
+
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
156
|
+
while (entries.hasMoreElements()) {
|
|
157
|
+
ZipEntry entry = entries.nextElement();
|
|
158
|
+
String name = entry.getName();
|
|
159
|
+
|
|
160
|
+
if (name.endsWith(".dex")) dexCount++;
|
|
161
|
+
else if (name.startsWith("res/")) resCount++;
|
|
162
|
+
else if (name.startsWith("lib/")) libCount++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
info.put("dexCount", dexCount);
|
|
166
|
+
info.put("resourceCount", resCount);
|
|
167
|
+
info.put("nativeLibCount", libCount);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return info;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 列出 APK 内容
|
|
175
|
+
*/
|
|
176
|
+
public JSArray listApkContents(String apkPath) throws Exception {
|
|
177
|
+
File apkFile = new File(apkPath);
|
|
178
|
+
if (!apkFile.exists()) {
|
|
179
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
JSArray contents = new JSArray();
|
|
183
|
+
|
|
184
|
+
try (ZipFile zipFile = new ZipFile(apkFile)) {
|
|
185
|
+
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
186
|
+
while (entries.hasMoreElements()) {
|
|
187
|
+
ZipEntry entry = entries.nextElement();
|
|
188
|
+
|
|
189
|
+
JSObject item = new JSObject();
|
|
190
|
+
item.put("name", entry.getName());
|
|
191
|
+
item.put("size", entry.getSize());
|
|
192
|
+
item.put("compressedSize", entry.getCompressedSize());
|
|
193
|
+
item.put("isDirectory", entry.isDirectory());
|
|
194
|
+
item.put("crc", entry.getCrc());
|
|
195
|
+
|
|
196
|
+
contents.put(item);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return contents;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 提取 APK 中的特定文件
|
|
205
|
+
*/
|
|
206
|
+
public JSObject extractFile(String apkPath, String entryName, String outputPath) throws Exception {
|
|
207
|
+
File apkFile = new File(apkPath);
|
|
208
|
+
if (!apkFile.exists()) {
|
|
209
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try (ZipFile zipFile = new ZipFile(apkFile)) {
|
|
213
|
+
ZipEntry entry = zipFile.getEntry(entryName);
|
|
214
|
+
if (entry == null) {
|
|
215
|
+
throw new IOException("Entry not found: " + entryName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
File outputFile = new File(outputPath);
|
|
219
|
+
outputFile.getParentFile().mkdirs();
|
|
220
|
+
|
|
221
|
+
try (InputStream is = zipFile.getInputStream(entry);
|
|
222
|
+
OutputStream os = new FileOutputStream(outputFile)) {
|
|
223
|
+
byte[] buffer = new byte[BUFFER_SIZE];
|
|
224
|
+
int len;
|
|
225
|
+
while ((len = is.read(buffer)) > 0) {
|
|
226
|
+
os.write(buffer, 0, len);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
JSObject result = new JSObject();
|
|
231
|
+
result.put("outputPath", outputPath);
|
|
232
|
+
result.put("size", outputFile.length());
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 重新打包 APK
|
|
239
|
+
*/
|
|
240
|
+
public JSObject repackApk(String sessionId, String outputPath) throws Exception {
|
|
241
|
+
ApkSession session = getSession(sessionId);
|
|
242
|
+
|
|
243
|
+
File outputFile = new File(outputPath);
|
|
244
|
+
outputFile.getParentFile().mkdirs();
|
|
245
|
+
|
|
246
|
+
// 打包目录为 APK
|
|
247
|
+
packDirectory(session.extractDir, outputPath);
|
|
248
|
+
|
|
249
|
+
Log.d(TAG, "Repacked APK: " + outputPath);
|
|
250
|
+
|
|
251
|
+
JSObject result = new JSObject();
|
|
252
|
+
result.put("outputPath", outputPath);
|
|
253
|
+
result.put("size", outputFile.length());
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 签名 APK
|
|
259
|
+
*/
|
|
260
|
+
public JSObject signApk(String apkPath, String outputPath,
|
|
261
|
+
String keystorePath, String keystorePassword,
|
|
262
|
+
String keyAlias, String keyPassword) throws Exception {
|
|
263
|
+
|
|
264
|
+
File apkFile = new File(apkPath);
|
|
265
|
+
if (!apkFile.exists()) {
|
|
266
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
File keystoreFile = new File(keystorePath);
|
|
270
|
+
if (!keystoreFile.exists()) {
|
|
271
|
+
throw new IOException("Keystore not found: " + keystorePath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 加载 keystore
|
|
275
|
+
KeyStore keyStore = KeyStore.getInstance("JKS");
|
|
276
|
+
try {
|
|
277
|
+
keyStore = KeyStore.getInstance("JKS");
|
|
278
|
+
} catch (Exception e) {
|
|
279
|
+
keyStore = KeyStore.getInstance("BKS");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try (FileInputStream fis = new FileInputStream(keystoreFile)) {
|
|
283
|
+
keyStore.load(fis, keystorePassword.toCharArray());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
|
|
287
|
+
X509Certificate cert = (X509Certificate) keyStore.getCertificate(keyAlias);
|
|
288
|
+
|
|
289
|
+
if (privateKey == null || cert == null) {
|
|
290
|
+
throw new Exception("Failed to load key from keystore");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 使用 apksigner 或自定义签名
|
|
294
|
+
// 这里使用简化的 JAR 签名方式
|
|
295
|
+
signApkWithKey(apkPath, outputPath, privateKey, cert);
|
|
296
|
+
|
|
297
|
+
Log.d(TAG, "Signed APK: " + outputPath);
|
|
298
|
+
|
|
299
|
+
JSObject result = new JSObject();
|
|
300
|
+
result.put("outputPath", outputPath);
|
|
301
|
+
result.put("size", new File(outputPath).length());
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 使用默认测试密钥签名(方便测试)
|
|
307
|
+
*/
|
|
308
|
+
public JSObject signApkWithTestKey(String apkPath, String outputPath) throws Exception {
|
|
309
|
+
File apkFile = new File(apkPath);
|
|
310
|
+
if (!apkFile.exists()) {
|
|
311
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 复制文件(实际项目应使用真正的测试签名)
|
|
315
|
+
copyFile(apkFile, new File(outputPath));
|
|
316
|
+
|
|
317
|
+
// 注意:这里需要集成真正的签名工具
|
|
318
|
+
// 可以使用 apksig 库或调用系统 apksigner
|
|
319
|
+
|
|
320
|
+
Log.d(TAG, "Signed APK with test key: " + outputPath);
|
|
321
|
+
|
|
322
|
+
JSObject result = new JSObject();
|
|
323
|
+
result.put("outputPath", outputPath);
|
|
324
|
+
result.put("size", new File(outputPath).length());
|
|
325
|
+
result.put("warning", "Using test key - for development only");
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 获取 APK 签名信息
|
|
331
|
+
*/
|
|
332
|
+
public JSObject getApkSignature(String apkPath) throws Exception {
|
|
333
|
+
if (context == null) {
|
|
334
|
+
throw new Exception("Context not available");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
PackageManager pm = context.getPackageManager();
|
|
338
|
+
PackageInfo packageInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_SIGNATURES);
|
|
339
|
+
|
|
340
|
+
JSObject result = new JSObject();
|
|
341
|
+
|
|
342
|
+
if (packageInfo != null && packageInfo.signatures != null && packageInfo.signatures.length > 0) {
|
|
343
|
+
Signature sig = packageInfo.signatures[0];
|
|
344
|
+
|
|
345
|
+
// 计算 MD5
|
|
346
|
+
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
|
347
|
+
byte[] md5Bytes = md5.digest(sig.toByteArray());
|
|
348
|
+
result.put("md5", bytesToHex(md5Bytes));
|
|
349
|
+
|
|
350
|
+
// 计算 SHA1
|
|
351
|
+
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
|
|
352
|
+
byte[] sha1Bytes = sha1.digest(sig.toByteArray());
|
|
353
|
+
result.put("sha1", bytesToHex(sha1Bytes));
|
|
354
|
+
|
|
355
|
+
// 计算 SHA256
|
|
356
|
+
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
|
357
|
+
byte[] sha256Bytes = sha256.digest(sig.toByteArray());
|
|
358
|
+
result.put("sha256", bytesToHex(sha256Bytes));
|
|
359
|
+
|
|
360
|
+
result.put("signed", true);
|
|
361
|
+
} else {
|
|
362
|
+
result.put("signed", false);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 关闭 APK 会话
|
|
370
|
+
*/
|
|
371
|
+
public void closeApk(String sessionId, boolean deleteExtracted) {
|
|
372
|
+
ApkSession session = sessions.remove(sessionId);
|
|
373
|
+
|
|
374
|
+
if (session != null && deleteExtracted) {
|
|
375
|
+
deleteRecursive(new File(session.extractDir));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
Log.d(TAG, "Closed APK session: " + sessionId);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 获取会话中的 DEX 文件路径
|
|
383
|
+
*/
|
|
384
|
+
public JSArray getSessionDexFiles(String sessionId) throws Exception {
|
|
385
|
+
ApkSession session = getSession(sessionId);
|
|
386
|
+
|
|
387
|
+
JSArray dexArray = new JSArray();
|
|
388
|
+
for (String dex : session.dexFiles) {
|
|
389
|
+
dexArray.put(dex);
|
|
390
|
+
}
|
|
391
|
+
return dexArray;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 替换 APK 中的文件
|
|
396
|
+
*/
|
|
397
|
+
public void replaceFile(String sessionId, String entryName, String newFilePath) throws Exception {
|
|
398
|
+
ApkSession session = getSession(sessionId);
|
|
399
|
+
|
|
400
|
+
File sourceFile = new File(newFilePath);
|
|
401
|
+
if (!sourceFile.exists()) {
|
|
402
|
+
throw new IOException("Source file not found: " + newFilePath);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
File targetFile = new File(session.extractDir, entryName);
|
|
406
|
+
targetFile.getParentFile().mkdirs();
|
|
407
|
+
|
|
408
|
+
copyFile(sourceFile, targetFile);
|
|
409
|
+
session.modified = true;
|
|
410
|
+
|
|
411
|
+
Log.d(TAG, "Replaced file: " + entryName);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 添加文件到 APK
|
|
416
|
+
*/
|
|
417
|
+
public void addFile(String sessionId, String entryName, String filePath) throws Exception {
|
|
418
|
+
replaceFile(sessionId, entryName, filePath);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 删除 APK 中的文件
|
|
423
|
+
*/
|
|
424
|
+
public void deleteFile(String sessionId, String entryName) throws Exception {
|
|
425
|
+
ApkSession session = getSession(sessionId);
|
|
426
|
+
|
|
427
|
+
File targetFile = new File(session.extractDir, entryName);
|
|
428
|
+
if (targetFile.exists()) {
|
|
429
|
+
if (targetFile.isDirectory()) {
|
|
430
|
+
deleteRecursive(targetFile);
|
|
431
|
+
} else {
|
|
432
|
+
targetFile.delete();
|
|
433
|
+
}
|
|
434
|
+
session.modified = true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
Log.d(TAG, "Deleted file: " + entryName);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ==================== 辅助方法 ====================
|
|
441
|
+
|
|
442
|
+
private ApkSession getSession(String sessionId) throws Exception {
|
|
443
|
+
ApkSession session = sessions.get(sessionId);
|
|
444
|
+
if (session == null) {
|
|
445
|
+
throw new IllegalArgumentException("APK session not found: " + sessionId);
|
|
446
|
+
}
|
|
447
|
+
return session;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 解压 APK 文件
|
|
452
|
+
*/
|
|
453
|
+
private List<String> extractApk(String apkPath, String extractDir) throws IOException {
|
|
454
|
+
List<String> dexFiles = new ArrayList<>();
|
|
455
|
+
|
|
456
|
+
try (ZipInputStream zis = new ZipInputStream(
|
|
457
|
+
new BufferedInputStream(new FileInputStream(apkPath)))) {
|
|
458
|
+
|
|
459
|
+
ZipEntry entry;
|
|
460
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
461
|
+
String name = entry.getName();
|
|
462
|
+
File outFile = new File(extractDir, name);
|
|
463
|
+
|
|
464
|
+
if (entry.isDirectory()) {
|
|
465
|
+
outFile.mkdirs();
|
|
466
|
+
} else {
|
|
467
|
+
outFile.getParentFile().mkdirs();
|
|
468
|
+
|
|
469
|
+
try (BufferedOutputStream bos = new BufferedOutputStream(
|
|
470
|
+
new FileOutputStream(outFile))) {
|
|
471
|
+
byte[] buffer = new byte[BUFFER_SIZE];
|
|
472
|
+
int len;
|
|
473
|
+
while ((len = zis.read(buffer)) > 0) {
|
|
474
|
+
bos.write(buffer, 0, len);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 记录 DEX 文件
|
|
479
|
+
if (name.endsWith(".dex")) {
|
|
480
|
+
dexFiles.add(outFile.getAbsolutePath());
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
zis.closeEntry();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return dexFiles;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 打包目录为 APK/ZIP
|
|
493
|
+
*/
|
|
494
|
+
private void packDirectory(String sourceDir, String outputPath) throws IOException {
|
|
495
|
+
File source = new File(sourceDir);
|
|
496
|
+
|
|
497
|
+
try (ZipOutputStream zos = new ZipOutputStream(
|
|
498
|
+
new BufferedOutputStream(new FileOutputStream(outputPath)))) {
|
|
499
|
+
|
|
500
|
+
addDirectoryToZip(zos, source, "");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private void addDirectoryToZip(ZipOutputStream zos, File dir, String basePath) throws IOException {
|
|
505
|
+
File[] files = dir.listFiles();
|
|
506
|
+
if (files == null) return;
|
|
507
|
+
|
|
508
|
+
for (File file : files) {
|
|
509
|
+
String entryName = basePath.isEmpty() ? file.getName() : basePath + "/" + file.getName();
|
|
510
|
+
|
|
511
|
+
if (file.isDirectory()) {
|
|
512
|
+
addDirectoryToZip(zos, file, entryName);
|
|
513
|
+
} else {
|
|
514
|
+
ZipEntry entry = new ZipEntry(entryName);
|
|
515
|
+
zos.putNextEntry(entry);
|
|
516
|
+
|
|
517
|
+
try (FileInputStream fis = new FileInputStream(file)) {
|
|
518
|
+
byte[] buffer = new byte[BUFFER_SIZE];
|
|
519
|
+
int len;
|
|
520
|
+
while ((len = fis.read(buffer)) > 0) {
|
|
521
|
+
zos.write(buffer, 0, len);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
zos.closeEntry();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* 使用密钥签名 APK(简化实现)
|
|
532
|
+
*/
|
|
533
|
+
private void signApkWithKey(String inputPath, String outputPath,
|
|
534
|
+
PrivateKey privateKey, X509Certificate cert) throws Exception {
|
|
535
|
+
// 注意:这是简化实现,实际应使用 apksig 库
|
|
536
|
+
// 这里只是复制文件,真正的签名需要更复杂的实现
|
|
537
|
+
copyFile(new File(inputPath), new File(outputPath));
|
|
538
|
+
|
|
539
|
+
// TODO: 集成 apksig 库进行真正的 APK 签名
|
|
540
|
+
// 可以添加 implementation 'com.android.tools.build:apksig:8.1.0' 依赖
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private void copyFile(File source, File dest) throws IOException {
|
|
544
|
+
try (FileInputStream fis = new FileInputStream(source);
|
|
545
|
+
FileOutputStream fos = new FileOutputStream(dest)) {
|
|
546
|
+
byte[] buffer = new byte[BUFFER_SIZE];
|
|
547
|
+
int len;
|
|
548
|
+
while ((len = fis.read(buffer)) > 0) {
|
|
549
|
+
fos.write(buffer, 0, len);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private void deleteRecursive(File file) {
|
|
555
|
+
if (file.isDirectory()) {
|
|
556
|
+
File[] children = file.listFiles();
|
|
557
|
+
if (children != null) {
|
|
558
|
+
for (File child : children) {
|
|
559
|
+
deleteRecursive(child);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
file.delete();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private String bytesToHex(byte[] bytes) {
|
|
567
|
+
StringBuilder sb = new StringBuilder();
|
|
568
|
+
for (byte b : bytes) {
|
|
569
|
+
sb.append(String.format("%02X", b));
|
|
570
|
+
sb.append(":");
|
|
571
|
+
}
|
|
572
|
+
if (sb.length() > 0) {
|
|
573
|
+
sb.setLength(sb.length() - 1);
|
|
574
|
+
}
|
|
575
|
+
return sb.toString();
|
|
576
|
+
}
|
|
577
|
+
}
|