Add a util class for constructing class loader context for primary dex.

This class is rewritten from
frameworks/base/services/core/java/com/android/server/pm/dex/DexoptUtils.java
with logic that is more readable.

Also:
- Add reflection wrappers that are used in this CL.

Bug: 229268202
Test: atest ArtServiceTests
Ignore-AOSP-first: ART services
Change-Id: I600da98585ff9aa2806bed4d18d6ce5eb6422d1a
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
new file mode 100644
index 0000000..d6b8a59
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2022 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.art;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.Immutable;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.art.wrapper.SharedLibraryInfo;
+
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.DexClassLoader;
+import dalvik.system.PathClassLoader;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** @hide */
+public class PrimaryDexUtils {
+    private static final String SHARED_LIBRARY_LOADER_TYPE = PathClassLoader.class.getName();
+
+    /**
+     * Returns the basic information about all primary dex files belonging to the package. The
+     * return value is a list where the entry at index 0 is the information about the base APK, and
+     * the entry at index i is the information about the (i-1)-th split APK.
+     */
+    @NonNull
+    public static List<PrimaryDexInfo> getDexInfo(@NonNull AndroidPackageApi pkg) {
+        return getDexInfoImpl(pkg)
+                .stream()
+                .map(builder -> builder.build())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Same as above, but requires {@link PackageState} in addition, and returns the detailed
+     * information, including the class loader context.
+     */
+    @NonNull
+    public static List<DetailedPrimaryDexInfo> getDetailedDexInfo(
+            @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) {
+        return getDetailedDexInfoImpl(pkgState, pkg)
+                .stream()
+                .map(builder -> builder.buildDetailed())
+                .collect(Collectors.toList());
+    }
+
+    @NonNull
+    private static List<PrimaryDexInfoBuilder> getDexInfoImpl(@NonNull AndroidPackageApi pkg) {
+        List<PrimaryDexInfoBuilder> dexInfos = new ArrayList<>();
+
+        PrimaryDexInfoBuilder baseInfo = new PrimaryDexInfoBuilder(pkg.getBaseApkPath());
+        baseInfo.mHasCode = pkg.isHasCode();
+        baseInfo.mIsBaseApk = true;
+        dexInfos.add(baseInfo);
+
+        String[] splitNames = pkg.getSplitNames();
+        String[] splitCodePaths = pkg.getSplitCodePaths();
+        int[] splitFlags = pkg.getSplitFlags();
+
+        for (int i = 0; i < splitNames.length; i++) {
+            PrimaryDexInfoBuilder splitInfo = new PrimaryDexInfoBuilder(splitCodePaths[i]);
+            splitInfo.mHasCode =
+                    splitFlags != null && (splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0;
+            splitInfo.mSplitIndex = i;
+            splitInfo.mSplitName = splitNames[i];
+            dexInfos.add(splitInfo);
+        }
+
+        return dexInfos;
+    }
+
+    @NonNull
+    private static List<PrimaryDexInfoBuilder> getDetailedDexInfoImpl(
+            @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) {
+        List<PrimaryDexInfoBuilder> dexInfos = getDexInfoImpl(pkg);
+
+        PrimaryDexInfoBuilder baseApk = dexInfos.get(0);
+        assert baseApk.mIsBaseApk;
+        baseApk.mClassLoaderName = pkg.getClassLoaderName();
+        File baseDexFile = new File(baseApk.mDexPath);
+        baseApk.mRelativeDexPath = baseDexFile.getName();
+
+        // Shared libraries are the dependencies of the base APK.
+        baseApk.mSharedLibrariesContext = encodeSharedLibraries(pkgState.getUsesLibraryInfos());
+
+        String[] splitClassLoaderNames = pkg.getSplitClassLoaderNames();
+        SparseArray<int[]> splitDependencies = pkg.getSplitDependencies();
+        boolean isIsolatedSplitLoading =
+                pkg.isIsolatedSplitLoading() && !Utils.isEmpty(splitDependencies);
+
+        for (int i = 1; i < dexInfos.size(); i++) {
+            assert dexInfos.get(i).mSplitIndex == i - 1;
+            File splitDexFile = new File(dexInfos.get(i).mDexPath);
+            if (!splitDexFile.getParent().equals(baseDexFile.getParent())) {
+                throw new IllegalStateException(
+                        "Split APK and base APK are in different directories: "
+                        + splitDexFile.getParent() + " != " + baseDexFile.getParent());
+            }
+            dexInfos.get(i).mRelativeDexPath = splitDexFile.getName();
+            if (isIsolatedSplitLoading && dexInfos.get(i).mHasCode) {
+                dexInfos.get(i).mClassLoaderName =
+                        splitClassLoaderNames[dexInfos.get(i).mSplitIndex];
+
+                // Keys and values of `splitDependencies` are `split index + 1` for split APK or 0
+                // for base APK, so they can be regarded as indices to `dexInfos`.
+                int[] dependencies = splitDependencies.get(i);
+                if (!Utils.isEmpty(dependencies)) {
+                    // We only care about the first dependency because it is the parent split. The
+                    // rest are configuration splits, which we don't care.
+                    dexInfos.get(i).mSplitDependency = dexInfos.get(dependencies[0]);
+                }
+            }
+        }
+
+        if (isIsolatedSplitLoading) {
+            computeClassLoaderContextsIsolated(dexInfos);
+        } else {
+            computeClassLoaderContexts(dexInfos);
+        }
+
+        return dexInfos;
+    }
+
+    /**
+     * Computes class loader context for an app that didn't request isolated split loading. Stores
+     * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}.
+     *
+     * In this case, all the splits will be loaded in the base apk class loader (in the order of
+     * their definition).
+     *
+     * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK is
+     * `CLN[base.apk, split_0.apk, ..., split_n-1.apk]{shared-libraries}`; where `CLN` is the
+     * class loader name for the base APK.
+     */
+    private static void computeClassLoaderContexts(@NonNull List<PrimaryDexInfoBuilder> dexInfos) {
+        String baseClassLoaderName = dexInfos.get(0).mClassLoaderName;
+        String sharedLibrariesContext = dexInfos.get(0).mSharedLibrariesContext;
+        List<String> classpath = new ArrayList<>();
+        for (PrimaryDexInfoBuilder dexInfo : dexInfos) {
+            if (dexInfo.mHasCode) {
+                dexInfo.mClassLoaderContext = encodeClassLoader(baseClassLoaderName, classpath,
+                        null /* parentContext */, sharedLibrariesContext);
+            }
+            // Note that the splits with no code are not removed from the classpath computation.
+            // I.e., split_n might get the split_n-1 in its classpath dependency even if split_n-1
+            // has no code.
+            // The splits with no code do not matter for the runtime which ignores APKs without code
+            // when doing the classpath checks. As such we could actually filter them but we don't
+            // do it in order to keep consistency with how the apps are loaded.
+            classpath.add(dexInfo.mRelativeDexPath);
+        }
+    }
+
+    /**
+     * Computes class loader context for an app that requested for isolated split loading. Stores
+     * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}.
+     *
+     * In this case, each split will be loaded with a separate class loader, whose context is a
+     * chain formed from inter-split dependencies.
+     *
+     * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK that
+     * depends on the base APK is `CLN_n[];CLN[base.apk]{shared-libraries}`; the CLC for the n-th
+     * split APK that depends on the m-th split APK is
+     * `CLN_n[];CLN_m[split_m.apk];...;CLN[base.apk]{shared-libraries}`; where `CLN` is the base
+     * class loader name for the base APK, `CLN_i` is the class loader name for the i-th split APK,
+     * and `...` represents the ancestors along the dependency chain.
+     *
+     * Specially, if a split does not have any dependency, the CLC for it is `CLN_n[]`.
+     */
+    private static void computeClassLoaderContextsIsolated(
+            @NonNull List<PrimaryDexInfoBuilder> dexInfos) {
+        for (PrimaryDexInfoBuilder dexInfo : dexInfos) {
+            if (dexInfo.mHasCode) {
+                dexInfo.mClassLoaderContext = encodeClassLoader(dexInfo.mClassLoaderName,
+                        null /* classpath */, getParentContextRecursive(dexInfo),
+                        dexInfo.mSharedLibrariesContext);
+            }
+        }
+    }
+
+    /**
+     * Computes the parent class loader context, recursively. Caches results in {@link
+     * PrimaryDexInfoBuilder#mContextForChildren}.
+     */
+    @Nullable
+    private static String getParentContextRecursive(@NonNull PrimaryDexInfoBuilder dexInfo) {
+        if (dexInfo.mSplitDependency == null) {
+            return null;
+        }
+        PrimaryDexInfoBuilder parent = dexInfo.mSplitDependency;
+        if (parent.mContextForChildren == null) {
+            parent.mContextForChildren =
+                    encodeClassLoader(parent.mClassLoaderName, List.of(parent.mRelativeDexPath),
+                            getParentContextRecursive(parent), parent.mSharedLibrariesContext);
+        }
+        return parent.mContextForChildren;
+    }
+
+    /**
+     * Returns class loader context in the format of
+     * `CLN[classpath...]{share-libraries};parent-context`, where `CLN` is the class loader name.
+     */
+    @NonNull
+    private static String encodeClassLoader(@Nullable String classLoaderName,
+            @Nullable List<String> classpath, @Nullable String parentContext,
+            @Nullable String sharedLibrariesContext) {
+        StringBuilder classLoaderContext = new StringBuilder();
+
+        classLoaderContext.append(encodeClassLoaderName(classLoaderName));
+
+        classLoaderContext.append(
+                "[" + (classpath != null ? String.join(":", classpath) : "") + "]");
+
+        if (!TextUtils.isEmpty(sharedLibrariesContext)) {
+            classLoaderContext.append(sharedLibrariesContext);
+        }
+
+        if (!TextUtils.isEmpty(parentContext)) {
+            classLoaderContext.append(";" + parentContext);
+        }
+
+        return classLoaderContext.toString();
+    }
+
+    @NonNull
+    private static String encodeClassLoaderName(@Nullable String classLoaderName) {
+        // `PathClassLoader` and `DexClassLoader` are grouped together because they have the same
+        // behavior. For null values we default to "PCL". This covers the case where a package does
+        // not specify any value for its class loader.
+        if (classLoaderName == null || PathClassLoader.class.getName().equals(classLoaderName)
+                || DexClassLoader.class.getName().equals(classLoaderName)) {
+            return "PCL";
+        } else if (DelegateLastClassLoader.class.getName().equals(classLoaderName)) {
+            return "DLC";
+        } else {
+            throw new IllegalStateException("Unsupported classLoaderName: " + classLoaderName);
+        }
+    }
+
+    /**
+     * Returns shared libraries context in the format of
+     * `{PCL[library_1_dex_1.jar:library_1_dex_2.jar:...]{library_1-dependencies}#PCL[
+     *     library_1_dex_2.jar:library_2_dex_2.jar:...]{library_2-dependencies}#...}`.
+     */
+    @Nullable
+    private static String encodeSharedLibraries(@Nullable List<SharedLibraryInfo> sharedLibraries) {
+        if (Utils.isEmpty(sharedLibraries)) {
+            return null;
+        }
+        return sharedLibraries.stream()
+                .map(library
+                        -> encodeClassLoader(SHARED_LIBRARY_LOADER_TYPE, library.getAllCodePaths(),
+                                null /* parentContext */,
+                                encodeSharedLibraries(library.getDependencies())))
+                .collect(Collectors.joining("#", "{", "}"));
+    }
+
+    /** Basic information about a primary dex file (either the base APK or a split APK). */
+    @Immutable
+    public static class PrimaryDexInfo {
+        private final @NonNull String mDexPath;
+        private final boolean mHasCode;
+        private final boolean mIsBaseApk;
+        private final int mSplitIndex;
+        private final @Nullable String mSplitName;
+
+        PrimaryDexInfo(@NonNull String dexPath, boolean hasCode, boolean isBaseApk, int splitIndex,
+                @Nullable String splitName) {
+            mDexPath = dexPath;
+            mHasCode = hasCode;
+            mIsBaseApk = isBaseApk;
+            mSplitIndex = splitIndex;
+            mSplitName = splitName;
+        }
+
+        /** The path to the dex file. */
+        public @NonNull String dexPath() {
+            return mDexPath;
+        }
+
+        /** True if the dex file has code. */
+        public boolean hasCode() {
+            return mHasCode;
+        }
+
+        /** True if the dex file is the base APK. */
+        public boolean isBaseApk() {
+            return mIsBaseApk;
+        }
+
+        /** The index to {@link AndroidPackageApi#getSplitNames()}, or -1 for base APK. */
+        public int splitIndex() {
+            return mSplitIndex;
+        }
+
+        /** The name of the split, or null for base APK. */
+        public @Nullable String splitName() {
+            return mSplitName;
+        }
+    }
+
+    /**
+     * Detailed information about a primary dex file (either the base APK or a split APK). It
+     * contains the class loader context in addition to what is in {@link PrimaryDexInfo}, but
+     * producing it requires {@link PackageState}.
+     */
+    @Immutable
+    public static class DetailedPrimaryDexInfo extends PrimaryDexInfo {
+        private final @Nullable String mClassLoaderContext;
+
+        DetailedPrimaryDexInfo(@NonNull String dexPath, boolean hasCode, boolean isBaseApk,
+                int splitIndex, @Nullable String splitName, @Nullable String classLoaderContext) {
+            super(dexPath, hasCode, isBaseApk, splitIndex, splitName);
+            mClassLoaderContext = classLoaderContext;
+        }
+
+        /**
+         * A string describing the structure of the class loader that the dex file is loaded with.
+         */
+        public @Nullable String classLoaderContext() {
+            return mClassLoaderContext;
+        }
+    }
+
+    private static class PrimaryDexInfoBuilder {
+        @NonNull String mDexPath;
+        boolean mHasCode = false;
+        boolean mIsBaseApk = false;
+        int mSplitIndex = -1;
+        @Nullable String mSplitName = null;
+        @Nullable String mRelativeDexPath = null;
+        @Nullable String mClassLoaderContext = null;
+        @Nullable String mClassLoaderName = null;
+        @Nullable PrimaryDexInfoBuilder mSplitDependency = null;
+        /** The class loader context of the shared libraries. Only applicable for the base APK. */
+        @Nullable String mSharedLibrariesContext = null;
+        /** The class loader context for children to use when this dex file is used as a parent. */
+        @Nullable String mContextForChildren = null;
+
+        PrimaryDexInfoBuilder(@NonNull String dexPath) {
+            mDexPath = dexPath;
+        }
+
+        PrimaryDexInfo build() {
+            return new PrimaryDexInfo(mDexPath, mHasCode, mIsBaseApk, mSplitIndex, mSplitName);
+        }
+
+        DetailedPrimaryDexInfo buildDetailed() {
+            return new DetailedPrimaryDexInfo(
+                    mDexPath, mHasCode, mIsBaseApk, mSplitIndex, mSplitName, mClassLoaderContext);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
new file mode 100644
index 0000000..6a54b75
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 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.art;
+
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.Collection;
+
+/** @hide */
+public final class Utils {
+    private Utils() {}
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static <T> boolean isEmpty(@Nullable Collection<T> array) {
+        return array == null || array.isEmpty();
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static <T> boolean isEmpty(@Nullable SparseArray<T> array) {
+        return array == null || array.size() == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable int[] array) {
+        return array == null || array.length == 0;
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
new file mode 100644
index 0000000..14917f3
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 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.art.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+/** @hide */
+public class AndroidPackageApi {
+    private final Object mPkg;
+
+    AndroidPackageApi(@NonNull Object pkg) {
+        mPkg = pkg;
+    }
+
+    @NonNull
+    public String getBaseApkPath() {
+        try {
+            return (String) mPkg.getClass().getMethod("getBaseApkPath").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean isHasCode() {
+        try {
+            return (boolean) mPkg.getClass().getMethod("isHasCode").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String[] getSplitNames() {
+        try {
+            return (String[]) mPkg.getClass().getMethod("getSplitNames").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String[] getSplitCodePaths() {
+        try {
+            return (String[]) mPkg.getClass().getMethod("getSplitCodePaths").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public int[] getSplitFlags() {
+        try {
+            Class<?> parsingPackageImplClass =
+                    Class.forName("com.android.server.pm.pkg.parsing.ParsingPackageImpl");
+            return (int[]) parsingPackageImplClass.getMethod("getSplitFlags").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public String getClassLoaderName() {
+        try {
+            return (String) mPkg.getClass().getMethod("getClassLoaderName").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String[] getSplitClassLoaderNames() {
+        try {
+            return (String[]) mPkg.getClass().getMethod("getSplitClassLoaderNames").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public SparseArray<int[]> getSplitDependencies() {
+        try {
+            return (SparseArray<int[]>) mPkg.getClass()
+                    .getMethod("getSplitDependencies")
+                    .invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean isIsolatedSplitLoading() {
+        try {
+            return (boolean) mPkg.getClass().getMethod("isIsolatedSplitLoading").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
new file mode 100644
index 0000000..3bc21d1
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 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.art.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** @hide */
+public class PackageState {
+    private final Object mPkgState;
+
+    PackageState(@NonNull Object pkgState) {
+        mPkgState = pkgState;
+    }
+
+    @Nullable
+    public AndroidPackageApi getAndroidPackage() {
+        try {
+            Object pkg = mPkgState.getClass().getMethod("getAndroidPackage").invoke(mPkgState);
+            return pkg != null ? new AndroidPackageApi(pkg) : null;
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String getPackageName() {
+        try {
+            return (String) mPkgState.getClass().getMethod("getPackageName").invoke(mPkgState);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public List<SharedLibraryInfo> getUsesLibraryInfos() {
+        try {
+            Object packageStateUnserialized =
+                    mPkgState.getClass().getMethod("getTransientState").invoke(mPkgState);
+            var list = (List<?>) packageStateUnserialized.getClass()
+                               .getMethod("getUsesLibraryInfos")
+                               .invoke(packageStateUnserialized);
+            return list.stream()
+                    .map(obj -> new SharedLibraryInfo(obj))
+                    .collect(Collectors.toList());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/README.md b/libartservice/service/java/com/android/server/art/wrapper/README.md
new file mode 100644
index 0000000..518bba9
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/README.md
@@ -0,0 +1,10 @@
+This folder contains temporary wrappers that access system server internal
+classes using reflection. Having the wrappers is the workaround for the current
+time being where required system APIs are not finalized. The classes and methods
+correspond to system APIs planned to be exposed.
+
+The mappings are:
+
+- `AndroidPackageApi`: `com.android.server.pm.pkg.AndroidPackageApi`
+- `PackageState`: `com.android.server.pm.pkg.PackageState`
+- `SharedLibraryInfo`: `android.content.pm.SharedLibraryInfo`
diff --git a/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java b/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java
new file mode 100644
index 0000000..f2bde16
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 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.art.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** @hide */
+public class SharedLibraryInfo {
+    private final Object mInfo;
+
+    SharedLibraryInfo(@NonNull Object info) {
+        mInfo = info;
+    }
+
+    @NonNull
+    public List<String> getAllCodePaths() {
+        try {
+            return (List<String>) mInfo.getClass().getMethod("getAllCodePaths").invoke(mInfo);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public List<SharedLibraryInfo> getDependencies() {
+        try {
+            var list = (List<?>) mInfo.getClass().getMethod("getDependencies").invoke(mInfo);
+            if (list == null) {
+                return null;
+            }
+            return list.stream()
+                    .map(obj -> new SharedLibraryInfo(obj))
+                    .collect(Collectors.toList());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java
new file mode 100644
index 0000000..7adade3
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2022 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.art;
+
+import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
+import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.ApplicationInfo;
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.PrimaryDexUtils;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.art.wrapper.SharedLibraryInfo;
+
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.DexClassLoader;
+import dalvik.system.PathClassLoader;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(MockitoJUnitRunner.class)
+public class PrimaryDexUtilsTest {
+    @Before
+    public void setUp() {}
+
+    @Test
+    public void testGetDexInfo() {
+        List<PrimaryDexInfo> infos =
+                PrimaryDexUtils.getDexInfo(createPackage(false /* isIsolatedSplitLoading */));
+        checkBasicInfo(infos);
+    }
+
+    @Test
+    public void testGetDetailedDexInfo() {
+        List<DetailedPrimaryDexInfo> infos = PrimaryDexUtils.getDetailedDexInfo(
+                createPackageState(), createPackage(false /* isIsolatedSplitLoading */));
+        checkBasicInfo(infos);
+
+        String sharedLibrariesContext = "{"
+                + "PCL[library_2.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}#"
+                + "PCL[library_3.jar]#"
+                + "PCL[library_4.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}"
+                + "}";
+
+        assertThat(infos.get(0).classLoaderContext()).isEqualTo("PCL[]" + sharedLibrariesContext);
+        assertThat(infos.get(1).classLoaderContext())
+                .isEqualTo("PCL[base.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(2).classLoaderContext()).isEqualTo(null);
+        assertThat(infos.get(3).classLoaderContext())
+                .isEqualTo("PCL[base.apk:split_0.apk:split_1.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(4).classLoaderContext())
+                .isEqualTo("PCL[base.apk:split_0.apk:split_1.apk:split_2.apk]"
+                        + sharedLibrariesContext);
+    }
+
+    @Test
+    public void testGetDetailedDexInfoIsolated() {
+        List<DetailedPrimaryDexInfo> infos = PrimaryDexUtils.getDetailedDexInfo(
+                createPackageState(), createPackage(true /* isIsolatedSplitLoading */));
+        checkBasicInfo(infos);
+
+        String sharedLibrariesContext = "{"
+                + "PCL[library_2.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}#"
+                + "PCL[library_3.jar]#"
+                + "PCL[library_4.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}"
+                + "}";
+
+        assertThat(infos.get(0).classLoaderContext()).isEqualTo("PCL[]" + sharedLibrariesContext);
+        assertThat(infos.get(1).classLoaderContext())
+                .isEqualTo("PCL[];DLC[split_2.apk];PCL[base.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(2).classLoaderContext()).isEqualTo(null);
+        assertThat(infos.get(3).classLoaderContext())
+                .isEqualTo("DLC[];PCL[base.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(4).classLoaderContext()).isEqualTo("PCL[]");
+        assertThat(infos.get(5).classLoaderContext()).isEqualTo("PCL[];PCL[split_3.apk]");
+    }
+
+    private <T extends PrimaryDexInfo> void checkBasicInfo(List<T> infos) {
+        assertThat(infos.get(0).dexPath()).isEqualTo("/data/app/foo/base.apk");
+        assertThat(infos.get(0).hasCode()).isTrue();
+        assertThat(infos.get(0).isBaseApk()).isTrue();
+        assertThat(infos.get(0).splitIndex()).isEqualTo(-1);
+        assertThat(infos.get(0).splitName()).isNull();
+
+        assertThat(infos.get(1).dexPath()).isEqualTo("/data/app/foo/split_0.apk");
+        assertThat(infos.get(1).hasCode()).isTrue();
+        assertThat(infos.get(1).isBaseApk()).isFalse();
+        assertThat(infos.get(1).splitIndex()).isEqualTo(0);
+        assertThat(infos.get(1).splitName()).isEqualTo("split_0");
+
+        assertThat(infos.get(2).dexPath()).isEqualTo("/data/app/foo/split_1.apk");
+        assertThat(infos.get(2).hasCode()).isFalse();
+        assertThat(infos.get(2).isBaseApk()).isFalse();
+        assertThat(infos.get(2).splitIndex()).isEqualTo(1);
+        assertThat(infos.get(2).splitName()).isEqualTo("split_1");
+
+        assertThat(infos.get(3).dexPath()).isEqualTo("/data/app/foo/split_2.apk");
+        assertThat(infos.get(3).hasCode()).isTrue();
+        assertThat(infos.get(3).isBaseApk()).isFalse();
+        assertThat(infos.get(3).splitIndex()).isEqualTo(2);
+        assertThat(infos.get(3).splitName()).isEqualTo("split_2");
+
+        assertThat(infos.get(4).dexPath()).isEqualTo("/data/app/foo/split_3.apk");
+        assertThat(infos.get(4).hasCode()).isTrue();
+        assertThat(infos.get(4).isBaseApk()).isFalse();
+        assertThat(infos.get(4).splitIndex()).isEqualTo(3);
+        assertThat(infos.get(4).splitName()).isEqualTo("split_3");
+
+        assertThat(infos.get(5).dexPath()).isEqualTo("/data/app/foo/split_4.apk");
+        assertThat(infos.get(5).hasCode()).isTrue();
+        assertThat(infos.get(5).isBaseApk()).isFalse();
+        assertThat(infos.get(5).splitIndex()).isEqualTo(4);
+        assertThat(infos.get(5).splitName()).isEqualTo("split_4");
+    }
+
+    private AndroidPackageApi createPackage(boolean isIsolatedSplitLoading) {
+        AndroidPackageApi pkg = mock(AndroidPackageApi.class);
+
+        when(pkg.getBaseApkPath()).thenReturn("/data/app/foo/base.apk");
+        when(pkg.isHasCode()).thenReturn(true);
+        when(pkg.getClassLoaderName()).thenReturn(PathClassLoader.class.getName());
+
+        when(pkg.getSplitNames())
+                .thenReturn(new String[] {"split_0", "split_1", "split_2", "split_3", "split_4"});
+        when(pkg.getSplitCodePaths())
+                .thenReturn(new String[] {
+                        "/data/app/foo/split_0.apk",
+                        "/data/app/foo/split_1.apk",
+                        "/data/app/foo/split_2.apk",
+                        "/data/app/foo/split_3.apk",
+                        "/data/app/foo/split_4.apk",
+                });
+        when(pkg.getSplitFlags())
+                .thenReturn(new int[] {
+                        ApplicationInfo.FLAG_HAS_CODE,
+                        0,
+                        ApplicationInfo.FLAG_HAS_CODE,
+                        ApplicationInfo.FLAG_HAS_CODE,
+                        ApplicationInfo.FLAG_HAS_CODE,
+                });
+
+        if (isIsolatedSplitLoading) {
+            // split_0: PCL(PathClassLoader), depends on split_2.
+            // split_1: no code.
+            // split_2: DLC(DelegateLastClassLoader), depends on base.
+            // split_3: PCL(DexClassLoader), no dependency.
+            // split_4: PCL(null), depends on split_3.
+            when(pkg.isIsolatedSplitLoading()).thenReturn(true);
+            when(pkg.getSplitClassLoaderNames())
+                    .thenReturn(new String[] {
+                            PathClassLoader.class.getName(),
+                            null,
+                            DelegateLastClassLoader.class.getName(),
+                            DexClassLoader.class.getName(),
+                            null,
+                    });
+            SparseArray<int[]> splitDependencies = new SparseArray<>();
+            splitDependencies.set(1, new int[] {3});
+            splitDependencies.set(3, new int[] {0});
+            splitDependencies.set(5, new int[] {4});
+            when(pkg.getSplitDependencies()).thenReturn(splitDependencies);
+        } else {
+            when(pkg.isIsolatedSplitLoading()).thenReturn(false);
+        }
+
+        return pkg;
+    }
+
+    private PackageState createPackageState() {
+        PackageState pkgState = mock(PackageState.class);
+
+        when(pkgState.getPackageName()).thenReturn("com.example.foo");
+
+        // Base depends on library 2, 3, 4.
+        // Library 2, 4 depends on library 1.
+        List<SharedLibraryInfo> usesLibraryInfos = new ArrayList<>();
+
+        SharedLibraryInfo library1 = mock(SharedLibraryInfo.class);
+        when(library1.getAllCodePaths())
+                .thenReturn(List.of("library_1_dex_1.jar", "library_1_dex_2.jar"));
+        when(library1.getDependencies()).thenReturn(null);
+
+        SharedLibraryInfo library2 = mock(SharedLibraryInfo.class);
+        when(library2.getAllCodePaths()).thenReturn(List.of("library_2.jar"));
+        when(library2.getDependencies()).thenReturn(List.of(library1));
+        usesLibraryInfos.add(library2);
+
+        SharedLibraryInfo library3 = mock(SharedLibraryInfo.class);
+        when(library3.getAllCodePaths()).thenReturn(List.of("library_3.jar"));
+        when(library3.getDependencies()).thenReturn(null);
+        usesLibraryInfos.add(library3);
+
+        SharedLibraryInfo library4 = mock(SharedLibraryInfo.class);
+        when(library4.getAllCodePaths()).thenReturn(List.of("library_4.jar"));
+        when(library4.getDependencies()).thenReturn(List.of(library1));
+        usesLibraryInfos.add(library4);
+
+        when(pkgState.getUsesLibraryInfos()).thenReturn(usesLibraryInfos);
+
+        return pkgState;
+    }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
new file mode 100644
index 0000000..917a26e
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 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.art;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.Utils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(MockitoJUnitRunner.class)
+public class UtilsTest {
+    @Test
+    public void testCollectionIsEmptyTrue() {
+        assertThat(Utils.isEmpty(List.of())).isTrue();
+    }
+
+    @Test
+    public void testCollectionIsEmptyFalse() {
+        assertThat(Utils.isEmpty(List.of(1))).isFalse();
+    }
+
+    @Test
+    public void testSparseArrayIsEmptyTrue() {
+        assertThat(Utils.isEmpty(new SparseArray<Integer>())).isTrue();
+    }
+
+    @Test
+    public void testSparseArrayIsEmptyFalse() {
+        SparseArray<Integer> array = new SparseArray<>();
+        array.put(1, 1);
+        assertThat(Utils.isEmpty(array)).isFalse();
+    }
+
+    @Test
+    public void testArrayIsEmptyTrue() {
+        assertThat(Utils.isEmpty(new int[0])).isTrue();
+    }
+
+    @Test
+    public void testArrayIsEmptyFalse() {
+        assertThat(Utils.isEmpty(new int[] {1})).isFalse();
+    }
+}