new: featureFlags support for SDK 35 apps (#3706)
Some checks are pending
Analyze / Analyze (push) Waiting to run
CI / analyze-mac-aapt (aapt2_64) (push) Waiting to run
CI / analyze-mac-aapt (aapt_64) (push) Waiting to run
CI / analyze-linux-aapt (aapt) (push) Waiting to run
CI / analyze-linux-aapt (aapt2) (push) Waiting to run
CI / analyze-linux-aapt (aapt2_64) (push) Waiting to run
CI / analyze-linux-aapt (aapt_64) (push) Waiting to run
CI / analyze-windows-aapt (aapt.exe) (push) Waiting to run
CI / analyze-windows-aapt (aapt2.exe) (push) Waiting to run
CI / analyze-windows-aapt (aapt2_64.exe) (push) Waiting to run
CI / analyze-windows-aapt (aapt_64.exe) (push) Waiting to run
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (11, macOS-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (11, ubuntu-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (11, windows-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (17, macOS-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (17, ubuntu-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (17, windows-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (21, macOS-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (21, ubuntu-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (21, windows-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (8, macOS-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (8, ubuntu-latest) (push) Blocked by required conditions
CI / Build/Test (JDK ${{ matrix.java }}, ${{ matrix.os }}) (8, windows-latest) (push) Blocked by required conditions
CI / Build apktool.jar (push) Blocked by required conditions

* 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
This commit is contained in:
Igor Eisberg 2024-10-04 00:10:02 +03:00 committed by GitHub
parent 03a7c67082
commit 5c99919d94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 86 additions and 5 deletions

View File

@ -189,6 +189,15 @@ public class AaptInvoker {
cmd.add("-x"); cmd.add("-x");
} }
if (!mApkInfo.featureFlags.isEmpty()) {
List<String> featureFlags = new ArrayList<>();
for (Map.Entry<String, Boolean> entry : mApkInfo.featureFlags.entrySet()) {
featureFlags.add(entry.getKey() + "=" + entry.getValue());
}
cmd.add("--feature-flags");
cmd.add(String.join(",", featureFlags));
}
if (include != null) { if (include != null) {
for (File file : include) { for (File file : include) {
cmd.add("-I"); cmd.add("-I");

View File

@ -21,6 +21,7 @@ import brut.androlib.exceptions.InFileNotFoundException;
import brut.androlib.exceptions.OutDirExistsException; import brut.androlib.exceptions.OutDirExistsException;
import brut.androlib.apk.ApkInfo; import brut.androlib.apk.ApkInfo;
import brut.androlib.res.ResourcesDecoder; import brut.androlib.res.ResourcesDecoder;
import brut.androlib.res.xml.ResXmlPatcher;
import brut.androlib.src.SmaliDecoder; import brut.androlib.src.SmaliDecoder;
import brut.directory.Directory; import brut.directory.Directory;
import brut.directory.ExtFile; import brut.directory.ExtFile;
@ -321,6 +322,15 @@ public class ApkDecoder {
mApkInfo.setMinSdkVersion(Integer.toString(mMinSdkVersion)); mApkInfo.setMinSdkVersion(Integer.toString(mMinSdkVersion));
} }
// record feature flags
File manifest = new File(outDir, "AndroidManifest.xml");
List<String> featureFlags = ResXmlPatcher.pullManifestFeatureFlags(manifest);
if (featureFlags != null) {
for (String flag : featureFlags) {
mApkInfo.addFeatureFlag(flag, true);
}
}
// record uncompressed files // record uncompressed files
try { try {
Map<String, String> resFileMapping = mResDecoder.getResFileMapping(); Map<String, String> resFileMapping = mResDecoder.getResFileMapping();

View File

@ -48,6 +48,7 @@ public class ApkInfo implements YamlSerializable {
public Map<String, String> sdkInfo = new LinkedHashMap<>(); public Map<String, String> sdkInfo = new LinkedHashMap<>();
public PackageInfo packageInfo = new PackageInfo(); public PackageInfo packageInfo = new PackageInfo();
public VersionInfo versionInfo = new VersionInfo(); public VersionInfo versionInfo = new VersionInfo();
public Map<String, Boolean> featureFlags = new LinkedHashMap<>();
public boolean sharedLibrary; public boolean sharedLibrary;
public boolean sparseResources; public boolean sparseResources;
public List<String> doNotCompress = new ArrayList<>(); public List<String> 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 { public void save(File file) throws AndrolibException {
try (YamlWriter writer = new YamlWriter(new FileOutputStream(file))) { try (YamlWriter writer = new YamlWriter(new FileOutputStream(file))) {
write(writer); write(writer);
@ -235,7 +240,7 @@ public class ApkInfo implements YamlSerializable {
} }
case "sdkInfo": { case "sdkInfo": {
sdkInfo.clear(); sdkInfo.clear();
reader.readMap(sdkInfo); reader.readStringMap(sdkInfo);
break; break;
} }
case "packageInfo": { case "packageInfo": {
@ -248,6 +253,11 @@ public class ApkInfo implements YamlSerializable {
reader.readObject(versionInfo); reader.readObject(versionInfo);
break; break;
} }
case "featureFlags": {
featureFlags.clear();
reader.readBoolMap(featureFlags);
break;
}
case "sharedLibrary": { case "sharedLibrary": {
sharedLibrary = line.getValueBool(); sharedLibrary = line.getValueBool();
break; break;
@ -270,9 +280,12 @@ public class ApkInfo implements YamlSerializable {
writer.writeString("apkFileName", apkFileName); writer.writeString("apkFileName", apkFileName);
writer.writeBool("isFrameworkApk", isFrameworkApk); writer.writeBool("isFrameworkApk", isFrameworkApk);
writer.writeObject("usesFramework", usesFramework); writer.writeObject("usesFramework", usesFramework);
writer.writeStringMap("sdkInfo", sdkInfo); writer.writeMap("sdkInfo", sdkInfo);
writer.writeObject("packageInfo", packageInfo); writer.writeObject("packageInfo", packageInfo);
writer.writeObject("versionInfo", versionInfo); writer.writeObject("versionInfo", versionInfo);
if (!featureFlags.isEmpty()) {
writer.writeMap("featureFlags", featureFlags);
}
writer.writeBool("sharedLibrary", sharedLibrary); writer.writeBool("sharedLibrary", sharedLibrary);
writer.writeBool("sparseResources", sparseResources); writer.writeBool("sparseResources", sparseResources);
if (!doNotCompress.isEmpty()) { if (!doNotCompress.isEmpty()) {

View File

@ -203,7 +203,7 @@ public class YamlReader {
readList(list, (items, reader) -> items.add(reader.getLine().getValueInt())); readList(list, (items, reader) -> items.add(reader.getLine().getValueInt()));
} }
public void readMap(Map<String, String> map) throws AndrolibException { public void readStringMap(Map<String, String> map) throws AndrolibException {
readObject(map, readObject(map,
line -> line.hasColon, line -> line.hasColon,
(items, reader) -> { (items, reader) -> {
@ -211,4 +211,13 @@ public class YamlReader {
items.put(line.getKey(), line.getValue()); items.put(line.getKey(), line.getValue());
}); });
} }
public void readBoolMap(Map<String, Boolean> map) throws AndrolibException {
readObject(map,
line -> line.hasColon,
(items, reader) -> {
YamlLine line = reader.getLine();
items.put(line.getKey(), line.getValueBool());
});
}
} }

View File

@ -95,7 +95,7 @@ public class YamlWriter implements Closeable {
} }
} }
public void writeStringMap(String key, Map<String, String> map) { public <T> void writeMap(String key, Map<String, T> map) {
if (Objects.isNull(map)) { if (Objects.isNull(map)) {
return; return;
} }
@ -103,7 +103,7 @@ public class YamlWriter implements Closeable {
mWriter.println(escape(key) + ":"); mWriter.println(escape(key) + ":");
nextIndent(); nextIndent();
for (String mapKey : map.keySet()) { for (String mapKey : map.keySet()) {
writeString(mapKey, map.get(mapKey)); writeString(mapKey, String.valueOf(map.get(mapKey)));
} }
prevIndent(); prevIndent();
} }

View File

@ -30,6 +30,7 @@ import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*; import javax.xml.xpath.*;
import java.io.*; import java.io.*;
import java.util.*;
import java.util.logging.Logger; import java.util.logging.Logger;
public final class ResXmlPatcher { 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<String> 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<String> 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 * @param file File to load into Document

View File

@ -11,4 +11,5 @@
<meta-data name="test_int" value="12345" /> <meta-data name="test_int" value="12345" />
</application> </application>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<permission android:featureFlag="brut.feature.flag" android:label="Test Permission" android:name="brut.permission.TEST" android:permissionGroup="android.permission-group.UNDEFINED" android:protectionLevel="signature"/>
</manifest> </manifest>

View File

@ -9,6 +9,8 @@ packageInfo:
versionInfo: versionInfo:
versionCode: '1' versionCode: '1'
versionName: '1.0' versionName: '1.0'
featureFlags:
brut.feature.flag: true
doNotCompress: doNotCompress:
- assets/0byte_file.jpg - assets/0byte_file.jpg
sparseResources: false sparseResources: false