From 5c99919d94cb8284f4aa86853ab327429053e82d Mon Sep 17 00:00:00 2001 From: Igor Eisberg <8811086+IgorEisberg@users.noreply.github.com> Date: Fri, 4 Oct 2024 00:10:02 +0300 Subject: [PATCH] new: featureFlags support for SDK 35 apps (#3706) * new: featureFlags support for SDK 35 apps This records all featureFlag attrs that were enabled when the APK was originally built. This is now required by AAPT2 to pass these flags and their enabled/disabled state if they are used in AndroidManifest.xml. The flags are recorded to apktool.yml and can be configured, if so desired. In normal usage, all flags should remain set to true (i.e. enabled). Sample APK sourced from AOSP Android 15. https://drive.google.com/file/d/1av7Ih7-YUXi73Hf0E3xlPv-V-nE_sXdt/view * test: adapt testapp for featureFlag --- .../main/java/brut/androlib/AaptInvoker.java | 9 +++++ .../main/java/brut/androlib/ApkDecoder.java | 10 +++++ .../main/java/brut/androlib/apk/ApkInfo.java | 17 ++++++++- .../java/brut/androlib/apk/YamlReader.java | 11 +++++- .../java/brut/androlib/apk/YamlWriter.java | 4 +- .../brut/androlib/res/xml/ResXmlPatcher.java | 37 +++++++++++++++++++ .../aapt2/testapp/AndroidManifest.xml | 1 + .../test/resources/aapt2/testapp/apktool.yml | 2 + 8 files changed, 86 insertions(+), 5 deletions(-) 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 675265ed..a249869b 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 @@ -189,6 +189,15 @@ public class AaptInvoker { cmd.add("-x"); } + if (!mApkInfo.featureFlags.isEmpty()) { + List featureFlags = new ArrayList<>(); + for (Map.Entry entry : mApkInfo.featureFlags.entrySet()) { + featureFlags.add(entry.getKey() + "=" + entry.getValue()); + } + cmd.add("--feature-flags"); + cmd.add(String.join(",", featureFlags)); + } + if (include != null) { for (File file : include) { cmd.add("-I"); 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 b0e84378..c73eeb05 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 @@ -21,6 +21,7 @@ import brut.androlib.exceptions.InFileNotFoundException; import brut.androlib.exceptions.OutDirExistsException; import brut.androlib.apk.ApkInfo; import brut.androlib.res.ResourcesDecoder; +import brut.androlib.res.xml.ResXmlPatcher; import brut.androlib.src.SmaliDecoder; import brut.directory.Directory; import brut.directory.ExtFile; @@ -321,6 +322,15 @@ public class ApkDecoder { mApkInfo.setMinSdkVersion(Integer.toString(mMinSdkVersion)); } + // record feature flags + File manifest = new File(outDir, "AndroidManifest.xml"); + List featureFlags = ResXmlPatcher.pullManifestFeatureFlags(manifest); + if (featureFlags != null) { + for (String flag : featureFlags) { + mApkInfo.addFeatureFlag(flag, true); + } + } + // record uncompressed files try { Map resFileMapping = mResDecoder.getResFileMapping(); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java index 6b6991d8..38a036d5 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java @@ -48,6 +48,7 @@ public class ApkInfo implements YamlSerializable { public Map sdkInfo = new LinkedHashMap<>(); public PackageInfo packageInfo = new PackageInfo(); public VersionInfo versionInfo = new VersionInfo(); + public Map featureFlags = new LinkedHashMap<>(); public boolean sharedLibrary; public boolean sparseResources; public List doNotCompress = new ArrayList<>(); @@ -185,6 +186,10 @@ public class ApkInfo implements YamlSerializable { } } + public void addFeatureFlag(String flag, boolean value) { + featureFlags.put(flag, value); + } + public void save(File file) throws AndrolibException { try (YamlWriter writer = new YamlWriter(new FileOutputStream(file))) { write(writer); @@ -235,7 +240,7 @@ public class ApkInfo implements YamlSerializable { } case "sdkInfo": { sdkInfo.clear(); - reader.readMap(sdkInfo); + reader.readStringMap(sdkInfo); break; } case "packageInfo": { @@ -248,6 +253,11 @@ public class ApkInfo implements YamlSerializable { reader.readObject(versionInfo); break; } + case "featureFlags": { + featureFlags.clear(); + reader.readBoolMap(featureFlags); + break; + } case "sharedLibrary": { sharedLibrary = line.getValueBool(); break; @@ -270,9 +280,12 @@ public class ApkInfo implements YamlSerializable { writer.writeString("apkFileName", apkFileName); writer.writeBool("isFrameworkApk", isFrameworkApk); writer.writeObject("usesFramework", usesFramework); - writer.writeStringMap("sdkInfo", sdkInfo); + writer.writeMap("sdkInfo", sdkInfo); writer.writeObject("packageInfo", packageInfo); writer.writeObject("versionInfo", versionInfo); + if (!featureFlags.isEmpty()) { + writer.writeMap("featureFlags", featureFlags); + } writer.writeBool("sharedLibrary", sharedLibrary); writer.writeBool("sparseResources", sparseResources); if (!doNotCompress.isEmpty()) { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java index 75b1017f..85569af2 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java @@ -203,7 +203,7 @@ public class YamlReader { readList(list, (items, reader) -> items.add(reader.getLine().getValueInt())); } - public void readMap(Map map) throws AndrolibException { + public void readStringMap(Map map) throws AndrolibException { readObject(map, line -> line.hasColon, (items, reader) -> { @@ -211,4 +211,13 @@ public class YamlReader { items.put(line.getKey(), line.getValue()); }); } + + public void readBoolMap(Map map) throws AndrolibException { + readObject(map, + line -> line.hasColon, + (items, reader) -> { + YamlLine line = reader.getLine(); + items.put(line.getKey(), line.getValueBool()); + }); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java index 1e4e76ae..1c37051d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java @@ -95,7 +95,7 @@ public class YamlWriter implements Closeable { } } - public void writeStringMap(String key, Map map) { + public void writeMap(String key, Map map) { if (Objects.isNull(map)) { return; } @@ -103,7 +103,7 @@ public class YamlWriter implements Closeable { mWriter.println(escape(key) + ":"); nextIndent(); for (String mapKey : map.keySet()) { - writeString(mapKey, map.get(mapKey)); + writeString(mapKey, String.valueOf(map.get(mapKey))); } prevIndent(); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java index 80a049ae..5f236c39 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java @@ -30,6 +30,7 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.*; import java.io.*; +import java.util.*; import java.util.logging.Logger; public final class ResXmlPatcher { @@ -357,6 +358,42 @@ public final class ResXmlPatcher { } } + /** + * Finds all feature flags set on permissions in AndroidManifest.xml. + * + * @param file File for AndroidManifest.xml + */ + public static List pullManifestFeatureFlags(File file) { + if (!file.exists()) { + return null; + } + try { + Document doc = loadDocument(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression expression = xPath.compile("/manifest/permission"); + + Object result = expression.evaluate(doc, XPathConstants.NODESET); + NodeList nodes = (NodeList) result; + + List featureFlags = new ArrayList<>(); + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node featureFlagAttr = attrs.getNamedItem("android:featureFlag"); + + if (featureFlagAttr != null) { + featureFlags.add(featureFlagAttr.getNodeValue()); + } + } + + return featureFlags; + + } catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) { + return null; + } + } + /** * * @param file File to load into Document diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml index 68f742cf..7a6c57e5 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml @@ -11,4 +11,5 @@ + diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml index f11e206f..4da39874 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml @@ -9,6 +9,8 @@ packageInfo: versionInfo: versionCode: '1' versionName: '1.0' +featureFlags: + brut.feature.flag: true doNotCompress: - assets/0byte_file.jpg sparseResources: false