Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 70da334f authored by Pedro Loureiro's avatar Pedro Loureiro
Browse files

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
parent 6f7539ec
Loading
Loading
Loading
Loading
+80 −7
Original line number Diff line number Diff line
@@ -108,17 +108,74 @@ public class SystemConfig {
        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);
        }

        @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, boolean 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 @@ public class SystemConfig {
                            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,11 +846,21 @@ public class SystemConfig {
                                Slog.w(TAG, "<" + name + "> without file in " + permFile + " at "
                                        + parser.getPositionDescription());
                            } else {
                                //Log.i(TAG, "Got library " + lname + " in " + lfile);
                                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(":"));
                                            ldependency == null
                                                    ? new String[0] : ldependency.split(":"),
                                            bcpSince, bcpBefore);
                                    mSharedLibraries.put(lname, entry);
                                }
                            }
                        } else {
                            logNotAllowedInPartition(name, permFile, parser);
                        }
+67 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.content.pm.parsing.ParsingPackage;
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 @@ public class PackageBackwardCompatibility extends PackageSharedLibraryUpdater {

        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 @@ public class PackageBackwardCompatibility extends PackageSharedLibraryUpdater {

    private final PackageSharedLibraryUpdater[] mPackageUpdaters;

    @VisibleForTesting
    PackageSharedLibraryUpdater[] getPackageUpdaters() {
        return mPackageUpdaters;
    }

    private PackageBackwardCompatibility(
            boolean bootClassPathContainsATB, PackageSharedLibraryUpdater[] packageUpdaters) {
        this.mBootClassPathContainsATB = bootClassPathContainsATB;
+281 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import static com.android.server.pm.parsing.library.SharedLibraryNames.ANDROID_T
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 @@ public class PackageBackwardCompatibilityTest extends PackageSharedLibraryUpdate
        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);
    }
Loading