capacitor-dex-editor 0.0.5 → 0.0.6
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.
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
package com.aetherlink.dexeditor;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.content.Intent;
|
|
4
5
|
import android.content.pm.PackageInfo;
|
|
5
6
|
import android.content.pm.PackageManager;
|
|
6
7
|
import android.content.pm.Signature;
|
|
8
|
+
import android.net.Uri;
|
|
9
|
+
import android.os.Build;
|
|
7
10
|
import android.util.Log;
|
|
8
11
|
|
|
12
|
+
import androidx.core.content.FileProvider;
|
|
13
|
+
|
|
9
14
|
import com.getcapacitor.JSArray;
|
|
10
15
|
import com.getcapacitor.JSObject;
|
|
11
16
|
|
|
@@ -113,7 +118,7 @@ public class ApkManager {
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
/**
|
|
116
|
-
* 获取 APK 信息
|
|
121
|
+
* 获取 APK 信息
|
|
117
122
|
*/
|
|
118
123
|
public JSObject getApkInfo(String apkPath) throws Exception {
|
|
119
124
|
File apkFile = new File(apkPath);
|
|
@@ -126,13 +131,32 @@ public class ApkManager {
|
|
|
126
131
|
info.put("size", apkFile.length());
|
|
127
132
|
info.put("lastModified", apkFile.lastModified());
|
|
128
133
|
|
|
129
|
-
//
|
|
134
|
+
// 获取包信息
|
|
135
|
+
if (context != null) {
|
|
136
|
+
PackageManager pm = context.getPackageManager();
|
|
137
|
+
PackageInfo packageInfo = pm.getPackageArchiveInfo(apkPath,
|
|
138
|
+
PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES);
|
|
139
|
+
|
|
140
|
+
if (packageInfo != null) {
|
|
141
|
+
info.put("packageName", packageInfo.packageName);
|
|
142
|
+
info.put("versionName", packageInfo.versionName);
|
|
143
|
+
info.put("versionCode", packageInfo.versionCode);
|
|
144
|
+
|
|
145
|
+
if (packageInfo.applicationInfo != null) {
|
|
146
|
+
packageInfo.applicationInfo.sourceDir = apkPath;
|
|
147
|
+
packageInfo.applicationInfo.publicSourceDir = apkPath;
|
|
148
|
+
CharSequence label = pm.getApplicationLabel(packageInfo.applicationInfo);
|
|
149
|
+
info.put("appName", label.toString());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 列出 APK 内容
|
|
130
155
|
try (ZipFile zipFile = new ZipFile(apkFile)) {
|
|
131
156
|
int dexCount = 0;
|
|
132
157
|
int resCount = 0;
|
|
133
158
|
int libCount = 0;
|
|
134
159
|
|
|
135
|
-
// 统计文件数量
|
|
136
160
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
137
161
|
while (entries.hasMoreElements()) {
|
|
138
162
|
ZipEntry entry = entries.nextElement();
|
|
@@ -146,167 +170,10 @@ public class ApkManager {
|
|
|
146
170
|
info.put("dexCount", dexCount);
|
|
147
171
|
info.put("resourceCount", resCount);
|
|
148
172
|
info.put("nativeLibCount", libCount);
|
|
149
|
-
|
|
150
|
-
// 解析 AndroidManifest.xml
|
|
151
|
-
ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
|
|
152
|
-
if (manifestEntry != null) {
|
|
153
|
-
try (InputStream is = zipFile.getInputStream(manifestEntry)) {
|
|
154
|
-
byte[] data = readAllBytes(is);
|
|
155
|
-
parseManifestBinary(data, info);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
return info;
|
|
161
176
|
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 解析二进制格式的 AndroidManifest.xml
|
|
165
|
-
*/
|
|
166
|
-
private void parseManifestBinary(byte[] data, JSObject info) {
|
|
167
|
-
try {
|
|
168
|
-
// 二进制 XML 格式解析
|
|
169
|
-
if (data.length < 8) return;
|
|
170
|
-
|
|
171
|
-
// 跳过 XML 头部,查找字符串池
|
|
172
|
-
int offset = 8; // 跳过 magic number 和 file size
|
|
173
|
-
|
|
174
|
-
// 读取字符串池
|
|
175
|
-
if (offset + 8 > data.length) return;
|
|
176
|
-
int stringPoolOffset = offset;
|
|
177
|
-
int stringPoolType = readShort(data, offset);
|
|
178
|
-
if (stringPoolType != 0x0001) return; // String Pool chunk type
|
|
179
|
-
|
|
180
|
-
int stringPoolSize = readInt(data, offset + 4);
|
|
181
|
-
int stringCount = readInt(data, offset + 8);
|
|
182
|
-
int styleCount = readInt(data, offset + 12);
|
|
183
|
-
int stringsStart = readInt(data, offset + 20);
|
|
184
|
-
|
|
185
|
-
// 读取字符串偏移表
|
|
186
|
-
int[] stringOffsets = new int[stringCount];
|
|
187
|
-
int offsetTableStart = offset + 28;
|
|
188
|
-
for (int i = 0; i < stringCount && offsetTableStart + i * 4 + 4 <= data.length; i++) {
|
|
189
|
-
stringOffsets[i] = readInt(data, offsetTableStart + i * 4);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 字符串数据起始位置
|
|
193
|
-
int stringDataStart = stringPoolOffset + stringsStart;
|
|
194
|
-
|
|
195
|
-
// 提取字符串池
|
|
196
|
-
String[] strings = new String[stringCount];
|
|
197
|
-
for (int i = 0; i < stringCount; i++) {
|
|
198
|
-
int strOffset = stringDataStart + stringOffsets[i];
|
|
199
|
-
if (strOffset + 2 > data.length) continue;
|
|
200
|
-
|
|
201
|
-
// 检查是否 UTF-8 或 UTF-16
|
|
202
|
-
int len = data[strOffset] & 0xFF;
|
|
203
|
-
if (len == ((data[strOffset + 1] & 0xFF))) {
|
|
204
|
-
// UTF-8
|
|
205
|
-
strOffset += 2;
|
|
206
|
-
if (strOffset + len <= data.length) {
|
|
207
|
-
strings[i] = new String(data, strOffset, len, "UTF-8");
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
// UTF-16
|
|
211
|
-
int charLen = readShort(data, strOffset);
|
|
212
|
-
strOffset += 2;
|
|
213
|
-
if (strOffset + charLen * 2 <= data.length) {
|
|
214
|
-
strings[i] = new String(data, strOffset, charLen * 2, "UTF-16LE");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 在字符串池中查找关键信息
|
|
220
|
-
String packageName = null;
|
|
221
|
-
String versionName = null;
|
|
222
|
-
String versionCode = null;
|
|
223
|
-
String appLabel = null;
|
|
224
|
-
|
|
225
|
-
for (int i = 0; i < strings.length; i++) {
|
|
226
|
-
String s = strings[i];
|
|
227
|
-
if (s == null) continue;
|
|
228
|
-
|
|
229
|
-
// 查找 package 属性的值(通常在 manifest 标签后)
|
|
230
|
-
if (s.matches("^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$")) {
|
|
231
|
-
if (packageName == null) packageName = s;
|
|
232
|
-
}
|
|
233
|
-
// 版本名通常包含数字和点
|
|
234
|
-
if (s.matches("^\\d+\\.\\d+.*$") || s.matches("^v?\\d+\\.\\d+.*$")) {
|
|
235
|
-
if (versionName == null) versionName = s;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// 更精确的解析:遍历 XML 元素
|
|
240
|
-
offset = stringPoolOffset + stringPoolSize;
|
|
241
|
-
while (offset + 8 < data.length) {
|
|
242
|
-
int chunkType = readShort(data, offset);
|
|
243
|
-
int chunkSize = readInt(data, offset + 4);
|
|
244
|
-
|
|
245
|
-
if (chunkType == 0x0102) { // START_TAG
|
|
246
|
-
int attrStart = offset + 28;
|
|
247
|
-
int attrCount = readShort(data, offset + 24);
|
|
248
|
-
|
|
249
|
-
for (int i = 0; i < attrCount && attrStart + 20 <= data.length; i++) {
|
|
250
|
-
int nameIdx = readInt(data, attrStart + 4);
|
|
251
|
-
int valueIdx = readInt(data, attrStart + 8);
|
|
252
|
-
int valueType = data[attrStart + 15] & 0xFF;
|
|
253
|
-
int valueData = readInt(data, attrStart + 16);
|
|
254
|
-
|
|
255
|
-
if (nameIdx >= 0 && nameIdx < strings.length) {
|
|
256
|
-
String attrName = strings[nameIdx];
|
|
257
|
-
if ("package".equals(attrName) && valueIdx >= 0 && valueIdx < strings.length) {
|
|
258
|
-
packageName = strings[valueIdx];
|
|
259
|
-
} else if ("versionName".equals(attrName) && valueIdx >= 0 && valueIdx < strings.length) {
|
|
260
|
-
versionName = strings[valueIdx];
|
|
261
|
-
} else if ("versionCode".equals(attrName)) {
|
|
262
|
-
if (valueType == 0x10) { // TYPE_INT_DEC
|
|
263
|
-
versionCode = String.valueOf(valueData);
|
|
264
|
-
}
|
|
265
|
-
} else if ("label".equals(attrName) && valueIdx >= 0 && valueIdx < strings.length) {
|
|
266
|
-
String label = strings[valueIdx];
|
|
267
|
-
if (label != null && !label.startsWith("@")) {
|
|
268
|
-
appLabel = label;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
attrStart += 20;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (chunkSize <= 0) break;
|
|
277
|
-
offset += chunkSize;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (packageName != null) info.put("packageName", packageName);
|
|
281
|
-
if (versionName != null) info.put("versionName", versionName);
|
|
282
|
-
if (versionCode != null) info.put("versionCode", Integer.parseInt(versionCode));
|
|
283
|
-
if (appLabel != null) info.put("appName", appLabel);
|
|
284
|
-
|
|
285
|
-
} catch (Exception e) {
|
|
286
|
-
Log.w(TAG, "Failed to parse manifest binary: " + e.getMessage());
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private int readShort(byte[] data, int offset) {
|
|
291
|
-
return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private int readInt(byte[] data, int offset) {
|
|
295
|
-
return (data[offset] & 0xFF) |
|
|
296
|
-
((data[offset + 1] & 0xFF) << 8) |
|
|
297
|
-
((data[offset + 2] & 0xFF) << 16) |
|
|
298
|
-
((data[offset + 3] & 0xFF) << 24);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private byte[] readAllBytes(InputStream is) throws IOException {
|
|
302
|
-
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
|
303
|
-
byte[] data = new byte[BUFFER_SIZE];
|
|
304
|
-
int len;
|
|
305
|
-
while ((len = is.read(data)) != -1) {
|
|
306
|
-
buffer.write(data, 0, len);
|
|
307
|
-
}
|
|
308
|
-
return buffer.toByteArray();
|
|
309
|
-
}
|
|
310
177
|
|
|
311
178
|
/**
|
|
312
179
|
* 列出 APK 内容
|
|
@@ -503,6 +370,145 @@ public class ApkManager {
|
|
|
503
370
|
return result;
|
|
504
371
|
}
|
|
505
372
|
|
|
373
|
+
/**
|
|
374
|
+
* 安装 APK
|
|
375
|
+
*/
|
|
376
|
+
public void installApk(String apkPath) throws Exception {
|
|
377
|
+
if (context == null) {
|
|
378
|
+
throw new Exception("Context not available");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
File apkFile = new File(apkPath);
|
|
382
|
+
if (!apkFile.exists()) {
|
|
383
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
387
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
388
|
+
|
|
389
|
+
Uri apkUri;
|
|
390
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
391
|
+
// Android 7.0+ 需要使用 FileProvider
|
|
392
|
+
String authority = context.getPackageName() + ".fileprovider";
|
|
393
|
+
apkUri = FileProvider.getUriForFile(context, authority, apkFile);
|
|
394
|
+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
395
|
+
} else {
|
|
396
|
+
apkUri = Uri.fromFile(apkFile);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
|
|
400
|
+
context.startActivity(intent);
|
|
401
|
+
|
|
402
|
+
Log.d(TAG, "Started APK installation: " + apkPath);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 列出 APK 指定目录的内容(支持目录导航)
|
|
407
|
+
*/
|
|
408
|
+
public JSObject listApkDirectory(String apkPath, String directory) throws Exception {
|
|
409
|
+
File apkFile = new File(apkPath);
|
|
410
|
+
if (!apkFile.exists()) {
|
|
411
|
+
throw new IOException("APK file not found: " + apkPath);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 规范化目录路径
|
|
415
|
+
if (directory == null) directory = "";
|
|
416
|
+
if (directory.startsWith("/")) directory = directory.substring(1);
|
|
417
|
+
if (!directory.isEmpty() && !directory.endsWith("/")) directory += "/";
|
|
418
|
+
|
|
419
|
+
JSObject result = new JSObject();
|
|
420
|
+
result.put("currentPath", directory.isEmpty() ? "/" : "/" + directory);
|
|
421
|
+
|
|
422
|
+
JSArray items = new JSArray();
|
|
423
|
+
int folderCount = 0;
|
|
424
|
+
int fileCount = 0;
|
|
425
|
+
|
|
426
|
+
// 用于跟踪已添加的目录
|
|
427
|
+
java.util.Set<String> addedDirs = new java.util.HashSet<>();
|
|
428
|
+
|
|
429
|
+
try (ZipFile zipFile = new ZipFile(apkFile)) {
|
|
430
|
+
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
431
|
+
while (entries.hasMoreElements()) {
|
|
432
|
+
ZipEntry entry = entries.nextElement();
|
|
433
|
+
String name = entry.getName();
|
|
434
|
+
|
|
435
|
+
// 如果指定了目录,只显示该目录下的内容
|
|
436
|
+
if (!directory.isEmpty()) {
|
|
437
|
+
if (!name.startsWith(directory)) continue;
|
|
438
|
+
name = name.substring(directory.length());
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (name.isEmpty()) continue;
|
|
442
|
+
|
|
443
|
+
// 检查是否是直接子项(不包含更深层的路径)
|
|
444
|
+
int slashIndex = name.indexOf('/');
|
|
445
|
+
boolean isDirectChild = slashIndex == -1 || slashIndex == name.length() - 1;
|
|
446
|
+
|
|
447
|
+
if (isDirectChild) {
|
|
448
|
+
// 直接子项
|
|
449
|
+
JSObject item = new JSObject();
|
|
450
|
+
String displayName = name.endsWith("/") ? name.substring(0, name.length() - 1) : name;
|
|
451
|
+
item.put("name", displayName);
|
|
452
|
+
item.put("path", directory + name);
|
|
453
|
+
item.put("isDirectory", entry.isDirectory());
|
|
454
|
+
item.put("size", entry.getSize());
|
|
455
|
+
item.put("compressedSize", entry.getCompressedSize());
|
|
456
|
+
item.put("lastModified", entry.getTime());
|
|
457
|
+
|
|
458
|
+
// 根据文件类型设置图标类型
|
|
459
|
+
item.put("type", getFileType(displayName, entry.isDirectory()));
|
|
460
|
+
|
|
461
|
+
items.put(item);
|
|
462
|
+
if (entry.isDirectory()) folderCount++;
|
|
463
|
+
else fileCount++;
|
|
464
|
+
} else {
|
|
465
|
+
// 子目录中的文件,添加其父目录
|
|
466
|
+
String dirName = name.substring(0, slashIndex);
|
|
467
|
+
if (!addedDirs.contains(dirName)) {
|
|
468
|
+
addedDirs.add(dirName);
|
|
469
|
+
|
|
470
|
+
JSObject item = new JSObject();
|
|
471
|
+
item.put("name", dirName);
|
|
472
|
+
item.put("path", directory + dirName + "/");
|
|
473
|
+
item.put("isDirectory", true);
|
|
474
|
+
item.put("size", 0);
|
|
475
|
+
item.put("type", "folder");
|
|
476
|
+
|
|
477
|
+
items.put(item);
|
|
478
|
+
folderCount++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
result.put("items", items);
|
|
485
|
+
result.put("folderCount", folderCount);
|
|
486
|
+
result.put("fileCount", fileCount);
|
|
487
|
+
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 获取文件类型
|
|
493
|
+
*/
|
|
494
|
+
private String getFileType(String name, boolean isDirectory) {
|
|
495
|
+
if (isDirectory) return "folder";
|
|
496
|
+
|
|
497
|
+
String lowerName = name.toLowerCase();
|
|
498
|
+
if (lowerName.endsWith(".dex")) return "dex";
|
|
499
|
+
if (lowerName.endsWith(".xml")) return "xml";
|
|
500
|
+
if (lowerName.endsWith(".arsc")) return "resource";
|
|
501
|
+
if (lowerName.endsWith(".so")) return "native";
|
|
502
|
+
if (lowerName.endsWith(".png") || lowerName.endsWith(".jpg") ||
|
|
503
|
+
lowerName.endsWith(".jpeg") || lowerName.endsWith(".webp") ||
|
|
504
|
+
lowerName.endsWith(".gif")) return "image";
|
|
505
|
+
if (lowerName.endsWith(".smali")) return "smali";
|
|
506
|
+
if (lowerName.endsWith(".bin") || lowerName.endsWith(".dat")) return "binary";
|
|
507
|
+
if (lowerName.equals("androidmanifest.xml")) return "manifest";
|
|
508
|
+
|
|
509
|
+
return "file";
|
|
510
|
+
}
|
|
511
|
+
|
|
506
512
|
/**
|
|
507
513
|
* 关闭 APK 会话
|
|
508
514
|
*/
|
|
@@ -390,6 +390,17 @@ public class DexEditorPluginPlugin extends Plugin {
|
|
|
390
390
|
result.put("data", apkManager.getSessionDexFiles(params.getString("sessionId")));
|
|
391
391
|
break;
|
|
392
392
|
|
|
393
|
+
case "installApk":
|
|
394
|
+
apkManager.installApk(params.getString("apkPath"));
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case "listApkDirectory":
|
|
398
|
+
result.put("data", apkManager.listApkDirectory(
|
|
399
|
+
params.getString("apkPath"),
|
|
400
|
+
params.optString("directory", "")
|
|
401
|
+
));
|
|
402
|
+
break;
|
|
403
|
+
|
|
393
404
|
default:
|
|
394
405
|
result.put("success", false);
|
|
395
406
|
result.put("error", "Unknown action: " + action);
|