| /* |
| * 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.os.Build; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.text.TextUtils; |
| |
| import androidx.annotation.RequiresApi; |
| |
| import com.android.internal.annotations.Immutable; |
| import com.android.server.art.model.DetailedDexInfo; |
| import com.android.server.pm.pkg.AndroidPackage; |
| import com.android.server.pm.pkg.AndroidPackageSplit; |
| import com.android.server.pm.pkg.PackageState; |
| import com.android.server.pm.pkg.PackageUserState; |
| import com.android.server.pm.pkg.SharedLibrary; |
| |
| 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.Objects; |
| import java.util.function.Predicate; |
| import java.util.stream.Collectors; |
| |
| /** @hide */ |
| @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) |
| public class PrimaryDexUtils { |
| public static final String PROFILE_PRIMARY = "primary"; |
| private static final String SHARED_LIBRARY_LOADER_TYPE = PathClassLoader.class.getName(); |
| |
| /** |
| * Returns the basic information about all primary dex files belonging to the package, excluding |
| * files that have no code. |
| */ |
| @NonNull |
| public static List<PrimaryDexInfo> getDexInfo(@NonNull AndroidPackage pkg) { |
| return getDexInfoImpl(pkg) |
| .stream() |
| .map(builder -> builder.build()) |
| .filter(info -> info.hasCode()) |
| .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 AndroidPackage pkg) { |
| return getDetailedDexInfoImpl(pkgState, pkg) |
| .stream() |
| .map(builder -> builder.buildDetailed()) |
| .filter(info -> info.hasCode()) |
| .collect(Collectors.toList()); |
| } |
| |
| /** Returns the basic information about a dex file specified by {@code splitName}. */ |
| @NonNull |
| public static PrimaryDexInfo getDexInfoBySplitName( |
| @NonNull AndroidPackage pkg, @Nullable String splitName) { |
| PrimaryDexInfo dexInfo = |
| findDexInfo(pkg, info -> Objects.equals(info.splitName(), splitName)); |
| if (dexInfo == null) { |
| throw new IllegalArgumentException(String.format("Split '%s' not found", splitName)); |
| } |
| return dexInfo; |
| } |
| |
| /** |
| * Returns the basic information about the first dex file matching {@code predicate}, or null if |
| * not found. |
| */ |
| @Nullable |
| public static PrimaryDexInfo findDexInfo( |
| @NonNull AndroidPackage pkg, Predicate<PrimaryDexInfo> predicate) { |
| return getDexInfoImpl(pkg) |
| .stream() |
| .map(builder -> builder.build()) |
| .filter(predicate) |
| .findFirst() |
| .orElse(null); |
| } |
| |
| @NonNull |
| private static List<PrimaryDexInfoBuilder> getDexInfoImpl(@NonNull AndroidPackage pkg) { |
| List<PrimaryDexInfoBuilder> dexInfos = new ArrayList<>(); |
| |
| for (var split : pkg.getSplits()) { |
| dexInfos.add(new PrimaryDexInfoBuilder(split)); |
| } |
| |
| return dexInfos; |
| } |
| |
| @NonNull |
| private static List<PrimaryDexInfoBuilder> getDetailedDexInfoImpl( |
| @NonNull PackageState pkgState, @NonNull AndroidPackage pkg) { |
| List<PrimaryDexInfoBuilder> dexInfos = getDexInfoImpl(pkg); |
| |
| PrimaryDexInfoBuilder baseApk = dexInfos.get(0); |
| baseApk.mClassLoaderName = baseApk.mSplit.getClassLoaderName(); |
| File baseDexFile = new File(baseApk.mSplit.getPath()); |
| baseApk.mRelativeDexPath = baseDexFile.getName(); |
| |
| // Shared libraries are the dependencies of the base APK. |
| baseApk.mSharedLibrariesContext = |
| encodeSharedLibraries(pkgState.getSharedLibraryDependencies()); |
| |
| boolean isIsolatedSplitLoading = isIsolatedSplitLoading(pkg); |
| |
| for (int i = 1; i < dexInfos.size(); i++) { |
| var dexInfoBuilder = dexInfos.get(i); |
| File splitDexFile = new File(dexInfoBuilder.mSplit.getPath()); |
| if (!splitDexFile.getParent().equals(baseDexFile.getParent())) { |
| throw new IllegalStateException( |
| "Split APK and base APK are in different directories: " |
| + splitDexFile.getParent() + " != " + baseDexFile.getParent()); |
| } |
| dexInfoBuilder.mRelativeDexPath = splitDexFile.getName(); |
| if (isIsolatedSplitLoading && dexInfoBuilder.mSplit.isHasCode()) { |
| dexInfoBuilder.mClassLoaderName = dexInfoBuilder.mSplit.getClassLoaderName(); |
| |
| List<AndroidPackageSplit> dependencies = dexInfoBuilder.mSplit.getDependencies(); |
| 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. |
| AndroidPackageSplit dependency = dependencies.get(0); |
| for (var dexInfo : dexInfos) { |
| if (Objects.equals(dexInfo.mSplit, dependency)) { |
| dexInfoBuilder.mSplitDependency = dexInfo; |
| break; |
| } |
| } |
| |
| if (dexInfoBuilder.mSplitDependency == null) { |
| throw new IllegalStateException( |
| "Split dependency not found for " + splitDexFile); |
| } |
| } |
| } |
| } |
| |
| 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.mSplit.isHasCode()) { |
| 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.mSplit.isHasCode()) { |
| 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<SharedLibrary> sharedLibraries) { |
| if (Utils.isEmpty(sharedLibraries)) { |
| return null; |
| } |
| return sharedLibraries.stream() |
| .filter(library -> !library.isNative()) |
| .map(library |
| -> encodeClassLoader(SHARED_LIBRARY_LOADER_TYPE, library.getAllCodePaths(), |
| null /* parentContext */, |
| encodeSharedLibraries(library.getDependencies()))) |
| .collect(Collectors.joining("#", "{", "}")); |
| } |
| |
| public static boolean isIsolatedSplitLoading(@NonNull AndroidPackage pkg) { |
| return pkg.isIsolatedSplitLoading() && pkg.getSplits().size() > 1; |
| } |
| |
| @NonNull |
| public static ProfilePath buildRefProfilePath( |
| @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) { |
| String profileName = getProfileName(dexInfo.splitName()); |
| return AidlUtils.buildProfilePathForPrimaryRef(pkgState.getPackageName(), profileName); |
| } |
| |
| @NonNull |
| public static OutputProfile buildOutputProfile(@NonNull PackageState pkgState, |
| @NonNull PrimaryDexInfo dexInfo, int uid, int gid, boolean isPublic) { |
| String profileName = getProfileName(dexInfo.splitName()); |
| return AidlUtils.buildOutputProfileForPrimary( |
| pkgState.getPackageName(), profileName, uid, gid, isPublic); |
| } |
| |
| @NonNull |
| public static List<ProfilePath> getCurProfiles(@NonNull UserManager userManager, |
| @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) { |
| return getCurProfiles( |
| userManager.getUserHandles(true /* excludeDying */), pkgState, dexInfo); |
| } |
| |
| @NonNull |
| public static List<ProfilePath> getCurProfiles(@NonNull List<UserHandle> userHandles, |
| @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) { |
| List<ProfilePath> profiles = new ArrayList<>(); |
| for (UserHandle handle : userHandles) { |
| int userId = handle.getIdentifier(); |
| PackageUserState userState = pkgState.getStateForUser(handle); |
| if (userState.isInstalled()) { |
| profiles.add(AidlUtils.buildProfilePathForPrimaryCur( |
| userId, pkgState.getPackageName(), getProfileName(dexInfo.splitName()))); |
| } |
| } |
| return profiles; |
| } |
| |
| @NonNull |
| public static String getProfileName(@Nullable String splitName) { |
| return splitName == null ? PROFILE_PRIMARY : splitName + ".split"; |
| } |
| |
| @NonNull |
| public static List<ProfilePath> getExternalProfiles(@NonNull PrimaryDexInfo dexInfo) { |
| return List.of(AidlUtils.buildProfilePathForPrebuilt(dexInfo.dexPath()), |
| AidlUtils.buildProfilePathForDm(dexInfo.dexPath())); |
| } |
| |
| /** Basic information about a primary dex file (either the base APK or a split APK). */ |
| @Immutable |
| public static class PrimaryDexInfo { |
| private final @NonNull AndroidPackageSplit mSplit; |
| |
| PrimaryDexInfo(@NonNull AndroidPackageSplit split) { |
| mSplit = split; |
| } |
| |
| /** The path to the dex file. */ |
| public @NonNull String dexPath() { |
| return mSplit.getPath(); |
| } |
| |
| /** True if the dex file has code. */ |
| public boolean hasCode() { |
| return mSplit.isHasCode(); |
| } |
| |
| /** The name of the split, or null for base APK. */ |
| public @Nullable String splitName() { |
| return mSplit.getName(); |
| } |
| } |
| |
| /** |
| * 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 implements DetailedDexInfo { |
| private final @Nullable String mClassLoaderContext; |
| |
| DetailedPrimaryDexInfo( |
| @NonNull AndroidPackageSplit split, @Nullable String classLoaderContext) { |
| super(split); |
| 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 AndroidPackageSplit mSplit; |
| @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 AndroidPackageSplit split) { |
| mSplit = split; |
| } |
| |
| PrimaryDexInfo build() { |
| return new PrimaryDexInfo(mSplit); |
| } |
| |
| DetailedPrimaryDexInfo buildDetailed() { |
| return new DetailedPrimaryDexInfo(mSplit, mClassLoaderContext); |
| } |
| } |
| } |