| /* |
| * 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); |
| } |
| } |
| } |