Merge "Add key-value mappers for improved safety."
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfig.java b/services/core/java/com/android/server/signedconfig/SignedConfig.java
index a3f452c..e6bb800 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfig.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfig.java
@@ -48,7 +48,7 @@
     private static final String CONFIG_KEY_VALUE = "value";
 
     /**
-     * Represents config values targetting to an SDK range.
+     * Represents config values targeting an SDK range.
      */
     public static class PerSdkConfig {
         public final int minSdk;
@@ -90,11 +90,24 @@
     /**
      * Parse configuration from an APK.
      *
-     * @param config config as read from the APK metadata.
+     * @param config Config string as read from the APK metadata.
+     * @param allowedKeys Set of allowed keys in the config. Any key/value mapping for a key not in
+     *                    this set will result in an {@link InvalidConfigException} being thrown.
+     * @param keyValueMappers Mappings for values per key. The keys in the top level map should be
+     *                        a subset of {@code allowedKeys}. The keys in the inner map indicate
+     *                        the set of allowed values for that keys value. This map will be
+     *                        applied to the value in the configuration. This is intended to allow
+     *                        enum-like values to be encoded as strings in the configuration, and
+     *                        mapped back to integers when the configuration is parsed.
+     *
+     *                        <p>Any config key with a value that does not appear in the
+     *                        corresponding map will result in an {@link InvalidConfigException}
+     *                        being thrown.
      * @return Parsed configuration.
      * @throws InvalidConfigException If there's a problem parsing the config.
      */
-    public static SignedConfig parse(String config, Set<String> allowedKeys)
+    public static SignedConfig parse(String config, Set<String> allowedKeys,
+            Map<String, Map<String, String>> keyValueMappers)
             throws InvalidConfigException {
         try {
             JSONObject json = new JSONObject(config);
@@ -103,7 +116,8 @@
             JSONArray perSdkConfig = json.getJSONArray(KEY_CONFIG);
             List<PerSdkConfig> parsedConfigs = new ArrayList<>();
             for (int i = 0; i < perSdkConfig.length(); ++i) {
-                parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys));
+                parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys,
+                        keyValueMappers));
             }
 
             return new SignedConfig(version, parsedConfigs);
@@ -113,8 +127,17 @@
 
     }
 
+    private static CharSequence quoted(Object s) {
+        if (s == null) {
+            return "null";
+        } else {
+            return "\"" + s + "\"";
+        }
+    }
+
     @VisibleForTesting
-    static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys)
+    static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys,
+            Map<String, Map<String, String>> keyValueMappers)
             throws JSONException, InvalidConfigException {
         int minSdk = json.getInt(CONFIG_KEY_MIN_SDK);
         int maxSdk = json.getInt(CONFIG_KEY_MAX_SDK);
@@ -123,12 +146,23 @@
         for (int i = 0; i < valueArray.length(); ++i) {
             JSONObject keyValuePair = valueArray.getJSONObject(i);
             String key = keyValuePair.getString(CONFIG_KEY_KEY);
-            String value = keyValuePair.has(CONFIG_KEY_VALUE)
-                    ? keyValuePair.getString(CONFIG_KEY_VALUE)
+            Object valueObject = keyValuePair.has(CONFIG_KEY_VALUE)
+                    ? keyValuePair.get(CONFIG_KEY_VALUE)
                     : null;
+            String value = valueObject == JSONObject.NULL || valueObject == null
+                            ? null
+                            : valueObject.toString();
             if (!allowedKeys.contains(key)) {
                 throw new InvalidConfigException("Config key " + key + " is not allowed");
             }
+            if (keyValueMappers.containsKey(key)) {
+                Map<String, String> mapper = keyValueMappers.get(key);
+                if (!mapper.containsKey(value)) {
+                    throw new InvalidConfigException(
+                            "Config key " + key + " contains unsupported value " + quoted(value));
+                }
+                value = mapper.get(value);
+            }
             values.put(key, value);
         }
         return new PerSdkConfig(minSdk, maxSdk, values);
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
index 2312f5f..e4d799a 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
@@ -17,8 +17,10 @@
 package com.android.server.signedconfig;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.os.Build;
 import android.provider.Settings;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
 
@@ -37,6 +39,30 @@
                     Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS
             )));
 
+    private static final Map<String, String> HIDDEN_API_POLICY_KEY_MAP = makeMap(
+            "DEFAULT", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT),
+            "DISABLED", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_DISABLED),
+            "JUST_WARN", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_JUST_WARN),
+            "ENABLED", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_ENABLED)
+    );
+
+    private static final Map<String, Map<String, String>> KEY_VALUE_MAPPERS = makeMap(
+            Settings.Global.HIDDEN_API_POLICY, HIDDEN_API_POLICY_KEY_MAP
+    );
+
+    private static <K, V> Map<K, V> makeMap(Object... keyValuePairs) {
+        if (keyValuePairs.length % 2 != 0) {
+            throw new IllegalArgumentException();
+        }
+        final int len = keyValuePairs.length / 2;
+        ArrayMap<K, V> m = new ArrayMap<>(len);
+        for (int i = 0; i < len;  ++i) {
+            m.put((K) keyValuePairs[i * 2], (V) keyValuePairs[(i * 2) + 1]);
+        }
+        return Collections.unmodifiableMap(m);
+
+    }
+
     private final Context mContext;
     private final String mSourcePackage;
 
@@ -75,7 +101,7 @@
         }
         SignedConfig config;
         try {
-            config = SignedConfig.parse(configStr, ALLOWED_KEYS);
+            config = SignedConfig.parse(configStr, ALLOWED_KEYS, KEY_VALUE_MAPPERS);
         } catch (InvalidConfigException e) {
             Slog.e(TAG, "Failed to parse config from package " + mSourcePackage, e);
             return;
diff --git a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
index a9d4519..70fadd1 100644
--- a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
@@ -20,8 +20,11 @@
 
 import static org.junit.Assert.fail;
 
+import static java.util.Collections.emptyMap;
 import static java.util.Collections.emptySet;
 
+import android.util.ArrayMap;
+
 import androidx.test.runner.AndroidJUnit4;
 
 import com.google.common.collect.Sets;
@@ -33,6 +36,7 @@
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 
 
@@ -46,10 +50,25 @@
         return Sets.newHashSet(values);
     }
 
+    private static <K, V> Map<K, V> mapOf(Object... keyValuePairs) {
+        if (keyValuePairs.length % 2 != 0) {
+            throw new IllegalArgumentException();
+        }
+        final int len = keyValuePairs.length / 2;
+        ArrayMap<K, V> m = new ArrayMap<>(len);
+        for (int i = 0; i < len;  ++i) {
+            m.put((K) keyValuePairs[i * 2], (V) keyValuePairs[(i * 2) + 1]);
+        }
+        return Collections.unmodifiableMap(m);
+
+    }
+
+
     @Test
     public void testParsePerSdkConfigSdkMinMax() throws JSONException, InvalidConfigException {
         JSONObject json = new JSONObject("{\"minSdk\":2, \"maxSdk\": 3, \"values\": []}");
-        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet());
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet(),
+                emptyMap());
         assertThat(config.minSdk).isEqualTo(2);
         assertThat(config.maxSdk).isEqualTo(3);
     }
@@ -58,7 +77,7 @@
     public void testParsePerSdkConfigNoMinSdk() throws JSONException {
         JSONObject json = new JSONObject("{\"maxSdk\": 3, \"values\": []}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -69,7 +88,7 @@
     public void testParsePerSdkConfigNoMaxSdk() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"values\": []}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -80,7 +99,7 @@
     public void testParsePerSdkConfigNoValues() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -88,10 +107,10 @@
     }
 
     @Test
-    public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException, InvalidConfigException {
+    public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\":null, \"maxSdk\": 3, \"values\": []}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -99,10 +118,10 @@
     }
 
     @Test
-    public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException, InvalidConfigException {
+    public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\":1, \"maxSdk\": null, \"values\": []}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -113,7 +132,7 @@
     public void testParsePerSdkConfigNullValues() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": null}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -124,7 +143,8 @@
     public void testParsePerSdkConfigZeroValues()
             throws JSONException, InvalidConfigException {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": []}");
-        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+                emptyMap());
         assertThat(config.values).hasSize(0);
     }
 
@@ -133,18 +153,30 @@
             throws JSONException, InvalidConfigException {
         JSONObject json = new JSONObject(
                 "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
-        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+                emptyMap());
         assertThat(config.values).containsExactly("a", "1");
     }
 
     @Test
+    public void testParsePerSdkConfigSingleKeyNullValue()
+            throws JSONException, InvalidConfigException {
+        JSONObject json = new JSONObject(
+                "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": null}]}");
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+                emptyMap());
+        assertThat(config.values.keySet()).containsExactly("a");
+        assertThat(config.values.get("a")).isNull();
+    }
+
+    @Test
     public void testParsePerSdkConfigMultiKeys()
             throws JSONException, InvalidConfigException {
         JSONObject json = new JSONObject(
                 "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}, "
                         + "{\"key\":\"c\", \"value\": \"2\"}]}");
         SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(
-                json, setOf("a", "b", "c"));
+                json, setOf("a", "b", "c"), emptyMap());
         assertThat(config.values).containsExactly("a", "1", "c", "2");
     }
 
@@ -153,7 +185,7 @@
         JSONObject json = new JSONObject(
                 "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
         try {
-            SignedConfig.parsePerSdkConfig(json, setOf("b"));
+            SignedConfig.parsePerSdkConfig(json, setOf("b"), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -161,11 +193,67 @@
     }
 
     @Test
+    public void testParsePerSdkConfigSingleKeyWithMap()
+            throws JSONException, InvalidConfigException {
+        JSONObject json = new JSONObject(
+                "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
+                mapOf("a", mapOf("1", "one")));
+        assertThat(config.values).containsExactly("a", "one");
+    }
+
+    @Test
+    public void testParsePerSdkConfigSingleKeyWithMapInvalidValue() throws JSONException {
+        JSONObject json = new JSONObject(
+                "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"2\"}]}");
+        try {
+            SignedConfig.parsePerSdkConfig(json, setOf("b"), mapOf("a", mapOf("1", "one")));
+            fail("Expected InvalidConfigException");
+        } catch (InvalidConfigException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testParsePerSdkConfigMultiKeysWithMap()
+            throws JSONException, InvalidConfigException {
+        JSONObject json = new JSONObject(
+                "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"},"
+                + "{\"key\":\"b\", \"value\": \"1\"}]}");
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+                mapOf("a", mapOf("1", "one")));
+        assertThat(config.values).containsExactly("a", "one", "b", "1");
+    }
+
+    @Test
+    public void testParsePerSdkConfigSingleKeyWithMapToNull()
+            throws JSONException, InvalidConfigException {
+        JSONObject json = new JSONObject(
+                "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
+                mapOf("a", mapOf("1", null)));
+        assertThat(config.values).containsExactly("a", null);
+    }
+
+    @Test
+    public void testParsePerSdkConfigSingleKeyWithMapFromNull()
+            throws JSONException, InvalidConfigException {
+        assertThat(mapOf(null, "allitnil")).containsExactly(null, "allitnil");
+        assertThat(mapOf(null, "allitnil").containsKey(null)).isTrue();
+        JSONObject json = new JSONObject(
+                "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": null}]}");
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
+                mapOf("a", mapOf(null, "allitnil")));
+        assertThat(config.values).containsExactly("a", "allitnil");
+    }
+
+    @Test
     public void testParsePerSdkConfigSingleKeyNoValue()
             throws JSONException, InvalidConfigException {
         JSONObject json = new JSONObject(
                 "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\"}]}");
-        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+        SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+                emptyMap());
         assertThat(config.values).containsExactly("a", null);
     }
 
@@ -173,7 +261,7 @@
     public void testParsePerSdkConfigValuesInvalid() throws JSONException  {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1,  \"values\": \"foo\"}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -184,7 +272,7 @@
     public void testParsePerSdkConfigConfigEntryInvalid() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1,  \"values\": [1, 2]}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -195,7 +283,7 @@
     public void testParsePerSdkConfigConfigEntryNull() throws JSONException {
         JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1,  \"values\": [null]}");
         try {
-            SignedConfig.parsePerSdkConfig(json, emptySet());
+            SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
             fail("Expected InvalidConfigException or JSONException");
         } catch (JSONException | InvalidConfigException e) {
             // expected
@@ -205,14 +293,15 @@
     @Test
     public void testParseVersion() throws InvalidConfigException {
         SignedConfig config = SignedConfig.parse(
-                "{\"version\": 1, \"config\": []}", emptySet());
+                "{\"version\": 1, \"config\": []}", emptySet(), emptyMap());
         assertThat(config.version).isEqualTo(1);
     }
 
     @Test
     public void testParseVersionInvalid() {
         try {
-            SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet());
+            SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet(),
+                    emptyMap());
             fail("Expected InvalidConfigException");
         } catch (InvalidConfigException e) {
             //expected
@@ -222,7 +311,7 @@
     @Test
     public void testParseNoVersion() {
         try {
-            SignedConfig.parse("{\"config\": []}", emptySet());
+            SignedConfig.parse("{\"config\": []}", emptySet(), emptyMap());
             fail("Expected InvalidConfigException");
         } catch (InvalidConfigException e) {
             //expected
@@ -232,7 +321,7 @@
     @Test
     public void testParseNoConfig() {
         try {
-            SignedConfig.parse("{\"version\": 1}", emptySet());
+            SignedConfig.parse("{\"version\": 1}", emptySet(), emptyMap());
             fail("Expected InvalidConfigException");
         } catch (InvalidConfigException e) {
             //expected
@@ -242,7 +331,7 @@
     @Test
     public void testParseConfigNull() {
         try {
-            SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet());
+            SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet(), emptyMap());
             fail("Expected InvalidConfigException");
         } catch (InvalidConfigException e) {
             //expected
@@ -252,7 +341,7 @@
     @Test
     public void testParseVersionNull() {
         try {
-            SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet());
+            SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet(), emptyMap());
             fail("Expected InvalidConfigException");
         } catch (InvalidConfigException e) {
             //expected
@@ -262,7 +351,7 @@
     @Test
     public void testParseConfigInvalidEntry() {
         try {
-            SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet());
+            SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet(), emptyMap());
             fail("Expected InvalidConfigException");
         } catch (InvalidConfigException e) {
             //expected
@@ -273,7 +362,7 @@
     public void testParseSdkConfigSingle() throws InvalidConfigException {
         SignedConfig config = SignedConfig.parse(
                 "{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}]}",
-                emptySet());
+                emptySet(), emptyMap());
         assertThat(config.perSdkConfig).hasSize(1);
     }
 
@@ -281,7 +370,8 @@
     public void testParseSdkConfigMultiple() throws InvalidConfigException {
         SignedConfig config = SignedConfig.parse(
                 "{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}, "
-                        + "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet());
+                        + "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet(),
+                emptyMap());
         assertThat(config.perSdkConfig).hasSize(2);
     }
 
@@ -314,7 +404,6 @@
         SignedConfig config = new SignedConfig(0, Arrays.asList(sdk13, sdk46));
         assertThat(config.getMatchingConfig(2)).isEqualTo(sdk13);
     }
-
     @Test
     public void testGetMatchingConfigNoMatch() {
         SignedConfig.PerSdkConfig sdk1 = new SignedConfig.PerSdkConfig(