diff --git a/brut.apktool/apktool-lib/build.gradle.kts b/brut.apktool/apktool-lib/build.gradle.kts index fd0602f1ac..ced180a5f0 100644 --- a/brut.apktool/apktool-lib/build.gradle.kts +++ b/brut.apktool/apktool-lib/build.gradle.kts @@ -38,4 +38,16 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.xmlunit) + + val sdkRoot = System.getenv("ANDROID_HOME") + compileOnly( + if (sdkRoot == null) { + GradleException("Missing ANDROID_HOME").printStackTrace() + + "com.google.android:android:4.1.1.4" + } else { + val androidVersion = 33 + files("$sdkRoot/platforms/android-$androidVersion/android.jar") + } + ) } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java index b8964b734d..b9b4124abe 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java @@ -78,6 +78,10 @@ private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir, if (resDir != null) { File buildDir = new File(resDir.getParent(), "build"); + + //noinspection ResultOfMethodCallIgnored + buildDir.mkdir(); + resourcesZip = new File(buildDir, "resources.zip"); } @@ -170,7 +174,8 @@ private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir, cmd.add("--no-version-transitions"); cmd.add("--no-resource-deduping"); - cmd.add("--no-compile-sdk-metadata"); + // TODO: Add this back, once AAPT2 from platform-tools 34.0.4 is stable + // cmd.add("--no-compile-sdk-metadata"); // #3427 - Ignore stricter parsing during aapt2 cmd.add("--warn-manifest-validation"); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java index a999052dff..3f69db1bb1 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java @@ -57,6 +57,12 @@ public ApkDecoder(ExtFile apkFile) { this(apkFile, Config.getDefaultConfig()); } + public ApkDecoder(ApkInfo apkInfo, Config config) { + this(apkInfo.getApkFile(), config); + + mApkInfo = apkInfo; + } + public ApkDecoder(ExtFile apkFile, Config config) { mApkFile = apkFile; mConfig = config; @@ -74,7 +80,7 @@ public ApkInfo decode(File outDir) throws AndrolibException { mWorker = new BackgroundWorker(mConfig.jobs - 1); } try { - mApkInfo = new ApkInfo(mApkFile); + if (mApkInfo == null) mApkInfo = new ApkInfo(mApkFile); mResDecoder = new ResourcesDecoder(mConfig, mApkInfo); try { @@ -325,8 +331,15 @@ private void writeApkInfo(File outDir) throws AndrolibException { } // record uncompressed files + Map resFileMapping = mResDecoder.getResFileMapping(); + recordUncompressedFiles(resFileMapping); + + // write apk info to file + mApkInfo.save(new File(outDir, "apktool.yml")); + } + + public void recordUncompressedFiles(Map resFileMapping) throws AndrolibException { try { - Map resFileMapping = mResDecoder.getResFileMapping(); Set uncompressedExts = new HashSet<>(); Set uncompressedFiles = new HashSet<>(); Directory in = mApkFile.getDirectory(); @@ -377,8 +390,5 @@ private void writeApkInfo(File outDir) throws AndrolibException { } catch (DirectoryException ex) { throw new AndrolibException(ex); } - - // write apk info to file - mApkInfo.save(new File(outDir, "apktool.yml")); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java index f94b6ddca1..0a837e679d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java @@ -28,6 +28,7 @@ import brut.directory.ExtFile; import brut.xmlpull.MXSerializer; import com.google.common.collect.Sets; +import brut.util.OSDetection; import org.xmlpull.v1.XmlSerializer; import java.io.*; @@ -46,6 +47,7 @@ public class ResourcesDecoder { private final ApkInfo mApkInfo; private final ResTable mResTable; private final Map mResFileMapping; + private boolean mIncludeAuxiliaryPublicXml; public ResourcesDecoder(Config config, ApkInfo apkInfo) { mConfig = config; @@ -70,6 +72,18 @@ public void loadMainPkg() throws AndrolibException { mResTable.loadMainPkg(mApkInfo.getApkFile()); } + public void loadAuxiliaryPkg(ExtFile apkFile) throws AndrolibException { + mResTable.loadAuxiliaryPkg(apkFile); + } + + public void loadAuxiliaryPkgs(Collection apkFiles) throws AndrolibException { + mResTable.loadAuxiliaryPkgs(apkFiles); + } + + public void setIncludeAuxiliaryPublicXml(boolean includeAuxiliaryPublicXml) { + mIncludeAuxiliaryPublicXml = includeAuxiliaryPublicXml; + } + public void decodeManifest(File apkDir) throws AndrolibException { if (!mApkInfo.hasManifest()) { return; @@ -157,11 +171,15 @@ public void decodeResources(File apkDir) throws AndrolibException { return; } - mResTable.loadMainPkg(mApkInfo.getApkFile()); + loadMainPkg(); ResStreamDecoderContainer decoders = new ResStreamDecoderContainer(); decoders.setDecoder("raw", new ResRawStreamDecoder()); - decoders.setDecoder("9patch", new Res9patchStreamDecoder()); + + decoders.setDecoder( + "9patch", + OSDetection.isAndroid() ? new Res9patchAndroidStreamDecoder() : new Res9patchStreamDecoder() + ); AXmlResourceParser axmlParser = new AXmlResourceParser(mResTable); XmlSerializer xmlSerializer = newXmlSerializer(); @@ -188,7 +206,7 @@ public void decodeResources(File apkDir) throws AndrolibException { generateValuesFile(valuesFile, outDir, xmlSerializer); } - generatePublicXml(pkg, outDir, xmlSerializer); + generatePublicXml(pkg, outDir, xmlSerializer, mIncludeAuxiliaryPublicXml); } AndrolibException decodeError = axmlParser.getFirstError(); @@ -197,7 +215,7 @@ public void decodeResources(File apkDir) throws AndrolibException { } } - private XmlSerializer newXmlSerializer() throws AndrolibException { + public XmlSerializer newXmlSerializer() throws AndrolibException { try { XmlSerializer serial = new MXSerializer(); serial.setFeature(MXSerializer.FEATURE_ATTR_VALUE_NO_ESCAPE, true); @@ -232,14 +250,14 @@ private void generateValuesFile(ResValuesFile valuesFile, Directory resDir, XmlS } } - private void generatePublicXml(ResPackage pkg, Directory resDir, XmlSerializer serial) + private void generatePublicXml(ResPackage pkg, Directory resDir, XmlSerializer serial, boolean includeAuxiliary) throws AndrolibException { try (OutputStream out = resDir.getFileOutput("values/public.xml")) { serial.setOutput(out, null); serial.startDocument(null, null); serial.startTag(null, "resources"); - for (ResResSpec spec : pkg.listResSpecs()) { + for (ResResSpec spec : mResTable.listResSpecs(pkg, includeAuxiliary)) { serial.startTag(null, "public"); serial.attribute(null, "type", spec.getType().getName()); serial.attribute(null, "name", spec.getName()); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java index c2c51dda8c..60cba27258 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java @@ -42,6 +42,8 @@ public class ResTable { private final ApkInfo mApkInfo; private final Map mPackagesById; private final Map mPackagesByName; + private final Map mAuxiliaryResSpecs; + private final Map mAuxiliaryResSpecsByName; private final Set mMainPackages; private final Set mFramePackages; @@ -63,6 +65,8 @@ public ResTable(Config config, ApkInfo apkInfo) { mApkInfo = apkInfo; mPackagesById = new HashMap<>(); mPackagesByName = new HashMap<>(); + mAuxiliaryResSpecs = new LinkedHashMap<>(); + mAuxiliaryResSpecsByName = new LinkedHashMap<>(); mMainPackages = new LinkedHashSet<>(); mFramePackages = new LinkedHashSet<>(); } @@ -91,6 +95,16 @@ public ResResSpec getResSpec(int resID) throws AndrolibException { } public ResResSpec getResSpec(ResID resID) throws AndrolibException { + ResPackage pkg = mPackagesById.get(resID.pkgId); + if (pkg != null && pkg.hasResSpec(resID)) { + return pkg.getResSpec(resID); + } + + ResResSpec auxiliarySpec = mAuxiliaryResSpecs.get(resID); + if (auxiliarySpec != null) { + return auxiliarySpec; + } + return getPackage(resID.pkgId).getResSpec(resID); } @@ -131,29 +145,27 @@ private ResPackage selectPkgWithMostResSpecs(ResPackage[] pkgs) { } public void loadMainPkg(ExtFile apkFile) throws AndrolibException { - LOGGER.info("Loading resource table..."); - ResPackage[] pkgs = loadResPackagesFromApk(apkFile, mConfig.keepBrokenResources); - ResPackage pkg; - - switch (pkgs.length) { - case 0: - pkg = new ResPackage(this, 0, null); - break; - case 1: - pkg = pkgs[0]; - break; - case 2: - LOGGER.warning("Skipping package group: " + pkgs[0].getName()); - pkg = pkgs[1]; - break; - default: - pkg = selectPkgWithMostResSpecs(pkgs); - break; + if (mMainPkgLoaded) { + return; } + + LOGGER.info("Loading resource table..."); + ResPackage pkg = selectPkg(loadResPackagesFromApk(apkFile, mConfig.keepBrokenResources)); addPackage(pkg, true); mMainPkgLoaded = true; } + public void loadAuxiliaryPkg(ExtFile apkFile) throws AndrolibException { + LOGGER.info("Loading auxiliary resource table from file: " + apkFile); + addAuxiliaryPackage(selectPkg(loadResPackagesFromApk(apkFile, mConfig.keepBrokenResources))); + } + + public void loadAuxiliaryPkgs(Collection apkFiles) throws AndrolibException { + for (ExtFile apkFile : apkFiles) { + loadAuxiliaryPkg(apkFile); + } + } + private ResPackage loadFrameworkPkg(int id) throws AndrolibException { Framework framework = new Framework(mConfig); File frameworkApk = framework.getFrameworkApk(id, mConfig.frameworkTag); @@ -224,7 +236,32 @@ public ResPackage getPackage(String name) throws AndrolibException { } public ResValue getValue(String pkg, String type, String name) throws AndrolibException { - return getPackage(pkg).getType(type).getResSpec(name).getDefaultResource().getValue(); + try { + return getPackage(pkg).getType(type).getResSpec(name).getDefaultResource().getValue(); + } catch (UndefinedResObjectException ex) { + ResResSpec auxiliarySpec = mAuxiliaryResSpecsByName.get(getSpecNameKey(pkg, type, name)); + if (auxiliarySpec == null) { + throw ex; + } + return auxiliarySpec.getDefaultResource().getValue(); + } + } + + public Collection listResSpecs(ResPackage pkg, boolean includeAuxiliary) { + if (!includeAuxiliary) { + return pkg.listResSpecs(); + } + + Map specs = new LinkedHashMap<>(); + for (ResResSpec spec : pkg.listResSpecs()) { + specs.put(spec.getId(), spec); + } + for (ResResSpec spec : mAuxiliaryResSpecs.values()) { + if (spec.getId().pkgId == pkg.getId()) { + specs.putIfAbsent(spec.getId(), spec); + } + } + return specs.values(); } public void addPackage(ResPackage pkg, boolean main) throws AndrolibException { @@ -246,6 +283,12 @@ public void addPackage(ResPackage pkg, boolean main) throws AndrolibException { } } + void addAuxiliaryPackage(ResPackage pkg) { + for (ResResSpec spec : pkg.listResSpecs()) { + addAuxiliaryResSpec(spec); + } + } + public void setPackageRenamed(String pkg) { mPackageRenamed = pkg; } @@ -381,4 +424,44 @@ private void loadVersionName(File apkDir) { mApkInfo.versionInfo.versionName = refValue; } } + + private ResPackage selectPkg(ResPackage[] pkgs) { + switch (pkgs.length) { + case 0: + return new ResPackage(this, 0, null); + case 1: + return pkgs[0]; + case 2: + LOGGER.warning("Skipping package group: " + pkgs[0].getName()); + return pkgs[1]; + default: + return selectPkgWithMostResSpecs(pkgs); + } + } + + private void addAuxiliaryResSpec(ResResSpec spec) { + ResResSpec existingSpec = mAuxiliaryResSpecs.get(spec.getId()); + if (existingSpec != null) { + if (!existingSpec.getName().equals(spec.getName()) + || !existingSpec.getType().getName().equals(spec.getType().getName())) { + LOGGER.warning("Ignoring conflicting auxiliary resource spec: " + spec); + } + return; + } + + mAuxiliaryResSpecs.put(spec.getId(), spec); + + String key = getSpecNameKey(spec.getPackage().getName(), spec.getType().getName(), spec.getName()); + ResResSpec existingNamedSpec = mAuxiliaryResSpecsByName.get(key); + if (existingNamedSpec != null && !existingNamedSpec.getId().equals(spec.getId())) { + LOGGER.warning("Ignoring conflicting auxiliary resource name: " + key); + return; + } + + mAuxiliaryResSpecsByName.putIfAbsent(key, spec); + } + + private String getSpecNameKey(String pkg, String type, String name) { + return pkg + ":" + type + "/" + name; + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java new file mode 100644 index 0000000000..794cafad27 --- /dev/null +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java @@ -0,0 +1,136 @@ +package brut.androlib.res.decoder; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import brut.androlib.exceptions.AndrolibException; +import brut.androlib.exceptions.CantFind9PatchChunkException; +import brut.androlib.res.data.ninepatch.NinePatchData; +import brut.androlib.res.data.ninepatch.OpticalInset; +import brut.util.ExtDataInput; +import brut.util.ExtDataInputStream; +import org.apache.commons.io.IOUtils; + +import java.io.*; + +public class Res9patchAndroidStreamDecoder implements ResStreamDecoder { + public void decode(InputStream in, OutputStream out) throws AndrolibException { + try { + byte[] data = IOUtils.toByteArray(in); + + if (data.length == 0) { + return; + } + Bitmap bm = BitmapFactory.decodeByteArray(data, 0, data.length); + int width = bm.getWidth(), height = bm.getHeight(); + + Bitmap outImg = Bitmap.createBitmap(width + 2, height + 2, bm.getConfig()); + + for (int w = 0; w < width; w++) + for (int h = 0; h < height; h++) outImg.setPixel(w + 1, h + 1, bm.getPixel(w, h)); + + NinePatchData np = getNinePatch(data); + drawHLineA(outImg, height + 1, np.padLeft + 1, width - np.padRight); + drawVLineA(outImg, width + 1, np.padTop + 1, height - np.padBottom); + + int[] xDivs = np.xDivs; + if (xDivs.length == 0) { + drawHLineA(outImg, 0, 1, width); + } else { + for (int i = 0; i < xDivs.length; i += 2) { + drawHLineA(outImg, 0, xDivs[i] + 1, xDivs[i + 1]); + } + } + + int[] yDivs = np.yDivs; + if (yDivs.length == 0) { + drawVLineA(outImg, 0, 1, height); + } else { + for (int i = 0; i < yDivs.length; i += 2) { + drawVLineA(outImg, 0, yDivs[i] + 1, yDivs[i + 1]); + } + } + + // Some images additionally use Optical Bounds + // https://developer.android.com/about/versions/android-4.3.html#OpticalBounds + try { + OpticalInset oi = getOpticalInset(data); + + for (int i = 0; i < oi.layoutBoundsLeft; i++) { + int x = 1 + i; + outImg.setPixel(x, height + 1, OI_COLOR); + } + + for (int i = 0; i < oi.layoutBoundsRight; i++) { + int x = width - i; + outImg.setPixel(x, height + 1, OI_COLOR); + } + + for (int i = 0; i < oi.layoutBoundsTop; i++) { + int y = 1 + i; + outImg.setPixel(width + 1, y, OI_COLOR); + } + + for (int i = 0; i < oi.layoutBoundsBottom; i++) { + int y = height - i; + outImg.setPixel(width + 1, y, OI_COLOR); + } + } catch (CantFind9PatchChunkException t) { + // This chunk might not exist + } + + outImg.compress(Bitmap.CompressFormat.PNG, 100, out); + bm.recycle(); + outImg.recycle(); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + } + + private NinePatchData getNinePatch(byte[] data) throws AndrolibException, + IOException { + ExtDataInput di = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data)); + find9patchChunk(di, NP_CHUNK_TYPE); + return NinePatchData.decode(di); + } + + private OpticalInset getOpticalInset(byte[] data) throws AndrolibException, + IOException { + ExtDataInput di = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data)); + find9patchChunk(di, OI_CHUNK_TYPE); + return OpticalInset.decode(di); + } + + private void find9patchChunk(DataInput di, int magic) throws AndrolibException, + IOException { + di.skipBytes(8); + while (true) { + int size; + try { + size = di.readInt(); + } catch (IOException ex) { + throw new CantFind9PatchChunkException("Cant find nine patch chunk", ex); + } + if (di.readInt() == magic) { + return; + } + di.skipBytes(size + 4); + } + } + + private void drawHLineA(Bitmap bm, int y, int x1, int x2) { + for (int x = x1; x <= x2; x++) { + bm.setPixel(x, y, NP_COLOR); + } + } + + private void drawVLineA(Bitmap bm, int x, int y1, int y2) { + for (int y = y1; y <= y2; y++) { + bm.setPixel(x, y, NP_COLOR); + } + } + + private static final int NP_CHUNK_TYPE = 0x6e705463; // npTc + private static final int OI_CHUNK_TYPE = 0x6e704c62; // npLb + private static final int NP_COLOR = 0xff000000; + private static final int OI_COLOR = 0xffff0000; +} diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java index 4cb1a5b0ef..93525f5901 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java @@ -18,10 +18,13 @@ import brut.androlib.BaseTest; import brut.androlib.TestUtils; +import brut.androlib.res.decoder.Res9patchAndroidStreamDecoder; import brut.androlib.res.decoder.Res9patchStreamDecoder; +import brut.androlib.res.decoder.ResStreamDecoder; import brut.common.BrutException; import brut.directory.ExtFile; import brut.util.OS; +import brut.util.OSDetection; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -54,7 +57,7 @@ public void assertMissingDivAdded() throws Exception { byte[] data; try (InputStream in = Files.newInputStream(file.toPath())) { - Res9patchStreamDecoder decoder = new Res9patchStreamDecoder(); + ResStreamDecoder decoder = OSDetection.isAndroid() ? new Res9patchAndroidStreamDecoder() : new Res9patchStreamDecoder(); ByteArrayOutputStream out = new ByteArrayOutputStream(); decoder.decode(in, out); data = out.toByteArray(); diff --git a/brut.j.util/src/main/java/brut/util/OSDetection.java b/brut.j.util/src/main/java/brut/util/OSDetection.java index 1c27b616c3..57bf79b817 100644 --- a/brut.j.util/src/main/java/brut/util/OSDetection.java +++ b/brut.j.util/src/main/java/brut/util/OSDetection.java @@ -18,7 +18,7 @@ public final class OSDetection { private static final String OS = System.getProperty("os.name").toLowerCase(); - private static final String BIT = System.getProperty("sun.arch.data.model").toLowerCase(); + private static final String BIT = System.getProperty("sun.arch.data.model"); private OSDetection() { // Private constructor for utility class @@ -36,6 +36,15 @@ public static boolean isUnix() { return (OS.contains("nix") || OS.contains("nux") || OS.contains("aix") || (OS.contains("sunos"))); } + public static boolean isAndroid() { + try { + Class.forName("android.app.Activity"); + return true; + } catch (ClassNotFoundException ignored) { + return false; + } + } + public static boolean is64Bit() { if (isWindows()) { String arch = System.getenv("PROCESSOR_ARCHITECTURE"); @@ -43,7 +52,7 @@ public static boolean is64Bit() { return arch != null && arch.endsWith("64") || wow64Arch != null && wow64Arch.endsWith("64"); } - return BIT.equals("64"); + return BIT != null && BIT.equalsIgnoreCase("64"); } public static String returnOS() { diff --git a/build.gradle.kts b/build.gradle.kts index 44d7163790..60b3f21e08 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import java.io.ByteArrayOutputStream -val version = "2.10.1" -val suffix = "SNAPSHOT" +val version = "2.10.1.1" +val suffix = "" // Strings embedded into the build. var gitRevision by extra("") @@ -97,21 +97,17 @@ subprojects { publishing { repositories { maven { - url = if (suffix.contains("SNAPSHOT")) { - uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") - } else { - uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") - } + url = uri("https://maven.pkg.github.com/revanced/Apktool") credentials { - username = (project.properties["ossrhUsername"] ?: "").toString() - password = (project.properties["ossrhPassword"] ?: "").toString() + username = System.getenv("GITHUB_ACTOR") ?: project.findProperty("gpr.user").toString() + password = System.getenv("GITHUB_TOKEN") ?: project.findProperty("gpr.key").toString() } } } publications { register("mavenJava", MavenPublication::class) { from(components["java"]) - groupId = "org.apktool" + groupId = "app.revanced" artifactId = project.name version = apktoolVersion @@ -139,9 +135,9 @@ subprojects { } } scm { - connection = "scm:git:git://github.com/iBotPeaches/Apktool.git" - developerConnection = "scm:git:git@github.com:iBotPeaches/Apktool.git" - url = "https://github.com/iBotPeaches/Apktool" + connection = "scm:git:git://github.com/revanced/Apktool.git" + developerConnection = "scm:git:git@github.com:revanced/Apktool.git" + url = "https://github.com/revanced/Apktool" } } } @@ -153,6 +149,7 @@ subprojects { } signing { + useGpgCmd() sign(publishing.publications["mavenJava"]) } }