diff --git a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java
index 64511263..a3c9dc91 100644
--- a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java
+++ b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java
@@ -1,6 +1,8 @@
package com.youlai.boot.app.packages.component;
import com.android.aapt.Resources;
+import com.android.bundle.Commands;
+import com.android.tools.apk.analyzer.BinaryXmlParser;
import com.android.tools.build.bundletool.commands.DumpCommand;
import com.android.tools.build.bundletool.model.AndroidManifest;
import com.android.tools.build.bundletool.model.AppBundle;
@@ -23,6 +25,8 @@ import org.zeroturnaround.zip.ZipUtil;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -96,6 +100,9 @@ public class ApkMetaParser {
*
* 直接使用 bundletool 依赖(非命令行、非 jar)的 {@link DumpCommand} 输出
* base 模块的 AndroidManifest.xml 文本,再提取关键字段。
+ *
+ * splits
+ * toc.pb
*/
public static ApkMetaResult parseAab(File aabFile) throws IOException {
ApkMetaResult apkMetaResult = new ApkMetaResult();
@@ -118,7 +125,7 @@ public class ApkMetaParser {
String packageName = manifest.getPackageName();
apkMetaResult.setPackageName(packageName);
int versionCode = manifest.getVersionCode().orElse(-1);
- apkMetaResult.setVersionCode(Long.valueOf(versionCode));
+ apkMetaResult.setVersionCode((long) versionCode);
String versionName = manifest.getVersionName().orElse("");
apkMetaResult.setVersionName(versionName);
apkMetaResult.setMinSdk(manifest.getMinSdkVersion().orElse(-1));
@@ -317,13 +324,128 @@ public class ApkMetaParser {
* bundletool 依赖可直接解析 .apks 整包并 dump 其 Manifest;同时枚举 ZIP 内
* 的 apk 条目记录 split 列表。
*/
- @Deprecated
public static ApkMetaResult parseApks(File apksFile) throws IOException {
- ApkMetaResult result = parseByBundleTool(apksFile);
- result.setSplitApkList(collectApkEntries(apksFile));
- // 图标位于内部各 split apk 的 res/mipmap-* 或 res/drawable-*,逐包扫描
- fillBundleIcon(result, apksFile, false);
- return result;
+ List baseApkSuffixList = new ArrayList<>(Arrays.asList("base-master.apk", "universal.apk"));
+
+ ApkMetaResult apkMetaResult = new ApkMetaResult();
+
+ try (ZipFile apksZip = new ZipFile(apksFile)) {
+ ZipEntry tocEntry = apksZip.getEntry("toc.pb");
+ if (tocEntry == null) {
+ LOGGER.warn("非标准的 .apks 文件,缺失 toc.pb");
+ baseApkSuffixList.add("base.apk");
+ } else {
+// try (InputStream tocStream = apksZip.getInputStream(tocEntry)) {
+// Commands.BuildApksResult apksResult = Commands.BuildApksResult.parseFrom(tocStream);
+// // System.out.println("=== 基础信息 (来自 toc.pb) ===" );
+// // System.out.println("Package Name: " + apksResult.getPackageName());
+// //
+// for (Commands.Variant variant : apksResult.getVariantList()) {
+// System.out.println("Variant Number: " + variant.getVariantNumber());
+// for (Commands.ApkSet apkSet : variant.getApkSetList()) {
+// for (Commands.ApkDescription apkDesc : apkSet.getApkDescriptionList()) {
+// System.out.println(" - 包含 APK: " + apkDesc.getPath());
+// }
+// }
+// }
+// }
+ }
+
+ // 补全 split 列表:读取 .apks 容器内所有 *.apk 条目
+ List splitApkList = apksZip.stream()
+ .map(ZipEntry::getName)
+ .filter(name -> name.toLowerCase(Locale.ROOT).endsWith(".apk"))
+ .collect(Collectors.toList());
+ apkMetaResult.setSplitApkList(splitApkList);
+
+ // ==========================================
+ // 步骤 1:定位并提取 .apks 中的 Base APK 或 Universal APK
+ // ==========================================
+ ZipEntry baseApkEntry = apksZip.stream()
+ .filter(e -> baseApkSuffixList.stream().anyMatch(e.getName()::endsWith))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("在 .apks 中未找到有效的 Base APK 或 Universal APK"));
+
+ Path tempBaseApkPath = Files.createTempFile("optimized_base_", ".apk");
+
+ try {
+ try (InputStream is = apksZip.getInputStream(baseApkEntry)) {
+ Files.copy(is, tempBaseApkPath, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ try (ApkFile apkFileParser = new ApkFile(tempBaseApkPath.toFile())) {
+ apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE);
+ ApkMeta meta = apkFileParser.getApkMeta();
+
+ fillFromApkMeta(apkMetaResult, meta);
+ apkMetaResult.setAbiList(extractAbiList(tempBaseApkPath.toFile()));
+ fillIcon(apkMetaResult, apkFileParser);
+ }
+
+ // ==========================================
+ // 步骤 2:读取 Base APK 内部的二进制 AndroidManifest.xml
+ // ==========================================
+ try (ZipFile baseApkZip = new ZipFile(tempBaseApkPath.toFile())) {
+ ZipEntry manifestEntry = baseApkZip.getEntry("AndroidManifest.xml");
+ if (manifestEntry == null) {
+ throw new RuntimeException("Base APK 中缺失 AndroidManifest.xml 文件");
+ }
+
+ byte[] binaryManifestBytes;
+ try (InputStream manifestIs = baseApkZip.getInputStream(manifestEntry);
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
+ byte[] data = new byte[4096];
+ int nRead;
+ while ((nRead = manifestIs.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ binaryManifestBytes = buffer.toByteArray();
+ }
+
+ // ==========================================
+ // 步骤 3:使用 apkanalyzer 的 BinaryXmlParser 解码二进制 XML
+ // ==========================================
+ byte[] decodedXmlBytes = BinaryXmlParser.decodeXml(binaryManifestBytes);
+ String manifestXml = new String(decodedXmlBytes, StandardCharsets.UTF_8);
+
+ // ==========================================
+ // 步骤 4:提取包名与版本详情
+ // ==========================================
+ String label = extractXmlAttribute(manifestXml, "android:label");
+ String packageName = extractXmlAttribute(manifestXml, "package");
+ String versionName = extractXmlAttribute(manifestXml, "android:versionName");
+// long versionCode = extractXmlAttribute(manifestXml, "android:versionCode");
+// int compileSdkVersion = extractXmlAttribute(manifestXml, "android:compileSdkVersion");
+// int minSdkVersion = extractXmlAttribute(manifestXml, "android:minSdkVersion");
+// int targetSdkVersion = extractXmlAttribute(manifestXml, "android:targetSdkVersion");
+
+// apkMetaResult.setAppLabel(label);
+// apkMetaResult.setPackageName(packageName);
+// apkMetaResult.setVersionName(versionName);
+
+ }
+
+ } finally {
+ // 安全清理临时生成的 APK 文件
+ Files.deleteIfExists(tempBaseApkPath);
+ }
+ } catch (Exception e) {
+ LOGGER.error("解析 APKS 失败", e);
+ }
+ return apkMetaResult;
+ }
+
+
+ /**
+ * 辅助方法:从 XML 字符串中极简提取属性(仅作演示,实战可用 JDK org.w3c.dom 库)
+ */
+ private static String extractXmlAttribute(String xml, String attributeName) {
+ String regex = attributeName + "=\"([^\"]+)\"";
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile(regex).matcher(xml);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return "";
}
// ============================ APKM ============================
@@ -558,6 +680,7 @@ public class ApkMetaParser {
@Deprecated
private static ApkMetaResult parseSapk(File xapkFile) {
ApkMetaResult apkMetaResult = new ApkMetaResult();
+ // todo 暂未实现,不知道哪里有这种格式
return apkMetaResult;
}
@@ -822,7 +945,10 @@ public class ApkMetaParser {
try {
List icons = apkFileParser.getIconFiles();
if (icons != null && !icons.isEmpty()) {
- Icon icon = icons.get(0);
+ // 从所有候选图标中选取 density 最大(分辨率最高)的一个
+ Icon icon = icons.stream()
+ .max(Comparator.comparingInt(Icon::getDensity))
+ .orElse(icons.get(0));
result.setIconPath(icon.getPath());
byte[] data = icon.getData();
if (data != null && data.length > 0) {
diff --git a/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java b/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java
index 62c1b667..d7bad0cb 100644
--- a/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java
+++ b/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java
@@ -33,6 +33,8 @@ class ApkMetaParserTest {
"D:\\apkTest\\XwadOSHonor_V1.4.7.aab",
"D:\\apkTest\\XwadOSHonor_V1.4.7.apks",
"D:\\apkTest\\Twitter_v8.95.0-release.00_apkpure.com.xapk",
+ "D:\\apkTest\\MIR4_0.488243_APKPure.xapk",
+ "D:\\apkTest\\GitHub_1.271.1_APKPure.xapk",
"D:\\apkTest\\com.instagram.android_440.1.0.46.86-384611414_1dpi_c0d8039a2b3d4f69b31e6cbb1c611c5a_apkmirror.com.apkm",
"D:\\apkTest\\抖音V39.1.2谷歌版.apks"
);
@@ -70,10 +72,11 @@ class ApkMetaParserTest {
int total = paths.size();
int success = 0;
- for (String path : paths) {
+ for (int i = 0; i < paths.size(); i++) {
+ String path = paths.get(i);
File apkFile = new File(path);
System.out.println();
- System.out.println(">>>>>> [" + (success + 1) + "/" + total + "] " + apkFile.getAbsolutePath());
+ System.out.println(">>>>>> [" + (i + 1) + "/" + total + "] " + apkFile.getAbsolutePath());
if (!apkFile.exists() || !apkFile.isFile()) {
System.out.println("!! 跳过:文件不存在或不是普通文件");