From 3a94306ff2d19799eec7650450c22a96d9c579c8 Mon Sep 17 00:00:00 2001 From: Ivan Chiang Date: Wed, 21 Jun 2023 09:29:45 +0000 Subject: [PATCH 1/2] [pm] Add --no-profile option into install command Bug: 257532944 Test: adb install --no-profile foo.apk. adb shell pm help Change-Id: I8b7ad8f6ff6dac340b6658e68c3a37d8060edbf7 --- core/java/android/content/pm/PackageManager.java | 8 ++++++++ .../java/com/android/server/pm/InstallPackageHelper.java | 6 +++++- .../android/server/pm/PackageManagerShellCommand.java | 9 +++++++-- .../server/pm/pkg/parsing/ParsingPackageUtils.java | 9 ++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index e5468a58e4be6..4ce77e9f33f35 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1431,6 +1431,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 {} @@ -1645,6 +1646,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/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index e01e1d3f3edd1..45fc6ec985ab1 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..7fbd4a9495b01 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 @@ -240,6 +240,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 +256,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 {} From 8e23b094c88eb1336de95d2388894d381160fe6a Mon Sep 17 00:00:00 2001 From: Ivan Chiang Date: Wed, 21 Jun 2023 06:02:00 +0000 Subject: [PATCH 2/2] [pm] Extract the profiles from the apk in install stage When we parse the apk, if there is no flag to not extract the profiles, extract the profiles from the apk and create a ZIP archive includes them. And put the zip fie in the same directory with the apk Bug: 257532944 Test: 1. adb install foo.apk After installng the apk successfully, the .dm files exist in the same directory with the apk. 2. adb install --no-profile foo.apk. No .dm files exist in the same directory after the installation is completed. 3. adb install-multiple foo.apk foo.dm Don't extract the profiles again. 4. Test install-create case Change-Id: I02651e12adf4964754f8bb44fd19889ea94bea3c --- .../content/pm/dex/DexMetadataHelper.java | 82 +++++++++++++++++++ .../pm/pkg/parsing/ParsingPackageUtils.java | 22 +++++ 2 files changed, 104 insertions(+) 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/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index 7fbd4a9495b01..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; @@ -566,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,