diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index f6b05e2bc8268..4bc1eebc17f3c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1432,6 +1432,7 @@ public abstract class PackageManager { INSTALL_ALLOW_DOWNGRADE, INSTALL_STAGED, INSTALL_REQUEST_UPDATE_OWNERSHIP, + INSTALL_DONT_EXTRACT_BASELINE_PROFILES, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} @@ -1646,6 +1647,13 @@ public abstract class PackageManager { */ public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26; + /** + * Flag parameter for {@link PackageInstaller.SessionParams} to indicate that do not extract + * the baseline profiles when parsing the apk + * @hide + */ + public static final int INSTALL_DONT_EXTRACT_BASELINE_PROFILES = 1 << 27; + /** @hide */ @IntDef(flag = true, value = { DONT_KILL_APP, diff --git a/core/java/android/content/pm/dex/DexMetadataHelper.java b/core/java/android/content/pm/dex/DexMetadataHelper.java index e75aa065d3d33..3b53c25d1d8b3 100644 --- a/core/java/android/content/pm/dex/DexMetadataHelper.java +++ b/core/java/android/content/pm/dex/DexMetadataHelper.java @@ -23,6 +23,9 @@ import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.PackageLite; import android.content.pm.parsing.result.ParseInput; import android.content.pm.parsing.result.ParseResult; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.JsonReader; @@ -33,6 +36,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.security.VerityUtils; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -44,6 +48,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; /** * Helper class used to compute and validate the location of dex metadata files. @@ -62,6 +67,11 @@ public class DexMetadataHelper { private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; + private static final String PROFILE_FILE_NAME = "primary.prof"; + private static final String PROFILE_METADATA_FILE_NAME = "primary.profm"; + private static final String BASELINE_PROFILE_SOURCE_RELATIVE_PATH = "dexopt/baseline.prof"; + private static final String BASELINE_PROFILE_METADATA_RELATIVE_PATH = "dexopt/baseline.profm"; + private DexMetadataHelper() {} /** Return true if the given file is a dex metadata file. */ @@ -313,4 +323,76 @@ public class DexMetadataHelper { } } + /** + * Extract the baseline profiles from the assets directory in the apk. Then create a + * ZIP archive with the (.dm) file extension (e.g. foo.dm from foo.apk) to include the + * baseline profiles and put the DexMetadata file in the same directory with the apk. + * + * @param assetManager The {@link AssetManager} to use. + * @param apkPath The path of the apk + * @return {@code true} if the extraction is successful. Otherwise, return {@code false}. + * + * @see #buildDexMetadataPathForApk(String) + */ + public static boolean extractBaselineProfilesToDexMetadataFileFromApk(AssetManager assetManager, + String apkPath) { + if (!ApkLiteParseUtils.isApkPath(apkPath)) { + if (DEBUG) { + Log.d(TAG, "It is not an apk file: " + apkPath); + } + return false; + } + + // get the name of the DexMetadata file from the path of the apk + final File dmFile = new File(buildDexMetadataPathForApk(apkPath)); + boolean success = false; + + // load profile and profile metadata from assets directory in the apk + try (InputStream profileIs = openStreamFromAssets(assetManager, + BASELINE_PROFILE_SOURCE_RELATIVE_PATH); + InputStream profileMetadataIs = openStreamFromAssets(assetManager, + BASELINE_PROFILE_METADATA_RELATIVE_PATH)) { + // Create the zip archive file and write the baseline profiles into it. + try (FileOutputStream fos = new FileOutputStream(dmFile)) { + try (ZipOutputStream zipOs = new ZipOutputStream(fos)) { + zipOs.putNextEntry(new ZipEntry(PROFILE_FILE_NAME)); + zipOs.write(profileIs.readAllBytes()); + zipOs.closeEntry(); + + zipOs.putNextEntry(new ZipEntry(PROFILE_METADATA_FILE_NAME)); + zipOs.write(profileMetadataIs.readAllBytes()); + zipOs.closeEntry(); + success = true; + } + } + } catch (IOException e) { + if (DEBUG) { + Log.e(TAG, "Extract baseline profiles from apk failed: " + e.getMessage()); + } + } finally { + if (!success) { + if (dmFile.exists()) { + dmFile.delete(); + } + } + } + return success; + } + + /** + * Loads an {@link AutoCloseInputStream} from assets with the path. + * + * @param assetManager The {@link AssetManager} to use. + * @param path The source file's relative path. + * @return An AutoCloseInputStream in case the file was successfully read. + * @throws IOException If anything goes wrong while opening or reading the file. + */ + private static AutoCloseInputStream openStreamFromAssets(AssetManager assetManager, String path) + throws IOException { + AssetFileDescriptor descriptor = assetManager.openFd(path); + // Based on the java doc of AssetFileDescriptor#createInputStream, it will always return + // an AutoCloseInputStream. It should be fine we cast it from FileInputStream to + // AutoCloseInputStream here. + return (AutoCloseInputStream) descriptor.createInputStream(); + } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 58a85cfc19a74..7d4776285db08 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1077,6 +1077,8 @@ final class InstallPackageHelper { final boolean isApex = ((installFlags & PackageManager.INSTALL_APEX) != 0); final boolean isRollback = request.getInstallReason() == PackageManager.INSTALL_REASON_ROLLBACK; + final boolean extractProfile = + ((installFlags & PackageManager.INSTALL_DONT_EXTRACT_BASELINE_PROFILES) == 0); @PackageManagerService.ScanFlags int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE; if (request.isInstallMove()) { // moving a complete application; perform an initial scan on the new install location @@ -1112,7 +1114,9 @@ final class InstallPackageHelper { @ParsingPackageUtils.ParseFlags final int parseFlags = mPm.getDefParseFlags() | ParsingPackageUtils.PARSE_CHATTY | ParsingPackageUtils.PARSE_ENFORCE_CODE - | (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0); + | (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0) + | (extractProfile + ? ParsingPackageUtils.PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK : 0); Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage"); final ParsedPackage parsedPackage; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index e4692f19ada98..5cb480c0278f1 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -3436,6 +3436,10 @@ class PackageManagerShellCommand extends ShellCommand { sessionParams.installFlags |= PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; break; + case "--no-profile": + sessionParams.installFlags |= + PackageManager.INSTALL_DONT_EXTRACT_BASELINE_PROFILES; + break; default: throw new IllegalArgumentException("Unknown option " + opt); } @@ -4324,7 +4328,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--install-reason 0/1/2/3/4] [--originating-uri URI]"); pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); - pw.println(" [--enable-rollback]"); + pw.println(" [--enable-rollback] [--no-profile]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); pw.println(" [--apex] [--force-non-staged] [--staged-ready-timeout TIMEOUT]"); pw.println(" [PATH [SPLIT...]|-]"); @@ -4357,6 +4361,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --apex: install an .apex file, not an .apk"); pw.println(" --force-non-staged: force the installation to run under a non-staged"); pw.println(" session, which may complete without requiring a reboot"); + pw.println(" --no-profile: don't extract the profiles from the apk"); pw.println(" --staged-ready-timeout: By default, staged sessions wait " + DEFAULT_STAGED_READY_TIMEOUT_MS); pw.println(" milliseconds for pre-reboot verification to complete when"); @@ -4378,7 +4383,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]"); - pw.println(" [--multi-package] [--staged] [--update-ownership]"); + pw.println(" [--multi-package] [--staged] [--no-profile] [--update-ownership]"); pw.println(" Like \"install\", but starts an install session. Use \"install-write\""); pw.println(" to push data into the session, and \"install-commit\" to finish."); pw.println(""); diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index e2cb87e72c7a8..d737b1c6bfa6d 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -50,6 +50,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.Property; import android.content.pm.Signature; import android.content.pm.SigningDetails; +import android.content.pm.dex.DexMetadataHelper; import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.FrameworkParsingPackageUtils; import android.content.pm.parsing.PackageLite; @@ -73,6 +74,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.ext.SdkExtensions; +import android.os.incremental.IncrementalManager; import android.permission.PermissionManager; import android.text.TextUtils; import android.util.ArrayMap; @@ -240,6 +242,11 @@ public class ParsingPackageUtils { public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7; public static final int PARSE_APK_IN_APEX = 1 << 9; + /** + * This flag is to determine whether to extract the baseline profiles from the apk or not. + */ + public static final int PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK = 1 << 10; + public static final int PARSE_CHATTY = 1 << 31; /** The total maximum number of activities, services, providers and activity-aliases */ @@ -251,14 +258,16 @@ public class ParsingPackageUtils { private static final int MAX_PERMISSION_NAME_LENGTH = 512; @IntDef(flag = true, prefix = { "PARSE_" }, value = { + PARSE_APK_IN_APEX, PARSE_CHATTY, PARSE_COLLECT_CERTIFICATES, PARSE_ENFORCE_CODE, PARSE_EXTERNAL_STORAGE, + PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK, + PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY, PARSE_IGNORE_PROCESSES, PARSE_IS_SYSTEM_DIR, PARSE_MUST_BE_APK, - PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY, }) @Retention(RetentionPolicy.SOURCE) public @interface ParseFlags {} @@ -559,6 +568,26 @@ public class ParsingPackageUtils { pkg.setSigningDetails(SigningDetails.UNKNOWN); } + // 1. The apkFile is an apk file + // 2. The flags include PARSE_EXTRACT_PROFILE_FROM_APK + // 3. The apk patch is NOT an incremental path + // 4. If the .dm file exists in the current apk directory, it means the caller + // prepares the .dm file. Don't extract the profiles from the apk again. + if (ApkLiteParseUtils.isApkFile(apkFile) + && (flags & PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK) != 0 + && !IncrementalManager.isIncrementalPath(apkPath) + && DexMetadataHelper.findDexMetadataForFile(apkFile) == null) { + // Extract the baseline profiles from the apk if the profiles exist in the assets + // directory in the apk. + boolean extractedResult = + DexMetadataHelper.extractBaselineProfilesToDexMetadataFileFromApk(assets, + apkPath); + + if (DEBUG_JAR) { + Slog.d(TAG, "Extract profiles " + (extractedResult ? "success" : "fail")); + } + } + return input.success(pkg); } catch (Exception e) { return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,