diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 4b883cd4c17bc..082ede2641ea1 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2369,6 +2369,8 @@ public class PackageInstaller { public int requireUserAction = USER_ACTION_UNSPECIFIED; /** {@hide} */ public boolean applicationEnabledSettingPersistent = false; + /** {@hide} */ + public int developmentInstallFlags = 0; private final ArrayMap mPermissionStates; @@ -2418,6 +2420,7 @@ public class PackageInstaller { requireUserAction = source.readInt(); packageSource = source.readInt(); applicationEnabledSettingPersistent = source.readBoolean(); + developmentInstallFlags = source.readInt(); } /** {@hide} */ @@ -2449,6 +2452,7 @@ public class PackageInstaller { ret.requireUserAction = requireUserAction; ret.packageSource = packageSource; ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; + ret.developmentInstallFlags = developmentInstallFlags; return ret; } @@ -3113,6 +3117,7 @@ public class PackageInstaller { pw.printPair("rollbackDataPolicy", rollbackDataPolicy); pw.printPair("applicationEnabledSettingPersistent", applicationEnabledSettingPersistent); + pw.printHexPair("developmentInstallFlags", developmentInstallFlags); pw.println(); } @@ -3154,6 +3159,7 @@ public class PackageInstaller { dest.writeInt(requireUserAction); dest.writeInt(packageSource); dest.writeBoolean(applicationEnabledSettingPersistent); + dest.writeInt(developmentInstallFlags); } public static final Parcelable.Creator diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 03799306955ac..7128b126c8ec5 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1453,6 +1453,16 @@ public abstract class PackageManager { @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} + /** + * Install flags that can only be used in development workflows (e.g. {@code adb install}). + * @hide + */ + @IntDef(flag = true, prefix = { "INSTALL_DEVELOPMENT_" }, value = { + INSTALL_DEVELOPMENT_FORCE_NON_STAGED_APEX_UPDATE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DevelopmentInstallFlags {} + /** * Flag parameter for {@link #installPackage} to indicate that you want to * replace an already installed package, if one exists. @@ -1663,6 +1673,14 @@ public abstract class PackageManager { */ public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26; + /** + * Flag parameter for {@link #installPackage} to force a non-staged update of an APEX. This is + * a development-only feature and should not be used on end user devices. + * + * @hide + */ + public static final int INSTALL_DEVELOPMENT_FORCE_NON_STAGED_APEX_UPDATE = 1; + /** @hide */ @IntDef(flag = true, value = { DONT_KILL_APP, diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index 5e62b56c7bcd4..42be07b3e3ba2 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -363,9 +363,13 @@ public abstract class ApexManager { /** * Performs a non-staged install of the given {@code apexFile}. * + * If {@code force} is {@code true}, then update is forced even for APEXes that do not support + * non-staged update. This feature is only available on debuggable builds to improve development + * velocity of the teams that have their code packaged in an APEX. + * * @return {@code ApeInfo} about the newly installed APEX package. */ - abstract ApexInfo installPackage(File apexFile) throws PackageManagerException; + abstract ApexInfo installPackage(File apexFile, boolean force) throws PackageManagerException; /** * Get a list of apex system services implemented in an apex. @@ -910,10 +914,11 @@ public abstract class ApexManager { } @Override - ApexInfo installPackage(File apexFile) + ApexInfo installPackage(File apexFile, boolean force) throws PackageManagerException { try { - return waitForApexService().installAndActivatePackage(apexFile.getAbsolutePath()); + return waitForApexService().installAndActivatePackage(apexFile.getAbsolutePath(), + force); } catch (RemoteException e) { throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, "apexservice not available"); @@ -1170,7 +1175,7 @@ public abstract class ApexManager { } @Override - ApexInfo installPackage(File apexFile) { + ApexInfo installPackage(File apexFile, boolean force) { throw new UnsupportedOperationException("APEX updates are not supported"); } diff --git a/services/core/java/com/android/server/pm/InstallArgs.java b/services/core/java/com/android/server/pm/InstallArgs.java index 6de7f07b78bb1..dd96a2b84a978 100644 --- a/services/core/java/com/android/server/pm/InstallArgs.java +++ b/services/core/java/com/android/server/pm/InstallArgs.java @@ -43,6 +43,7 @@ final class InstallArgs { final IPackageInstallObserver2 mObserver; // Always refers to PackageManager flags only final int mInstallFlags; + final int mDevelopmentInstallFlags; @NonNull final InstallSource mInstallSource; final String mVolumeUuid; @@ -69,8 +70,8 @@ final class InstallArgs { @Nullable String[] mInstructionSets; InstallArgs(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer, - int installFlags, InstallSource installSource, String volumeUuid, - UserHandle user, String[] instructionSets, String abiOverride, + int installFlags, int developmentInstallFlags, InstallSource installSource, + String volumeUuid, UserHandle user, String[] instructionSets, String abiOverride, @NonNull ArrayMap permissionStates, List allowlistedRestrictedPermissions, int autoRevokePermissionsMode, String traceMethod, int traceCookie, @@ -80,6 +81,7 @@ final class InstallArgs { mOriginInfo = originInfo; mMoveInfo = moveInfo; mInstallFlags = installFlags; + mDevelopmentInstallFlags = developmentInstallFlags; mObserver = observer; mInstallSource = Preconditions.checkNotNull(installSource); mVolumeUuid = volumeUuid; @@ -105,7 +107,7 @@ final class InstallArgs { * when cleaning up old installs, or used as a move source. */ InstallArgs(String codePath, String[] instructionSets) { - this(OriginInfo.fromNothing(), null, null, 0, InstallSource.EMPTY, null, null, + this(OriginInfo.fromNothing(), null, null, 0, 0, InstallSource.EMPTY, null, null, instructionSets, null, new ArrayMap<>(), null, MODE_DEFAULT, null, 0, SigningDetails.UNKNOWN, PackageManager.INSTALL_REASON_UNKNOWN, PackageManager.INSTALL_SCENARIO_DEFAULT, false, DataLoaderType.NONE, diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 34648740d54c5..6fc14e8145250 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -134,12 +134,13 @@ final class InstallRequest { InstallRequest(InstallingSession params) { mUserId = params.getUser().getIdentifier(); mInstallArgs = new InstallArgs(params.mOriginInfo, params.mMoveInfo, params.mObserver, - params.mInstallFlags, params.mInstallSource, params.mVolumeUuid, - params.getUser(), null /*instructionSets*/, params.mPackageAbiOverride, - params.mPermissionStates, params.mAllowlistedRestrictedPermissions, - params.mAutoRevokePermissionsMode, params.mTraceMethod, params.mTraceCookie, - params.mSigningDetails, params.mInstallReason, params.mInstallScenario, - params.mForceQueryableOverride, params.mDataLoaderType, params.mPackageSource, + params.mInstallFlags, params.mDevelopmentInstallFlags, params.mInstallSource, + params.mVolumeUuid, params.getUser(), null /*instructionSets*/, + params.mPackageAbiOverride, params.mPermissionStates, + params.mAllowlistedRestrictedPermissions, params.mAutoRevokePermissionsMode, + params.mTraceMethod, params.mTraceCookie, params.mSigningDetails, + params.mInstallReason, params.mInstallScenario, params.mForceQueryableOverride, + params.mDataLoaderType, params.mPackageSource, params.mApplicationEnabledSettingPersistent); mPackageMetrics = new PackageMetrics(this); mIsInstallInherit = params.mIsInherit; @@ -286,6 +287,10 @@ final class InstallRequest { return mInstallArgs == null ? 0 : mInstallArgs.mInstallFlags; } + public int getDevelopmentInstallFlags() { + return mInstallArgs == null ? 0 : mInstallArgs.mDevelopmentInstallFlags; + } + public int getInstallReason() { return mInstallArgs == null ? INSTALL_REASON_UNKNOWN : mInstallArgs.mInstallReason; } diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java index 35862db3d3db2..30a23bfd06413 100644 --- a/services/core/java/com/android/server/pm/InstallingSession.java +++ b/services/core/java/com/android/server/pm/InstallingSession.java @@ -68,6 +68,7 @@ class InstallingSession { final MoveInfo mMoveInfo; final IPackageInstallObserver2 mObserver; int mInstallFlags; + int mDevelopmentInstallFlags; @NonNull final InstallSource mInstallSource; final String mVolumeUuid; @@ -102,8 +103,8 @@ class InstallingSession { // For move install InstallingSession(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer, - int installFlags, InstallSource installSource, String volumeUuid, - UserHandle user, String packageAbiOverride, int packageSource, + int installFlags, int developmentInstallFlags, InstallSource installSource, + String volumeUuid, UserHandle user, String packageAbiOverride, int packageSource, PackageLite packageLite, PackageManagerService pm) { mPm = pm; mUser = user; @@ -113,6 +114,7 @@ class InstallingSession { mMoveInfo = moveInfo; mObserver = observer; mInstallFlags = installFlags; + mDevelopmentInstallFlags = developmentInstallFlags; mInstallSource = Preconditions.checkNotNull(installSource); mVolumeUuid = volumeUuid; mPackageAbiOverride = packageAbiOverride; @@ -149,6 +151,7 @@ class InstallingSession { mInstallScenario = sessionParams.installScenario; mObserver = observer; mInstallFlags = sessionParams.installFlags; + mDevelopmentInstallFlags = sessionParams.developmentInstallFlags; mInstallSource = installSource; mVolumeUuid = sessionParams.volumeUuid; mPackageAbiOverride = sessionParams.abiOverride; @@ -592,6 +595,10 @@ class InstallingSession { "Only a non-staged install of a single APEX is supported"); } InstallRequest request = requests.get(0); + boolean force = + (request.getDevelopmentInstallFlags() + & PackageManager.INSTALL_DEVELOPMENT_FORCE_NON_STAGED_APEX_UPDATE) + != 0; try { // Should directory scanning logic be moved to ApexManager for better test coverage? final File dir = request.getOriginInfo().mResolvedFile; @@ -608,7 +615,7 @@ class InstallingSession { PackageManagerException.INTERNAL_ERROR_APEX_MORE_THAN_ONE_FILE); } try (PackageParser2 packageParser = mPm.mInjector.getScanningPackageParser()) { - ApexInfo apexInfo = mPm.mApexManager.installPackage(apexes[0]); + ApexInfo apexInfo = mPm.mApexManager.installPackage(apexes[0], force); // APEX has been handled successfully by apexd. Let's continue the install flow // so it will be scanned and registered with the system. // TODO(b/225756739): Improve atomicity of rebootless APEX install. diff --git a/services/core/java/com/android/server/pm/MovePackageHelper.java b/services/core/java/com/android/server/pm/MovePackageHelper.java index f55d1eb21d873..adee143691b17 100644 --- a/services/core/java/com/android/server/pm/MovePackageHelper.java +++ b/services/core/java/com/android/server/pm/MovePackageHelper.java @@ -305,8 +305,9 @@ public final class MovePackageHelper { new File(origin.mResolvedPath), /* flags */ 0); final PackageLite lite = ret.isSuccess() ? ret.getResult() : null; final InstallingSession installingSession = new InstallingSession(origin, move, - installObserver, installFlags, installSource, volumeUuid, user, packageAbiOverride, - PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, lite, mPm); + installObserver, installFlags, /* developmentInstallFlags= */ 0, installSource, + volumeUuid, user, packageAbiOverride, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, + lite, mPm); installingSession.movePackage(); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index a9115371413cb..178719f3c4ecb 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -709,6 +709,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements != PackageManager.PERMISSION_GRANTED) { params.installFlags &= ~PackageManager.INSTALL_ALLOW_TEST; } + + // developmentInstallFlags can ony be set by shell or root. + params.developmentInstallFlags = 0; } String originatingPackageName = null; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index e043c90f4f140..2a7ebfb75dde7 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -3211,6 +3211,13 @@ class PackageManagerShellCommand extends ShellCommand { // Set package source to other by default sessionParams.setPackageSource(PackageInstaller.PACKAGE_SOURCE_OTHER); + // Encodes one of the states: + // 1. Install request explicitly specified --staged, then value will be true. + // 2. Install request explicitly specified --non-staged, then value will be false. + // 3. Install request did not specify either --staged or --non-staged, then for APEX + // installs the value will be true, and for apk installs it will be false. + Boolean staged = null; + String opt; boolean replaceExisting = true; boolean forceNonStaged = false; @@ -3319,7 +3326,6 @@ class PackageManagerShellCommand extends ShellCommand { break; case "--apex": sessionParams.setInstallAsApex(); - sessionParams.setStaged(); break; case "--force-non-staged": forceNonStaged = true; @@ -3328,7 +3334,10 @@ class PackageManagerShellCommand extends ShellCommand { sessionParams.setMultiPackage(); break; case "--staged": - sessionParams.setStaged(); + staged = true; + break; + case "--non-staged": + staged = false; break; case "--force-queryable": sessionParams.setForceQueryable(); @@ -3363,11 +3372,18 @@ class PackageManagerShellCommand extends ShellCommand { throw new IllegalArgumentException("Unknown option " + opt); } } + if (staged == null) { + staged = (sessionParams.installFlags & PackageManager.INSTALL_APEX) != 0; + } if (replaceExisting) { sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; } if (forceNonStaged) { sessionParams.isStaged = false; + sessionParams.developmentInstallFlags |= + PackageManager.INSTALL_DEVELOPMENT_FORCE_NON_STAGED_APEX_UPDATE; + } else if (staged) { + sessionParams.setStaged(); } return params; } @@ -4231,7 +4247,8 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); pw.println(" [--enable-rollback]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); - pw.println(" [--apex] [--force-non-staged] [--staged-ready-timeout TIMEOUT]"); + pw.println(" [--apex] [--non-staged] [--force-non-staged]"); + pw.println(" [--staged-ready-timeout TIMEOUT]"); pw.println(" [PATH [SPLIT...]|-]"); pw.println(" Install an application. Must provide the apk data to install, either as"); pw.println(" file path(s) or '-' to read from stdin. Options are:"); @@ -4260,8 +4277,12 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --update-ownership: request the update ownership enforcement"); pw.println(" --force-uuid: force install on to disk volume with given UUID"); pw.println(" --apex: install an .apex file, not an .apk"); + pw.println(" --non-staged: explicitly set this installation to be non-staged."); + pw.println(" This flag is only useful for APEX installs that are implicitly"); + pw.println(" assumed to be staged."); 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(" session, which may complete without requiring a reboot. This will"); + pw.println(" force a rebootless update even for APEXes that don't support it"); 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"); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java index 5b0e2f3800c32..3a73dd9eb101f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java @@ -19,6 +19,7 @@ package com.android.server.pm; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; @@ -368,10 +369,11 @@ public class ApexManagerTest { File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex"); ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true, /* isFactory= */ false, finalApex); - when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo); + when(mApexService.installAndActivatePackage(anyString(), anyBoolean())).thenReturn( + newApexInfo); File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex"); - newApexInfo = mApexManager.installPackage(installedApex); + newApexInfo = mApexManager.installPackage(installedApex, /* force= */ false); var newPkg = mockParsePackage(mPackageParser2, newApexInfo); assertThat(newPkg.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath()); @@ -398,10 +400,11 @@ public class ApexManagerTest { File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex"); ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true, /* isFactory= */ false, finalApex); - when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo); + when(mApexService.installAndActivatePackage(anyString(), anyBoolean())).thenReturn( + newApexInfo); File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex"); - newApexInfo = mApexManager.installPackage(installedApex); + newApexInfo = mApexManager.installPackage(installedApex, /* force= */ false); var newPkg = mockParsePackage(mPackageParser2, newApexInfo); assertThat(newPkg.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath()); @@ -416,13 +419,13 @@ public class ApexManagerTest { @Test public void testInstallPackageBinderCallFails() throws Exception { - when(mApexService.installAndActivatePackage(anyString())).thenThrow( + when(mApexService.installAndActivatePackage(anyString(), anyBoolean())).thenThrow( new RuntimeException("install failed :(")); File installedApex = extractResource("test.apex_rebootless_v1", "test.rebootless_apex_v1.apex"); assertThrows(PackageManagerException.class, - () -> mApexManager.installPackage(installedApex)); + () -> mApexManager.installPackage(installedApex, /* force= */ false)); } @Test