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