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();
+ }
+}