diff options
13 files changed, 383 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp index d6b303f62428..af312bf833e5 100644 --- a/Android.bp +++ b/Android.bp @@ -425,6 +425,7 @@ java_defaults {          "sounddose-aidl-java",          "modules-utils-expresslog",          "perfetto_trace_javastream_protos_jarjar", +        "libaconfig_java_proto_nano",      ],  } diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java new file mode 100644 index 000000000000..f306b0b02677 --- /dev/null +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.pm.pkg.component; + +import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE; + +import android.aconfig.nano.Aconfig; +import android.aconfig.nano.Aconfig.parsed_flag; +import android.aconfig.nano.Aconfig.parsed_flags; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Flags; +import android.content.res.XmlResourceParser; +import android.os.Environment; +import android.os.Process; +import android.util.ArrayMap; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.modules.utils.TypedXmlPullParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A class that manages a cache of all device feature flags and their default + override values. + * This class performs a very similar job to the one in {@code SettingsProvider}, with an important + * difference: this is a part of system server and is available for the server startup. Package + * parsing happens at the startup when {@code SettingsProvider} isn't available yet, so we need an + * own copy of the code here. + * @hide + */ +public class AconfigFlags { +    private static final String LOG_TAG = "AconfigFlags"; + +    private static final List<String> sTextProtoFilesOnDevice = List.of( +            "/system/etc/aconfig_flags.pb", +            "/system_ext/etc/aconfig_flags.pb", +            "/product/etc/aconfig_flags.pb", +            "/vendor/etc/aconfig_flags.pb"); + +    private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>(); + +    public AconfigFlags() { +        if (!Flags.manifestFlagging()) { +            Slog.v(LOG_TAG, "Feature disabled, skipped all loading"); +            return; +        } +        for (String fileName : sTextProtoFilesOnDevice) { +            try (var inputStream = new FileInputStream(fileName)) { +                loadAconfigDefaultValues(inputStream.readAllBytes()); +            } catch (IOException e) { +                Slog.e(LOG_TAG, "Failed to read Aconfig values from " + fileName, e); +            } +        } +        if (Process.myUid() == Process.SYSTEM_UID) { +            // Server overrides are only accessible to the system, no need to even try loading them +            // in user processes. +            loadServerOverrides(); +        } +    } + +    private void loadServerOverrides() { +        // Reading the proto files is enough for READ_ONLY flags but if it's a READ_WRITE flag +        // (which you can check with `flag.getPermission() == flag_permission.READ_WRITE`) then we +        // also need to check if there is a value pushed from the server in the file +        // `/data/system/users/0/settings_config.xml`. It will be in a <setting> node under the +        // root <settings> node with "name" attribute == "flag_namespace/flag_package.flag_name". +        // The "value" attribute will be true or false. +        // +        // The "name" attribute could also be "<namespace>/flag_namespace?flag_package.flag_name" +        // (prefixed with "staged/" or "device_config_overrides/" and a different separator between +        // namespace and name). This happens when a flag value is overridden either with a pushed +        // one from the server, or from the local command. +        // When the device reboots during package parsing, the staged value will still be there and +        // only later it will become a regular/non-staged value after SettingsProvider is +        // initialized. +        // +        // In all cases, when there is more than one value, the priority is: +        //      device_config_overrides > staged > default +        // + +        final var settingsFile = new File(Environment.getUserSystemDirectory(0), +                "settings_config.xml"); +        try (var inputStream = new FileInputStream(settingsFile)) { +            TypedXmlPullParser parser = Xml.resolvePullParser(inputStream); +            if (parser.next() != XmlPullParser.END_TAG && "settings".equals(parser.getName())) { +                final var flagPriority = new ArrayMap<String, Integer>(); +                final int outerDepth = parser.getDepth(); +                int type; +                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT +                        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { +                    if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { +                        continue; +                    } +                    if (!"setting".equals(parser.getName())) { +                        continue; +                    } +                    String name = parser.getAttributeValue(null, "name"); +                    final String value = parser.getAttributeValue(null, "value"); +                    if (name == null || value == null) { +                        continue; +                    } +                    // A non-boolean setting is definitely not an Aconfig flag value. +                    if (!"false".equalsIgnoreCase(value) && !"true".equalsIgnoreCase(value)) { +                        continue; +                    } +                    final var overridePrefix = "device_config_overrides/"; +                    final var stagedPrefix = "staged/"; +                    String separator = "/"; +                    String prefix = "default"; +                    int priority = 0; +                    if (name.startsWith(overridePrefix)) { +                        prefix = overridePrefix; +                        name = name.substring(overridePrefix.length()); +                        separator = ":"; +                        priority = 20; +                    } else if (name.startsWith(stagedPrefix)) { +                        prefix = stagedPrefix; +                        name = name.substring(stagedPrefix.length()); +                        separator = "*"; +                        priority = 10; +                    } +                    final String flagPackageAndName = parseFlagPackageAndName(name, separator); +                    if (flagPackageAndName == null) { +                        continue; +                    } +                    // We ignore all settings that aren't for flags. We'll know they are for flags +                    // if they correspond to flags read from the proto files. +                    if (!mFlagValues.containsKey(flagPackageAndName)) { +                        continue; +                    } +                    Slog.d(LOG_TAG, "Found " + prefix +                            + " Aconfig flag value for " + flagPackageAndName + " = " + value); +                    final Integer currentPriority = flagPriority.get(flagPackageAndName); +                    if (currentPriority != null && currentPriority >= priority) { +                        Slog.i(LOG_TAG, "Skipping " + prefix + " flag " + flagPackageAndName +                                + " because of the existing one with priority " + currentPriority); +                        continue; +                    } +                    flagPriority.put(flagPackageAndName, priority); +                    mFlagValues.put(flagPackageAndName, Boolean.parseBoolean(value)); +                } +            } +        } catch (IOException | XmlPullParserException e) { +            Slog.e(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e); +        } +    } + +    private static String parseFlagPackageAndName(String fullName, String separator) { +        int index = fullName.indexOf(separator); +        if (index < 0) { +            return null; +        } +        return fullName.substring(index + 1); +    } + +    private void loadAconfigDefaultValues(byte[] fileContents) throws IOException { +        parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); +        for (parsed_flag flag : parsedFlags.parsedFlag) { +            String flagPackageAndName = flag.package_ + "." + flag.name; +            boolean flagValue = (flag.state == Aconfig.ENABLED); +            Slog.v(LOG_TAG, "Read Aconfig default flag value " +                    + flagPackageAndName + " = " + flagValue); +            mFlagValues.put(flagPackageAndName, flagValue); +        } +    } + +    /** +     * Get the flag value, or null if the flag doesn't exist. +     * @param flagPackageAndName Full flag name formatted as 'package.flag' +     * @return the current value of the given Aconfig flag, or null if there is no such flag +     */ +    @Nullable +    public Boolean getFlagValue(@NonNull String flagPackageAndName) { +        Boolean value = mFlagValues.get(flagPackageAndName); +        Slog.d(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); +        return value; +    } + +    /** +     * Check if the element in {@code parser} should be skipped because of the feature flag. +     * @param parser XML parser object currently parsing an element +     * @return true if the element is disabled because of its feature flag +     */ +    public boolean skipCurrentElement(@NonNull XmlResourceParser parser) { +        if (!Flags.manifestFlagging()) { +            return false; +        } +        String featureFlag = parser.getAttributeValue(ANDROID_RES_NAMESPACE, "featureFlag"); +        if (featureFlag == null) { +            return false; +        } +        featureFlag = featureFlag.strip(); +        boolean negated = false; +        if (featureFlag.startsWith("!")) { +            negated = true; +            featureFlag = featureFlag.substring(1).strip(); +        } +        final Boolean flagValue = getFlagValue(featureFlag); +        if (flagValue == null) { +            Slog.w(LOG_TAG, "Skipping element " + parser.getName() +                    + " due to unknown feature flag " + featureFlag); +            return true; +        } +        // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated) +        if (flagValue == negated) { +            Slog.v(LOG_TAG, "Skipping element " + parser.getName() +                    + " behind feature flag " + featureFlag + " = " + flagValue); +            return true; +        } +        return false; +    } + +    /** +     * Add Aconfig flag values for testing flagging of manifest entries. +     * @param flagValues A map of flag name -> value. +     */ +    @VisibleForTesting +    public void addFlagValuesForTesting(@NonNull Map<String, Boolean> flagValues) { +        mFlagValues.putAll(flagValues); +    } +} diff --git a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java index db08005c833e..8858f9492890 100644 --- a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java @@ -61,6 +61,9 @@ public class ComponentParseUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                continue; +            }              final ParseResult result;              if ("meta-data".equals(parser.getName())) { diff --git a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java index 0b045919fb13..bb015812c225 100644 --- a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java +++ b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java @@ -27,6 +27,7 @@ import android.util.ArraySet;  import com.android.internal.R;  import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;  import org.xmlpull.v1.XmlPullParser;  import org.xmlpull.v1.XmlPullParserException; @@ -80,6 +81,9 @@ public class InstallConstraintsTagParser {                  }                  return input.success(prefixes);              } else if (type == XmlPullParser.START_TAG) { +                if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                    continue; +                }                  if (parser.getName().equals(TAG_FINGERPRINT_PREFIX)) {                      ParseResult<String> parsedPrefix =                              readFingerprintPrefixValue(input, res, parser); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java index 9f71d88c24bc..55baa532b434 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java @@ -393,6 +393,9 @@ public class ParsedActivityUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                continue; +            }              final ParseResult result;              if (parser.getName().equals("intent-filter")) { diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java index 05728eee174f..da48b23a2b81 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java @@ -99,6 +99,9 @@ public class ParsedIntentInfoUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                continue; +            }              final ParseResult result;              String nodeName = parser.getName(); @@ -197,6 +200,9 @@ public class ParsedIntentInfoUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                continue; +            }              final ParseResult result;              String nodeName = parser.getName(); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java index 12aff1c6669f..6af2a29822a2 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java @@ -36,6 +36,7 @@ import android.util.Slog;  import com.android.internal.R;  import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;  import com.android.internal.pm.pkg.parsing.ParsingUtils;  import org.xmlpull.v1.XmlPullParser; @@ -173,6 +174,9 @@ public class ParsedProviderUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                continue; +            }              String name = parser.getName();              final ParseResult result; diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java index 4ac542f84226..c68ea2dc8516 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -34,6 +34,7 @@ import android.os.Build;  import com.android.internal.R;  import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;  import com.android.internal.pm.pkg.parsing.ParsingUtils;  import org.xmlpull.v1.XmlPullParser; @@ -137,6 +138,9 @@ public class ParsedServiceUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { +                continue; +            }              final ParseResult parseResult;              switch (parser.getName()) { diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 1dcd893eb143..44fedb11b043 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -90,6 +90,7 @@ import com.android.internal.R;  import com.android.internal.os.ClassLoaderFactory;  import com.android.internal.pm.parsing.pkg.ParsedPackage;  import com.android.internal.pm.permission.CompatibilityPermissionInfo; +import com.android.internal.pm.pkg.component.AconfigFlags;  import com.android.internal.pm.pkg.component.ComponentMutateUtils;  import com.android.internal.pm.pkg.component.ComponentParseUtils;  import com.android.internal.pm.pkg.component.InstallConstraintsTagParser; @@ -292,6 +293,7 @@ public class ParsingPackageUtils {      @NonNull      private final List<PermissionManager.SplitPermissionInfo> mSplitPermissionInfos;      private final Callback mCallback; +    private static final AconfigFlags sAconfigFlags = new AconfigFlags();      public ParsingPackageUtils(String[] separateProcesses, DisplayMetrics displayMetrics,              @NonNull List<PermissionManager.SplitPermissionInfo> splitPermissions, @@ -761,6 +763,9 @@ public class ParsingPackageUtils {              if (outerDepth + 1 < parser.getDepth() || type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              final ParseResult result;              String tagName = parser.getName(); @@ -837,6 +842,9 @@ public class ParsingPackageUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              ParsedMainComponent mainComponent = null; @@ -980,6 +988,9 @@ public class ParsingPackageUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              String tagName = parser.getName();              final ParseResult result; @@ -1599,6 +1610,9 @@ public class ParsingPackageUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              final String innerTagName = parser.getName();              if (innerTagName.equals("uses-feature")) { @@ -1839,6 +1853,9 @@ public class ParsingPackageUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              if (parser.getName().equals("intent")) {                  ParseResult<ParsedIntentInfoImpl> result = ParsedIntentInfoUtils.parseIntentInfo(                          null /*className*/, pkg, res, parser, true /*allowGlobs*/, @@ -2185,6 +2202,9 @@ public class ParsingPackageUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              final ParseResult result;              String tagName = parser.getName(); @@ -2773,6 +2793,9 @@ public class ParsingPackageUtils {              if (type != XmlPullParser.START_TAG) {                  continue;              } +            if (sAconfigFlags.skipCurrentElement(parser)) { +                continue; +            }              final String nodeName = parser.getName();              if (nodeName.equals("additional-certificate")) { @@ -3458,4 +3481,11 @@ public class ParsingPackageUtils {          @NonNull Set<String> getInstallConstraintsAllowlist();      } + +    /** +     * Getter for the flags object +     */ +    public static AconfigFlags getAconfigFlags() { +        return sAconfigFlags; +    }  } diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp index ea7bb8b4a1d1..a738acb299c1 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -105,6 +105,7 @@ android_test {          ":PackageParserTestApp5",          ":PackageParserTestApp6",          ":PackageParserTestApp7", +        ":PackageParserTestApp8",      ],      resource_zips: [":PackageManagerServiceServerTests_apks_as_resources"], diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java index a0e0e1ef36ee..5da202f109d4 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java @@ -101,6 +101,7 @@ import com.android.internal.pm.pkg.component.ParsedServiceImpl;  import com.android.internal.pm.pkg.component.ParsedUsesPermission;  import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl;  import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;  import com.android.internal.util.ArrayUtils;  import com.android.server.pm.parsing.PackageCacher;  import com.android.server.pm.parsing.PackageInfoUtils; @@ -126,6 +127,7 @@ import java.util.ArrayList;  import java.util.Arrays;  import java.util.Collection;  import java.util.Collections; +import java.util.HashMap;  import java.util.HashSet;  import java.util.Iterator;  import java.util.List; @@ -154,6 +156,7 @@ public class PackageParserTest {      private static final String TEST_APP5_APK = "PackageParserTestApp5.apk";      private static final String TEST_APP6_APK = "PackageParserTestApp6.apk";      private static final String TEST_APP7_APK = "PackageParserTestApp7.apk"; +    private static final String TEST_APP8_APK = "PackageParserTestApp8.apk";      private static final String PACKAGE_NAME = "com.android.servicestests.apps.packageparserapp";      @Before @@ -814,6 +817,39 @@ public class PackageParserTest {          }      } +    @Test +    @RequiresFlagsEnabled(android.content.res.Flags.FLAG_MANIFEST_FLAGGING) +    public void testParseWithFeatureFlagAttributes() throws Exception { +        final File testFile = extractFile(TEST_APP8_APK); +        try (PackageParser2 parser = new TestPackageParser2()) { +            Map<String, Boolean> flagValues = new HashMap<>(); +            flagValues.put("my.flag1", true); +            flagValues.put("my.flag2", false); +            flagValues.put("my.flag3", false); +            flagValues.put("my.flag4", true); +            ParsingPackageUtils.getAconfigFlags().addFlagValuesForTesting(flagValues); + +            // The manifest has: +            //    <permission android:name="PERM1" android:featureFlag="my.flag1 " /> +            //    <permission android:name="PERM2" android:featureFlag=" !my.flag2" /> +            //    <permission android:name="PERM3" android:featureFlag="my.flag3" /> +            //    <permission android:name="PERM4" android:featureFlag="!my.flag4" /> +            //    <permission android:name="PERM5" android:featureFlag="unknown.flag" /> +            // Therefore with the above flag values, only PERM1 and PERM2 should be present. + +            final ParsedPackage pkg = parser.parsePackage(testFile, 0, false); +            List<String> permissionNames = +                    pkg.getPermissions().stream().map(ParsedComponent::getName).toList(); +            assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM1"); +            assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM2"); +            assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM3"); +            assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM4"); +            assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM5"); +        } finally { +            testFile.delete(); +        } +    } +      /**       * A subclass of package parser that adds a "cache_" prefix to the package name for the cached       * results. This is used by tests to tell if a ParsedPackage is generated from the cache or not. diff --git a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp index 131b380d9215..3def48aefa00 100644 --- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp +++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp @@ -116,3 +116,20 @@ android_test_helper_app {      resource_dirs: ["res"],      manifest: "AndroidManifestApp7.xml",  } + +android_test_helper_app { +    name: "PackageParserTestApp8", +    sdk_version: "current", +    srcs: ["**/*.java"], +    dex_preopt: { +        enabled: false, +    }, +    optimize: { +        enabled: false, +    }, +    resource_dirs: ["res"], +    aaptflags: [ +        "--feature-flags my.flag1,my.flag2,my.flag3,my.flag4,unknown.flag", +    ], +    manifest: "AndroidManifestApp8.xml", +} diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp8.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp8.xml new file mode 100644 index 000000000000..d489c1bb9e07 --- /dev/null +++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp8.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 The Android Open Source Project + +     Licensed under the Apache License, Version 2.0 (the "License"); +     you may not use this file except in compliance with the License. +     You may obtain a copy of the License at + +          http://www.apache.org/licenses/LICENSE-2.0 + +     Unless required by applicable law or agreed to in writing, software +     distributed under the License is distributed on an "AS IS" BASIS, +     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +     See the License for the specific language governing permissions and +     limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" +        package="com.android.servicestests.apps.packageparserapp" > + +    <application> +        <activity android:name=".TestActivity" +                  android:exported="true" /> +    </application> + +    <permission android:name="PERM1" android:featureFlag="my.flag1 " /> +    <permission android:name="PERM2" android:featureFlag=" !my.flag2" /> +    <permission android:name="PERM3" android:featureFlag="my.flag3" /> +    <permission android:name="PERM4" android:featureFlag="!my.flag4" /> +    <permission android:name="PERM5" android:featureFlag="unknown.flag" /> +</manifest>
\ No newline at end of file  |