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 信息 - 快速版本,直接解析二进制 AndroidManifest.xml
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
- // 直接从 ZIP 读取并解析 AndroidManifest.xml(比 PackageManager 快很多)
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-dex-editor",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Capacitor-plugin-for-editing-DEX-files-in-APK",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",