Parse new xml attributes used for updatable shared libraries

Attributes added to the `library` tag used in AndroidManifest.xml.
They allow to easily and transparently include/exclude a library from
apps for compatibility purposes.

Bug: 191978330
Test: atest com.android.server.pm.parsing.library.ApexSharedLibraryUpdaterTest com.android.server.systemconfig.SystemConfigTest
Change-Id: Ibdde742a05fd670a9aaee5ee77ae25b9c0801f53
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index d7eef0d..ec3dfad 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -108,17 +108,74 @@
         public final String name;
         public final String filename;
         public final String[] dependencies;
+
+        /**
+         * SDK version this library was added to the BOOTCLASSPATH.
+         *
+         * <p>At the SDK level specified in this field and higher, the apps' uses-library tags for
+         * this library will be ignored, since the library is always available on BOOTCLASSPATH.
+         *
+         * <p>0 means not specified.
+         */
+        public final int onBootclasspathSince;
+
+        /**
+         * SDK version this library was removed from the BOOTCLASSPATH.
+         *
+         * <p>At the SDK level specified in this field and higher, this library needs to be
+         * explicitly added by apps. For compatibility reasons, when an app
+         * targets an SDK less than the value of this attribute, this library is automatically
+         * added.
+         *
+         * <p>0 means not specified.
+         */
+        public final int onBootclasspathBefore;
+
+        /**
+         * Declares whether this library can be safely ignored from <uses-library> tags.
+         *
+         * <p> This can happen if the library initially had to be explicitly depended-on using that
+         * tag but has since been moved to the BOOTCLASSPATH which means now is always available
+         * and the tag is no longer required.
+         */
+        public final boolean canBeSafelyIgnored;
+
         public final boolean isNative;
 
-        SharedLibraryEntry(String name, String filename, String[] dependencies) {
-            this(name, filename, dependencies, false /* isNative */);
+
+        @VisibleForTesting
+        public SharedLibraryEntry(String name, String filename, String[] dependencies,
+                boolean isNative) {
+            this(name, filename, dependencies, 0 /* onBootclasspathSince */,
+                    0 /* onBootclasspathBefore */, isNative);
         }
 
-        SharedLibraryEntry(String name, String filename, String[] dependencies, boolean isNative) {
+        @VisibleForTesting
+        public SharedLibraryEntry(String name, String filename, String[] dependencies,
+                int onBootclasspathSince, int onBootclassPathBefore) {
+            this(name, filename, dependencies, onBootclasspathSince, onBootclassPathBefore,
+                    false /* isNative */);
+        }
+
+        SharedLibraryEntry(String name, String filename, String[] dependencies,
+                int onBootclasspathSince, int onBootclasspathBefore, boolean isNative) {
             this.name = name;
             this.filename = filename;
             this.dependencies = dependencies;
+            this.onBootclasspathSince = onBootclasspathSince;
+            this.onBootclasspathBefore = onBootclasspathBefore;
             this.isNative = isNative;
+
+            canBeSafelyIgnored = this.onBootclasspathSince != 0
+                    && isSdkAtLeast(this.onBootclasspathSince);
+        }
+
+        private static boolean isSdkAtLeast(int level) {
+            if ("REL".equals(Build.VERSION.CODENAME)) {
+                return Build.VERSION.SDK_INT >= level;
+            }
+            return level == Build.VERSION_CODES.CUR_DEVELOPMENT
+                    || Build.VERSION.SDK_INT >= level;
         }
     }
 
@@ -771,11 +828,17 @@
                             XmlUtils.skipCurrentTag(parser);
                         }
                     } break;
+                    case "updatable-library":
+                        // "updatable-library" is meant to behave exactly like "library"
                     case "library": {
                         if (allowLibs) {
                             String lname = parser.getAttributeValue(null, "name");
                             String lfile = parser.getAttributeValue(null, "file");
                             String ldependency = parser.getAttributeValue(null, "dependency");
+                            int minDeviceSdk = XmlUtils.readIntAttribute(parser, "min-device-sdk",
+                                    0);
+                            int maxDeviceSdk = XmlUtils.readIntAttribute(parser, "max-device-sdk",
+                                    0);
                             if (lname == null) {
                                 Slog.w(TAG, "<" + name + "> without name in " + permFile + " at "
                                         + parser.getPositionDescription());
@@ -783,10 +846,20 @@
                                 Slog.w(TAG, "<" + name + "> without file in " + permFile + " at "
                                         + parser.getPositionDescription());
                             } else {
-                                //Log.i(TAG, "Got library " + lname + " in " + lfile);
-                                SharedLibraryEntry entry = new SharedLibraryEntry(lname, lfile,
-                                        ldependency == null ? new String[0] : ldependency.split(":"));
-                                mSharedLibraries.put(lname, entry);
+                                boolean allowedMinSdk = minDeviceSdk <= Build.VERSION.SDK_INT;
+                                boolean allowedMaxSdk =
+                                        maxDeviceSdk == 0 || maxDeviceSdk >= Build.VERSION.SDK_INT;
+                                if (allowedMinSdk && allowedMaxSdk) {
+                                    int bcpSince = XmlUtils.readIntAttribute(parser,
+                                            "on-bootclasspath-since", 0);
+                                    int bcpBefore = XmlUtils.readIntAttribute(parser,
+                                            "on-bootclasspath-before", 0);
+                                    SharedLibraryEntry entry = new SharedLibraryEntry(lname, lfile,
+                                            ldependency == null
+                                                    ? new String[0] : ldependency.split(":"),
+                                            bcpSince, bcpBefore);
+                                    mSharedLibraries.put(lname, entry);
+                                }
                             }
                         } else {
                             logNotAllowedInPartition(name, permFile, parser);
diff --git a/services/core/java/com/android/server/pm/parsing/library/ApexSharedLibraryUpdater.java b/services/core/java/com/android/server/pm/parsing/library/ApexSharedLibraryUpdater.java
new file mode 100644
index 0000000..0418afb
--- /dev/null
+++ b/services/core/java/com/android/server/pm/parsing/library/ApexSharedLibraryUpdater.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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.server.pm.parsing.library;
+
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemConfig;
+import com.android.server.pm.parsing.pkg.ParsedPackage;
+
+/**
+ * Updates packages to add or remove dependencies on shared libraries as per attributes
+ * in the library declaration
+ *
+ * @hide
+ */
+@VisibleForTesting
+public class ApexSharedLibraryUpdater extends PackageSharedLibraryUpdater {
+
+    /**
+     * ArrayMap like the one you find in {@link SystemConfig}. The keys are the library names.
+     */
+    private final ArrayMap<String, SystemConfig.SharedLibraryEntry> mSharedLibraries;
+
+    public ApexSharedLibraryUpdater(
+            ArrayMap<String, SystemConfig.SharedLibraryEntry> sharedLibraries) {
+        mSharedLibraries = sharedLibraries;
+    }
+
+    @Override
+    public void updatePackage(ParsedPackage parsedPackage, boolean isUpdatedSystemApp) {
+        final int builtInLibCount = mSharedLibraries.size();
+        for (int i = 0; i < builtInLibCount; i++) {
+            updateSharedLibraryForPackage(mSharedLibraries.valueAt(i), parsedPackage);
+        }
+    }
+
+    private void updateSharedLibraryForPackage(SystemConfig.SharedLibraryEntry entry,
+            ParsedPackage parsedPackage) {
+        if (entry.onBootclasspathBefore != 0
+                && parsedPackage.getTargetSdkVersion() < entry.onBootclasspathBefore) {
+            // this package targets an API where this library was in the BCP, so add
+            // the library transparently in case the package is using it
+            prefixRequiredLibrary(parsedPackage, entry.name);
+        }
+
+        if (entry.canBeSafelyIgnored) {
+            // the library is now present in the BCP and always available; we don't need to add
+            // it a second time
+            removeLibrary(parsedPackage, entry.name);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/parsing/library/PackageBackwardCompatibility.java b/services/core/java/com/android/server/pm/parsing/library/PackageBackwardCompatibility.java
index 4334bf6..bbf584d 100644
--- a/services/core/java/com/android/server/pm/parsing/library/PackageBackwardCompatibility.java
+++ b/services/core/java/com/android/server/pm/parsing/library/PackageBackwardCompatibility.java
@@ -25,6 +25,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemConfig;
 import com.android.server.pm.parsing.pkg.ParsedPackage;
 
 import java.util.ArrayList;
@@ -63,6 +64,11 @@
 
         boolean bootClassPathContainsATB = !addUpdaterForAndroidTestBase(packageUpdaters);
 
+        // ApexSharedLibraryUpdater should be the last one, to allow modifications introduced by
+        // mainline after dessert release.
+        packageUpdaters.add(new ApexSharedLibraryUpdater(
+                SystemConfig.getInstance().getSharedLibraries()));
+
         PackageSharedLibraryUpdater[] updaterArray = packageUpdaters
                 .toArray(new PackageSharedLibraryUpdater[0]);
         INSTANCE = new PackageBackwardCompatibility(
@@ -106,6 +112,11 @@
 
     private final PackageSharedLibraryUpdater[] mPackageUpdaters;
 
+    @VisibleForTesting
+    PackageSharedLibraryUpdater[] getPackageUpdaters() {
+        return mPackageUpdaters;
+    }
+
     private PackageBackwardCompatibility(
             boolean bootClassPathContainsATB, PackageSharedLibraryUpdater[] packageUpdaters) {
         this.mBootClassPathContainsATB = bootClassPathContainsATB;
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java b/services/tests/servicestests/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java
new file mode 100644
index 0000000..1d9ea4b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2021 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.server.pm.parsing.library;
+
+import android.os.Build;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArrayMap;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.SystemConfig;
+import com.android.server.pm.parsing.pkg.AndroidPackage;
+import com.android.server.pm.parsing.pkg.PackageImpl;
+import com.android.server.pm.parsing.pkg.ParsedPackage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+/**
+ * Test for {@link ApexSharedLibraryUpdater}
+ */
+@Presubmit
+@SmallTest
+@RunWith(JUnit4.class)
+public class ApexSharedLibraryUpdaterTest extends PackageSharedLibraryUpdaterTest {
+
+    private final ArrayMap<String, SystemConfig.SharedLibraryEntry> mSharedLibraries =
+            new ArrayMap<>(8);
+
+    @Before
+    public void setUp() throws Exception {
+        installSharedLibraries();
+    }
+
+    private void installSharedLibraries() throws Exception {
+        mSharedLibraries.clear();
+        insertLibrary("foo", 0, 0);
+        insertLibrary("fooBcpSince30", 30, 0);
+        insertLibrary("fooBcpBefore30", 0, 30);
+        insertLibrary("fooFromFuture", Build.VERSION.SDK_INT + 2, 0);
+    }
+
+    private void insertLibrary(String libraryName, int onBootclasspathSince,
+            int onBootclasspathBefore) {
+        mSharedLibraries.put(libraryName, new SystemConfig.SharedLibraryEntry(
+                libraryName,
+                "foo.jar",
+                new String[0] /* dependencies */,
+                onBootclasspathSince,
+                onBootclasspathBefore
+                )
+        );
+    }
+
+    @Test
+    public void testRegularAppOnRPlus() {
+        // platform Q should have changes (tested below)
+
+        // these should have no changes
+        checkNoChanges(Build.VERSION_CODES.R);
+        checkNoChanges(Build.VERSION_CODES.S);
+        checkNoChanges(Build.VERSION_CODES.TIRAMISU);
+        checkNoChanges(Build.VERSION_CODES.CUR_DEVELOPMENT);
+    }
+
+    private void checkNoChanges(int targetSdkVersion) {
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(targetSdkVersion)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(targetSdkVersion)
+                .hideAsParsed())
+                .hideAsFinal();
+
+        checkBackwardsCompatibility(before, after);
+    }
+
+    @Test
+    public void testBcpSince30Applied() {
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .addUsesLibrary("fooBcpSince30")
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // note: target sdk is not what matters in this logic. It's the system SDK
+        // should be removed because on 30+ (R+) it is implicit
+
+        checkBackwardsCompatibility(before, after);
+    }
+
+    @Test
+    public void testBcpSince11kNotAppliedWithoutLibrary() {
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // note: target sdk is not what matters in this logic. It's the system SDK
+        // nothing should change because the implicit from is only from a future platform release
+        checkBackwardsCompatibility(before, after);
+    }
+
+    @Test
+    public void testBcpSince11kNotAppliedWithLibrary() {
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .addUsesLibrary("fooFromFuture")
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .addUsesLibrary("fooFromFuture")
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // note: target sdk is not what matters in this logic. It's the system SDK
+        // nothing should change because the implicit from is only from a future platform release
+        checkBackwardsCompatibility(before, after);
+    }
+
+    @Test
+    public void testBcpBefore30NotApplied() {
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // should not be affected because it is still in the BCP in 30 / R
+        checkBackwardsCompatibility(before, after);
+    }
+
+    @Test
+    public void testBcpBefore30Applied() {
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.Q)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.Q)
+                .addUsesLibrary("fooBcpBefore30")
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // should be present because this was in BCP in 29 / Q
+        checkBackwardsCompatibility(before, after);
+    }
+
+    /**
+     * Test a library that was first removed from the BCP [to a mainline module] and later was
+     * moved back to the BCP via a mainline module update. All of this happening before the current
+     * SDK.
+     */
+    @Test
+    public void testBcpRemovedThenAddedPast() {
+        insertLibrary("fooBcpRemovedThenAdded", 30, 28);
+
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.N)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.N)
+                .addUsesLibrary("fooBcpBefore30")
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // the library is now in the BOOTCLASSPATH (for the second time) so it doesn't need to be
+        // listed
+        checkBackwardsCompatibility(before, after);
+    }
+
+    /**
+     * Test a library that was first removed from the BCP [to a mainline module] and later was
+     * moved back to the BCP via a mainline module update. The first part happening before the
+     * current SDK and the second part after.
+     */
+    @Test
+    public void testBcpRemovedThenAddedMiddle_targetQ() {
+        insertLibrary("fooBcpRemovedThenAdded", Build.VERSION.SDK_INT + 1, 30);
+
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.Q)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.Q)
+                .addUsesLibrary("fooBcpRemovedThenAdded")
+                .addUsesLibrary("fooBcpBefore30")
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // in this example, we are at the point where the library is not in the BOOTCLASSPATH.
+        // Because the app targets Q / 29 (when this library was in the BCP) then we need to add it
+        checkBackwardsCompatibility(before, after);
+    }
+
+    /**
+     * Test a library that was first removed from the BCP [to a mainline module] and later was
+     * moved back to the BCP via a mainline module update. The first part happening before the
+     * current SDK and the second part after.
+     */
+    @Test
+    public void testBcpRemovedThenAddedMiddle_targetR() {
+        insertLibrary("fooBcpRemovedThenAdded", Build.VERSION.SDK_INT + 1, 30);
+
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // in this example, we are at the point where the library is not in the BOOTCLASSPATH.
+        // Because the app targets R/30 (when this library was removed from the BCP) then we don't
+        //need to add it
+        checkBackwardsCompatibility(before, after);
+    }
+
+    /**
+     * Test a library that was first removed from the BCP [to a mainline module] and later was
+     * moved back to the BCP via a mainline module update. The first part happening before the
+     * current SDK and the second part after.
+     */
+    @Test
+    public void testBcpRemovedThenAddedMiddle_targetR_usingLib() {
+        insertLibrary("fooBcpRemovedThenAdded", Build.VERSION.SDK_INT + 1, 30);
+
+        ParsedPackage before = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .addUsesLibrary("fooBcpRemovedThenAdded")
+                .hideAsParsed());
+
+        AndroidPackage after = ((ParsedPackage) PackageImpl.forTesting(PACKAGE_NAME)
+                .setTargetSdkVersion(Build.VERSION_CODES.R)
+                .addUsesLibrary("fooBcpRemovedThenAdded")
+                .hideAsParsed())
+                .hideAsFinal();
+
+        // in this example, we are at the point where the library is not in the BOOTCLASSPATH.
+        // Because the app wants to use the library, it needs to be present
+        checkBackwardsCompatibility(before, after);
+    }
+
+    private void checkBackwardsCompatibility(ParsedPackage before, AndroidPackage after) {
+        checkBackwardsCompatibility(before, after,
+                () -> new ApexSharedLibraryUpdater(mSharedLibraries));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java b/services/tests/servicestests/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
index 9768f17..5bcd0f6 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
@@ -21,6 +21,8 @@
 import static com.android.server.pm.parsing.library.SharedLibraryNames.ANDROID_TEST_RUNNER;
 import static com.android.server.pm.parsing.library.SharedLibraryNames.ORG_APACHE_HTTP_LEGACY;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.content.pm.parsing.ParsingPackage;
 import android.os.Build;
 import android.platform.test.annotations.Presubmit;
@@ -182,6 +184,22 @@
         checkBackwardsCompatibility(before, ((ParsedPackage) after.hideAsParsed()).hideAsFinal());
     }
 
+    /**
+     * Ensures that ApexSharedLibraryUpdater is the last updater in the list of package updaters
+     * used by PackageBackwardCompatibility.
+     *
+     * This is required so mainline can add and remove libraries installed by the platform updaters.
+     */
+    @Test
+    public void testApexPackageUpdaterOrdering() {
+        PackageBackwardCompatibility instance =
+                (PackageBackwardCompatibility) PackageBackwardCompatibility.getInstance();
+        PackageSharedLibraryUpdater[] updaterArray = instance.getPackageUpdaters();
+
+        PackageSharedLibraryUpdater lastUpdater = updaterArray[updaterArray.length - 1];
+        assertThat(lastUpdater).isInstanceOf(ApexSharedLibraryUpdater.class);
+    }
+
     private void checkBackwardsCompatibility(ParsedPackage before, AndroidPackage after) {
         checkBackwardsCompatibility(before, after, PackageBackwardCompatibility::getInstance);
     }
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index cc53160..4dcd633 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.testng.Assert.expectThrows;
 
+import android.os.Build;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -330,6 +331,164 @@
     }
 
     /**
+     * Tests that readPermissions works correctly for a library with on-bootclasspath-before
+     * and on-bootclasspath-since.
+     */
+    @Test
+    public void readPermissions_allowLibs_parsesSimpleLibrary() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        on-bootclasspath-before=\"10\"\n"
+                + "        on-bootclasspath-since=\"20\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertFooIsOnlySharedLibrary();
+        SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo");
+        assertThat(entry.onBootclasspathBefore).isEqualTo(10);
+        assertThat(entry.onBootclasspathSince).isEqualTo(20);
+    }
+
+    /**
+     * Tests that readPermissions works correctly for a library using the new
+     * {@code updatable-library} tag.
+     */
+    @Test
+    public void readPermissions_allowLibs_parsesUpdatableLibrary() throws IOException {
+        String contents =
+                "<permissions>\n"
+                        + "    <updatable-library \n"
+                        + "        name=\"foo\"\n"
+                        + "        file=\"foo.jar\"\n"
+                        + "        on-bootclasspath-before=\"10\"\n"
+                        + "        on-bootclasspath-since=\"20\"\n"
+                        + "     />\n\n"
+                        + " </permissions>";
+        parseSharedLibraries(contents);
+        assertFooIsOnlySharedLibrary();
+        SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo");
+        assertThat(entry.onBootclasspathBefore).isEqualTo(10);
+        assertThat(entry.onBootclasspathSince).isEqualTo(20);
+    }
+
+    /**
+     * Tests that readPermissions for a library with {@code min-device-sdk} lower than the current
+     * SDK results in the library being added to the shared libraries.
+     */
+    @Test
+    public void readPermissions_allowLibs_allowsOldMinSdk() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        min-device-sdk=\"30\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertFooIsOnlySharedLibrary();
+    }
+
+    /**
+     * Tests that readPermissions for a library with {@code min-device-sdk} equal to the current
+     * SDK results in the library being added to the shared libraries.
+     */
+    @Test
+    public void readPermissions_allowLibs_allowsCurrentMinSdk() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        min-device-sdk=\"" + Build.VERSION.SDK_INT + "\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertFooIsOnlySharedLibrary();
+    }
+
+    /**
+     * Tests that readPermissions for a library with {@code min-device-sdk} greater than the current
+     * SDK results in the library being ignored.
+     */
+    @Test
+    public void readPermissions_allowLibs_ignoresMinSdkInFuture() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        min-device-sdk=\"" + (Build.VERSION.SDK_INT + 1) + "\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertThat(mSysConfig.getSharedLibraries()).isEmpty();
+    }
+
+    /**
+     * Tests that readPermissions for a library with {@code max-device-sdk} less than the current
+     * SDK results in the library being ignored.
+     */
+    @Test
+    public void readPermissions_allowLibs_ignoredOldMaxSdk() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        max-device-sdk=\"30\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertThat(mSysConfig.getSharedLibraries()).isEmpty();
+    }
+
+    /**
+     * Tests that readPermissions for a library with {@code max-device-sdk} equal to the current
+     * SDK results in the library being added to the shared libraries.
+     */
+    @Test
+    public void readPermissions_allowLibs_allowsCurrentMaxSdk() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        max-device-sdk=\"" + Build.VERSION.SDK_INT + "\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertFooIsOnlySharedLibrary();
+    }
+
+    /**
+     * Tests that readPermissions for a library with {@code max-device-sdk} greater than the current
+     * SDK results in the library being added to the shared libraries.
+     */
+    @Test
+    public void readPermissions_allowLibs_allowsMaxSdkInFuture() throws IOException {
+        String contents =
+                "<permissions>\n"
+                + "    <library \n"
+                + "        name=\"foo\"\n"
+                + "        file=\"foo.jar\"\n"
+                + "        max-device-sdk=\"" + (Build.VERSION.SDK_INT + 1) + "\"\n"
+                + "     />\n\n"
+                + " </permissions>";
+        parseSharedLibraries(contents);
+        assertFooIsOnlySharedLibrary();
+    }
+
+    private void parseSharedLibraries(String contents) throws IOException {
+        File folder = createTempSubfolder("permissions_folder");
+        createTempFile(folder, "permissions.xml", contents);
+        readPermissions(folder, /* permissionFlag = ALLOW_LIBS */ 0x02);
+    }
+
+    /**
      * Creates folderName/fileName in the mTemporaryFolder and fills it with the contents.
      *
      * @param folderName subdirectory of mTemporaryFolder to put the file, creating if needed
@@ -366,4 +525,11 @@
 
         return folder;
     }
+
+    private void assertFooIsOnlySharedLibrary() {
+        assertThat(mSysConfig.getSharedLibraries().size()).isEqualTo(1);
+        SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo");
+        assertThat(entry.name).isEqualTo("foo");
+        assertThat(entry.filename).isEqualTo("foo.jar");
+    }
 }