From 44da627fd59fdb1d1f0e21186bc0bc67384ba630 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 13 Sep 2018 15:06:22 -0700 Subject: [PATCH] Adds new atomic install API This change adds the new atomic install system API to PackageInstaller and plumbs it through to PackageManager. It also adds support for committing multiple sessions via command line. Bug: 109941548 Test: Manually install 2 apps from command line Change-Id: I71d77026a55a40c76925e55e6956fb76efe16224 --- api/current.txt | 10 + .../content/pm/IPackageInstallerSession.aidl | 5 + .../android/content/pm/PackageInstaller.java | 146 ++++- .../android/content/pm/PackageManager.java | 5 + .../server/pm/PackageInstallerService.java | 164 +++--- .../server/pm/PackageInstallerSession.java | 514 ++++++++++++++---- .../server/pm/PackageManagerService.java | 403 ++++++++++---- .../server/pm/PackageManagerShellCommand.java | 72 ++- .../server/pm/PackageSessionProvider.java | 28 + 9 files changed, 1045 insertions(+), 302 deletions(-) create mode 100644 services/core/java/com/android/server/pm/PackageSessionProvider.java diff --git a/api/current.txt b/api/current.txt index dd4c781b6e7b4..fd3ada24ca910 100755 --- a/api/current.txt +++ b/api/current.txt @@ -11176,12 +11176,17 @@ package android.content.pm { public static class PackageInstaller.Session implements java.io.Closeable { method public void abandon(); + method public void addChildSessionId(int); method public void close(); method public void commit(android.content.IntentSender); method public void fsync(java.io.OutputStream) throws java.io.IOException; + method public int[] getChildSessionIds(); method public java.lang.String[] getNames() throws java.io.IOException; + method public int getParentSessionId(); + method public boolean isMultiPackage(); method public java.io.InputStream openRead(java.lang.String) throws java.io.IOException; method public java.io.OutputStream openWrite(java.lang.String, long, long) throws java.io.IOException; + method public void removeChildSessionId(int); method public void removeSplit(java.lang.String) throws java.io.IOException; method public void setStagingProgress(float); method public void transfer(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; @@ -11202,20 +11207,24 @@ package android.content.pm { method public android.graphics.Bitmap getAppIcon(); method public java.lang.CharSequence getAppLabel(); method public java.lang.String getAppPackageName(); + method public int[] getChildSessionIds(); method public int getInstallLocation(); method public int getInstallReason(); method public java.lang.String getInstallerPackageName(); method public int getMode(); method public int getOriginatingUid(); method public android.net.Uri getOriginatingUri(); + method public int getParentSessionId(); method public float getProgress(); method public android.net.Uri getReferrerUri(); method public int getSessionId(); method public long getSize(); method public boolean isActive(); + method public boolean isMultiPackage(); method public boolean isSealed(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int INVALID_ID = -1; // 0xffffffff } public static class PackageInstaller.SessionParams implements android.os.Parcelable { @@ -11226,6 +11235,7 @@ package android.content.pm { method public void setAppPackageName(java.lang.String); method public void setInstallLocation(int); method public void setInstallReason(int); + method public void setMultiPackage(); method public void setOriginatingUid(int); method public void setOriginatingUri(android.net.Uri); method public void setReferrerUri(android.net.Uri); diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 8fddb99b35a81..cef21f607e5f2 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -38,4 +38,9 @@ interface IPackageInstallerSession { void commit(in IntentSender statusReceiver, boolean forTransferred); void transfer(in String packageName); void abandon(); + boolean isMultiPackage(); + int[] getChildSessionIds(); + void addChildSessionId(in int sessionId); + void removeChildSessionId(in int sessionId); + int getParentSessionId(); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index e9cfa78deba69..38e1c4973cd97 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -365,12 +365,14 @@ public class PackageInstaller { */ public @NonNull Session openSession(int sessionId) throws IOException { try { - return new Session(mInstaller.openSession(sessionId)); + try { + return new Session(mInstaller.openSession(sessionId)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } catch (RuntimeException e) { ExceptionUtils.maybeUnwrapIOException(e); throw e; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); } } @@ -769,9 +771,18 @@ public class PackageInstaller { * If an APK included in this session is already defined by the existing * installation (for example, the same split name), the APK in this session * will replace the existing APK. + *

+ * In such a case that multiple packages need to be commited simultaneously, + * multiple sessions can be referenced by a single multi-package session. + * This session is created with no package name and calling + * {@link #setMultiPackage()} with {@code true}. The + * individual session IDs can be added with {@link #addChildSessionId(int)} + * and commit of the multi-package session will result in all child sessions + * being committed atomically. */ public static class Session implements Closeable { - private IPackageInstallerSession mSession; + /** {@hide} */ + protected final IPackageInstallerSession mSession; /** {@hide} */ public Session(IPackageInstallerSession session) { @@ -1080,6 +1091,71 @@ public class PackageInstaller { throw e.rethrowFromSystemServer(); } } + + /** + * @return {@code true} if this session will commit more than one package when it is + * committed. + */ + public boolean isMultiPackage() { + try { + return mSession.isMultiPackage(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @return the session ID of the multi-package session that this belongs to or + * {@link SessionInfo#INVALID_ID} if it does not belong to a multi-package session. + */ + public int getParentSessionId() { + try { + return mSession.getParentSessionId(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @return the set of session IDs that will be committed atomically when this session is + * committed if this is a multi-package session or null if none exist. + */ + @NonNull + public int[] getChildSessionIds() { + try { + return mSession.getChildSessionIds(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adds a session ID to the set of sessions that will be committed atomically + * when this session is committed. + * + * @param sessionId the session ID to add to this multi-package session. + */ + public void addChildSessionId(int sessionId) { + try { + mSession.addChildSessionId(sessionId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a session ID from the set of sessions that will be committed + * atomically when this session is committed. + * + * @param sessionId the session ID to remove from this multi-package session. + */ + public void removeChildSessionId(int sessionId) { + try { + mSession.removeChildSessionId(sessionId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -1149,6 +1225,8 @@ public class PackageInstaller { public String[] grantedRuntimePermissions; /** {@hide} */ public String installerPackageName; + /** {@hide} */ + public boolean isMultiPackage; /** * Construct parameters for a new package install session. @@ -1178,6 +1256,7 @@ public class PackageInstaller { volumeUuid = source.readString(); grantedRuntimePermissions = source.readStringArray(); installerPackageName = source.readString(); + isMultiPackage = source.readBoolean(); } /** @@ -1392,6 +1471,18 @@ public class PackageInstaller { this.installerPackageName = installerPackageName; } + /** + * Set this session to be the parent of a multi-package install. + * + * A multi-package install session contains no APKs and only references other install + * sessions via ID. When a multi-package session is committed, all of its children + * are committed to the system in an atomic manner. If any children fail to install, + * all of them do, including the multi-package session. + */ + public void setMultiPackage() { + this.isMultiPackage = true; + } + /** {@hide} */ public void dump(IndentingPrintWriter pw) { pw.printPair("mode", mode); @@ -1408,6 +1499,7 @@ public class PackageInstaller { pw.printPair("volumeUuid", volumeUuid); pw.printPair("grantedRuntimePermissions", grantedRuntimePermissions); pw.printPair("installerPackageName", installerPackageName); + pw.printPair("isMultiPackage", isMultiPackage); pw.println(); } @@ -1433,6 +1525,7 @@ public class PackageInstaller { dest.writeString(volumeUuid); dest.writeStringArray(grantedRuntimePermissions); dest.writeString(installerPackageName); + dest.writeBoolean(isMultiPackage); } public static final Parcelable.Creator @@ -1454,6 +1547,12 @@ public class PackageInstaller { */ public static class SessionInfo implements Parcelable { + /** + * A session ID that does not exist or is invalid. + */ + public static final int INVALID_ID = -1; + /** {@hide} */ + private static final int[] NO_SESSIONS = {}; /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public int sessionId; @@ -1503,6 +1602,12 @@ public class PackageInstaller { public String[] grantedRuntimePermissions; /** {@hide} */ public int installFlags; + /** {@hide} */ + public boolean isMultiPackage; + /** {@hide} */ + public int parentSessionId = INVALID_ID; + /** {@hide} */ + public int[] childSessionIds = NO_SESSIONS; /** {@hide} */ @UnsupportedAppUsage @@ -1531,6 +1636,12 @@ public class PackageInstaller { referrerUri = source.readParcelable(null); grantedRuntimePermissions = source.readStringArray(); installFlags = source.readInt(); + isMultiPackage = source.readBoolean(); + parentSessionId = source.readInt(); + childSessionIds = source.createIntArray(); + if (childSessionIds == null) { + childSessionIds = NO_SESSIONS; + } } /** @@ -1784,6 +1895,30 @@ public class PackageInstaller { return createDetailsIntent(); } + /** + * Returns true if this session is a multi-package session containing references to other + * sessions. + */ + public boolean isMultiPackage() { + return isMultiPackage; + } + + /** + * Returns the parent multi-package session ID if this session belongs to one, + * {@link #INVALID_ID} otherwise. + */ + public int getParentSessionId() { + return parentSessionId; + } + + /** + * Returns the set of session IDs that will be committed when this session is commited if + * this session is a multi-package session. + */ + public int[] getChildSessionIds() { + return childSessionIds; + } + @Override public int describeContents() { return 0; @@ -1811,6 +1946,9 @@ public class PackageInstaller { dest.writeParcelable(referrerUri, flags); dest.writeStringArray(grantedRuntimePermissions); dest.writeInt(installFlags); + dest.writeBoolean(isMultiPackage); + dest.writeInt(parentSessionId); + dest.writeIntArray(childSessionIds); } public static final Parcelable.Creator diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 67b86c0e6e42b..44b3fdaf9fd85 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -914,6 +914,11 @@ public abstract class PackageManager { */ public static final int INSTALL_REASON_USER = 4; + /** + * @hide + */ + public static final int INSTALL_UNKNOWN = 0; + /** * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} * on success. diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 6ccd0406bfccb..1a5b86cfbce0d 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -107,7 +107,9 @@ import java.util.List; import java.util.Objects; import java.util.Random; -public class PackageInstallerService extends IPackageInstaller.Stub { +/** The service responsible for installing packages. */ +public class PackageInstallerService extends IPackageInstaller.Stub implements + PackageSessionProvider { private static final String TAG = "PackageInstaller"; private static final boolean LOGD = false; @@ -296,6 +298,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub { in.setInput(fis, StandardCharsets.UTF_8.name()); int type; + PackageInstallerSession currentSession = null; while ((type = in.next()) != END_DOCUMENT) { if (type == START_TAG) { final String tag = in.getName(); @@ -303,8 +306,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub { final PackageInstallerSession session; try { session = PackageInstallerSession.readFromXml(in, mInternalCallback, - mContext, mPm, mInstallThread.getLooper(), mSessionsDir); + mContext, mPm, mInstallThread.getLooper(), mSessionsDir, this); + currentSession = session; } catch (Exception e) { + currentSession = null; Slog.e(TAG, "Could not read session", e); continue; } @@ -329,6 +334,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub { addHistoricalSessionLocked(session); } mAllocatedSessions.put(session.sessionId, true); + } else if (currentSession != null + && PackageInstallerSession.TAG_CHILD_SESSION.equals(tag)) { + currentSession.addChildSessionIdInternal( + PackageInstallerSession.readChildSessionIdFromXml(in)); } } } @@ -436,70 +445,72 @@ public class PackageInstallerService extends IPackageInstaller.Stub { } } - // Only system components can circumvent runtime permissions when installing. - if ((params.installFlags & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0 - && mContext.checkCallingOrSelfPermission(Manifest.permission - .INSTALL_GRANT_RUNTIME_PERMISSIONS) == PackageManager.PERMISSION_DENIED) { - throw new SecurityException("You need the " - + "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission " - + "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag"); - } - - if ((params.installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0 - || (params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { - throw new IllegalArgumentException( - "New installs into ASEC containers no longer supported"); - } - - // Defensively resize giant app icons - if (params.appIcon != null) { - final ActivityManager am = (ActivityManager) mContext.getSystemService( - Context.ACTIVITY_SERVICE); - final int iconSize = am.getLauncherLargeIconSize(); - if ((params.appIcon.getWidth() > iconSize * 2) - || (params.appIcon.getHeight() > iconSize * 2)) { - params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize, - true); - } - } - - switch (params.mode) { - case SessionParams.MODE_FULL_INSTALL: - case SessionParams.MODE_INHERIT_EXISTING: - break; - default: - throw new IllegalArgumentException("Invalid install mode: " + params.mode); - } - - // If caller requested explicit location, sanity check it, otherwise - // resolve the best internal or adopted location. - if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { - if (!PackageHelper.fitsOnInternal(mContext, params)) { - throw new IOException("No suitable internal storage available"); + if (!params.isMultiPackage) { + // Only system components can circumvent runtime permissions when installing. + if ((params.installFlags & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0 + && mContext.checkCallingOrSelfPermission(Manifest.permission + .INSTALL_GRANT_RUNTIME_PERMISSIONS) == PackageManager.PERMISSION_DENIED) { + throw new SecurityException("You need the " + + "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission " + + "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag"); } - } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { - if (!PackageHelper.fitsOnExternal(mContext, params)) { - throw new IOException("No suitable external storage available"); + if ((params.installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0 + || (params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { + throw new IllegalArgumentException( + "New installs into ASEC containers no longer supported"); } - } else if ((params.installFlags & PackageManager.INSTALL_FORCE_VOLUME_UUID) != 0) { - // For now, installs to adopted media are treated as internal from - // an install flag point-of-view. - params.setInstallFlagsInternal(); + // Defensively resize giant app icons + if (params.appIcon != null) { + final ActivityManager am = (ActivityManager) mContext.getSystemService( + Context.ACTIVITY_SERVICE); + final int iconSize = am.getLauncherLargeIconSize(); + if ((params.appIcon.getWidth() > iconSize * 2) + || (params.appIcon.getHeight() > iconSize * 2)) { + params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize, + true); + } + } - } else { - // For now, installs to adopted media are treated as internal from - // an install flag point-of-view. - params.setInstallFlagsInternal(); + switch (params.mode) { + case SessionParams.MODE_FULL_INSTALL: + case SessionParams.MODE_INHERIT_EXISTING: + break; + default: + throw new IllegalArgumentException("Invalid install mode: " + params.mode); + } - // Resolve best location for install, based on combination of - // requested install flags, delta size, and manifest settings. - final long ident = Binder.clearCallingIdentity(); - try { - params.volumeUuid = PackageHelper.resolveInstallVolume(mContext, params); - } finally { - Binder.restoreCallingIdentity(ident); + // If caller requested explicit location, sanity check it, otherwise + // resolve the best internal or adopted location. + if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { + if (!PackageHelper.fitsOnInternal(mContext, params)) { + throw new IOException("No suitable internal storage available"); + } + + } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { + if (!PackageHelper.fitsOnExternal(mContext, params)) { + throw new IOException("No suitable external storage available"); + } + + } else if ((params.installFlags & PackageManager.INSTALL_FORCE_VOLUME_UUID) != 0) { + // For now, installs to adopted media are treated as internal from + // an install flag point-of-view. + params.setInstallFlagsInternal(); + + } else { + // For now, installs to adopted media are treated as internal from + // an install flag point-of-view. + params.setInstallFlagsInternal(); + + // Resolve best location for install, based on combination of + // requested install flags, delta size, and manifest settings. + final long ident = Binder.clearCallingIdentity(); + try { + params.volumeUuid = PackageHelper.resolveInstallVolume(mContext, params); + } finally { + Binder.restoreCallingIdentity(ident); + } } } @@ -525,17 +536,19 @@ public class PackageInstallerService extends IPackageInstaller.Stub { // We're staging to exactly one location File stageDir = null; String stageCid = null; - if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { - final boolean isInstant = - (params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; - stageDir = buildStageDir(params.volumeUuid, sessionId, isInstant); - } else { - stageCid = buildExternalStageCid(sessionId); + if (!params.isMultiPackage) { + if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { + final boolean isInstant = + (params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; + stageDir = buildStageDir(params.volumeUuid, sessionId, isInstant); + } else { + stageCid = buildExternalStageCid(sessionId); + } } - - session = new PackageInstallerSession(mInternalCallback, mContext, mPm, - mInstallThread.getLooper(), sessionId, userId, installerPackageName, callingUid, - params, createdMillis, stageDir, stageCid, false, false); + session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this, + mInstallThread.getLooper(), sessionId, userId, installerPackageName, + callingUid, params, createdMillis, stageDir, stageCid, false, false, null, + SessionInfo.INVALID_ID); synchronized (mSessions) { mSessions.put(sessionId, session); @@ -678,7 +691,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub { synchronized (mSessions) { for (int i = 0; i < mSessions.size(); i++) { final PackageInstallerSession session = mSessions.valueAt(i); - if (session.userId == userId) { + if (session.userId == userId && !session.hasParentSessionId()) { result.add(session.generateInfo(false)); } } @@ -699,7 +712,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub { SessionInfo info = session.generateInfo(false); if (Objects.equals(info.getInstallerPackageName(), installerPackageName) - && session.userId == userId) { + && session.userId == userId && !session.hasParentSessionId()) { result.add(info); } } @@ -781,6 +794,13 @@ public class PackageInstallerService extends IPackageInstaller.Stub { mCallbacks.unregister(callback); } + @Override + public PackageInstallerSession getSession(int sessionId) { + synchronized (mSessions) { + return mSessions.get(sessionId); + } + } + private static int getSessionCount(SparseArray sessions, int installerUid) { int count = 0; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 6e450137185b6..4e2c3c5ac0258 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -47,6 +47,8 @@ import android.apex.IApexService; import android.app.admin.DeviceAdminInfo; import android.app.admin.DevicePolicyManagerInternal; import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; import android.content.pm.ApplicationInfo; @@ -69,6 +71,7 @@ import android.os.Bundle; import android.os.FileBridge; import android.os.FileUtils; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; @@ -90,6 +93,7 @@ import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.MathUtils; import android.util.Slog; +import android.util.SparseIntArray; import android.util.apk.ApkSignatureVerifier; import com.android.internal.annotations.GuardedBy; @@ -130,6 +134,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { /** XML constants used for persisting a session */ static final String TAG_SESSION = "session"; + static final String TAG_CHILD_SESSION = "childSession"; private static final String TAG_GRANTED_RUNTIME_PERMISSION = "granted-runtime-permission"; private static final String ATTR_SESSION_ID = "sessionId"; private static final String ATTR_USER_ID = "userId"; @@ -140,6 +145,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid"; private static final String ATTR_PREPARED = "prepared"; private static final String ATTR_SEALED = "sealed"; + private static final String ATTR_MULTI_PACKAGE = "multiPackage"; + private static final String ATTR_PARENT_SESSION_ID = "parentSessionId"; private static final String ATTR_MODE = "mode"; private static final String ATTR_INSTALL_FLAGS = "installFlags"; private static final String ATTR_INSTALL_LOCATION = "installLocation"; @@ -157,6 +164,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_INSTALL_REASON = "installRason"; private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill"; + private static final int[] EMPTY_CHILD_SESSION_ARRAY = {}; // TODO: enforce INSTALL_ALLOW_TEST // TODO: enforce INSTALL_ALLOW_DOWNGRADE @@ -165,6 +173,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final Context mContext; private final PackageManagerService mPm; private final Handler mHandler; + private final PackageSessionProvider mSessionProvider; final int sessionId; final int userId; @@ -236,6 +245,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private long mVersionCode; @GuardedBy("mLock") private PackageParser.SigningDetails mSigningDetails; + @GuardedBy("mLock") + private SparseIntArray mChildSessionIds = new SparseIntArray(); + @GuardedBy("mLock") + private int mParentSessionId; /** * Path to the validated base APK for this session, which may point at an @@ -372,12 +385,16 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } public PackageInstallerSession(PackageInstallerService.InternalCallback callback, - Context context, PackageManagerService pm, Looper looper, int sessionId, int userId, + Context context, PackageManagerService pm, + PackageSessionProvider sessionProvider, Looper looper, + int sessionId, int userId, String installerPackageName, int installerUid, SessionParams params, long createdMillis, - File stageDir, String stageCid, boolean prepared, boolean sealed) { + File stageDir, String stageCid, boolean prepared, boolean sealed, + @Nullable int[] childSessionIds, int parentSessionId) { mCallback = callback; mContext = context; mPm = pm; + mSessionProvider = sessionProvider; mHandler = new Handler(looper, mHandlerCallback); this.sessionId = sessionId; @@ -389,8 +406,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { this.createdMillis = createdMillis; this.stageDir = stageDir; this.stageCid = stageCid; + if (childSessionIds != null) { + for (int childSessionId : childSessionIds) { + mChildSessionIds.put(childSessionId, 0); + } + } + this.mParentSessionId = parentSessionId; - if ((stageDir == null) == (stageCid == null)) { + if (!params.isMultiPackage && (stageDir == null) == (stageCid == null)) { throw new IllegalArgumentException( "Exactly one of stageDir or stageCid stage must be set"); } @@ -439,6 +462,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { info.referrerUri = params.referrerUri; info.grantedRuntimePermissions = params.grantedRuntimePermissions; info.installFlags = params.installFlags; + info.isMultiPackage = params.isMultiPackage; + info.parentSessionId = mParentSessionId; + info.childSessionIds = mChildSessionIds.copyKeys(); + if (info.childSessionIds == null) { + info.childSessionIds = EMPTY_CHILD_SESSION_ARRAY; + } } return info; } @@ -762,6 +791,92 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) { + if (!markAsCommitted(statusReceiver, forTransfer /* enforce */)) { + return; + } + if (isMultiPackage()) { + + final SparseIntArray remainingSessions = mChildSessionIds.clone(); + final ChildStatusIntentReceiver localIntentReceiver = + new ChildStatusIntentReceiver(remainingSessions, statusReceiver); + for (int childSessionId : getChildSessionIds()) { + mSessionProvider.getSession(childSessionId) + .markAsCommitted(localIntentReceiver.getIntentSender(), forTransfer); + } + } + mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); + } + + private class ChildStatusIntentReceiver { + private final SparseIntArray mChildSessionsRemaining; + private final IntentSender mStatusReceiver; + private final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { + @Override + public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, + IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { + statusUpdate(intent); + } + }; + + private ChildStatusIntentReceiver(SparseIntArray remainingSessions, + IntentSender statusReceiver) { + this.mChildSessionsRemaining = remainingSessions; + this.mStatusReceiver = statusReceiver; + } + + public IntentSender getIntentSender() { + return new IntentSender((IIntentSender) mLocalSender); + } + + public void statusUpdate(Intent intent) { + mHandler.post(() -> { + if (mChildSessionsRemaining.size() == 0) { + return; + } + final int sessionId = intent.getIntExtra( + PackageInstaller.EXTRA_SESSION_ID, 0); + final int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, + PackageInstaller.STATUS_FAILURE); + final int sessionIndex = mChildSessionsRemaining.indexOfKey(sessionId); + if (PackageInstaller.STATUS_SUCCESS == status) { + mChildSessionsRemaining.removeAt(sessionIndex); + if (mChildSessionsRemaining.size() == 0) { + try { + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, + PackageInstallerSession.this.sessionId); + mStatusReceiver.sendIntent(mContext, 0, intent, null, null); + } catch (IntentSender.SendIntentException ignore) { + } + } + } else if (PackageInstaller.STATUS_PENDING_USER_ACTION == status) { + try { + mStatusReceiver.sendIntent(mContext, 0, intent, null, null); + } catch (IntentSender.SendIntentException ignore) { + } + } else { + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, + PackageInstallerSession.this.sessionId); + mChildSessionsRemaining.clear(); // we're done. Don't send any more. + try { + mStatusReceiver.sendIntent(mContext, 0, intent, null, null); + } catch (IntentSender.SendIntentException ignore) { + } + } + }); + } + } + + + /** + * Do everything but actually commit the session. If this was not already called, the session + * will be sealed and marked as committed. The caller of this method is responsible for + * subsequently submitting this session for processing. + * + * This method may be called multiple times to update the status receiver validate caller + * permissions. + */ + public boolean markAsCommitted( + @NonNull IntentSender statusReceiver, boolean forTransfer) { Preconditions.checkNotNull(statusReceiver); final boolean wasSealed; @@ -786,6 +901,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + // After validations and updating the observer, we can skip re-sealing, etc. because we + // have already marked ourselves as committed. + if (mCommitted) { + return true; + } + wasSealed = mSealed; if (!mSealed) { try { @@ -796,7 +917,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // Do now throw an exception here to stay compatible with O and older destroyInternal(); dispatchSessionFinished(e.error, ExceptionUtils.getCompleteMessage(e), null); - return; + return false; } } @@ -809,7 +930,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mActiveCount.incrementAndGet(); mCommitted = true; - mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); } if (!wasSealed) { @@ -818,6 +938,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // the session lock, since otherwise it's a lock inversion. mCallback.onSessionSealedBlocking(this); } + return true; } /** @@ -833,32 +954,37 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { assertNoWriteFileTransfersOpenLocked(); assertPreparedAndNotDestroyedLocked("sealing of session"); - final PackageInfo pkgInfo = mPm.getPackageInfo( - params.appPackageName, PackageManager.GET_SIGNATURES - | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId); - - resolveStageDirLocked(); - mSealed = true; - try { - if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) { - validateApexInstallLocked(pkgInfo); - } else { - // Verify that stage looks sane with respect to existing application. - // This currently only ensures packageName, versionCode, and certificate - // consistency. - validateApkInstallLocked(pkgInfo); - } - } catch (PackageManagerException e) { - throw e; - } catch (Throwable e) { - // Convert all exceptions into package manager exceptions as only those are handled - // in the code above - throw new PackageManagerException(e); - } // Read transfers from the original owner stay open, but as the session's data // cannot be modified anymore, there is no leak of information. + if (!params.isMultiPackage) { + final PackageInfo pkgInfo = mPm.getPackageInfo( + params.appPackageName, PackageManager.GET_SIGNATURES + | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId); + + resolveStageDirLocked(); + + // Verify that stage looks sane with respect to existing application. + // This currently only ensures packageName, versionCode, and certificate + // consistency. + try { + if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) { + validateApexInstallLocked(pkgInfo); + } else { + // Verify that stage looks sane with respect to existing application. + // This currently only ensures packageName, versionCode, and certificate + // consistency. + validateApkInstallLocked(pkgInfo); + } + } catch (PackageManagerException e) { + throw e; + } catch (Throwable e) { + // Convert all exceptions into package manager exceptions as only those are handled + // in the code above + throw new PackageManagerException(e); + } + } } @Override @@ -916,25 +1042,52 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private void commitLocked() throws PackageManagerException { - if (mRelinquished) { - Slog.d(TAG, "Ignoring commit after previous commit relinquished control"); + final PackageManagerService.ActiveInstallSession committingSession = + makeSessionActiveLocked(); + if (committingSession == null) { return; } - if (mDestroyed) { - throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed"); - } - if (!mSealed) { - throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed"); - } - - Preconditions.checkNotNull(mPackageName); - Preconditions.checkNotNull(mSigningDetails); - Preconditions.checkNotNull(mResolvedBaseFile); - - if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) { - commitApexLocked(); + if (isMultiPackage()) { + final int[] childSessionIds = getChildSessionIds(); + List childSessions = + new ArrayList<>(childSessionIds.length); + boolean success = true; + PackageManagerException failure = null; + for (int childSessionId : getChildSessionIds()) { + final PackageInstallerSession session = mSessionProvider.getSession(childSessionId); + try { + final PackageManagerService.ActiveInstallSession activeSession = + session.makeSessionActiveLocked(); + if (activeSession != null) { + if ((activeSession.getSessionParams().installFlags + & PackageManager.INSTALL_APEX) != 0) { + // TODO(b/118865310): Add exception to this case for staged installs + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + "Atomic install is not supported for APEX packages."); + } + childSessions.add(activeSession); + } + } catch (PackageManagerException e) { + failure = e; + success = false; + } + } + if (!success) { + try { + mRemoteObserver.onPackageInstalled( + null, failure.error, failure.getLocalizedMessage(), null); + } catch (RemoteException ignored) { + } + return; + } + mPm.installStage(childSessions); } else { - commitApkLocked(); + if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) { + commitApexLocked(); + } else { + mPm.installStage(committingSession); + } } } @@ -954,81 +1107,105 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + /** + * Stages this session for install and returns a + * {@link PackageManagerService.ActiveInstallSession} representing this new staged state or null + * in case permissions need to be requested before install can proceed. + */ @GuardedBy("mLock") - private void commitApkLocked() throws PackageManagerException { - if (needToAskForPermissionsLocked()) { - // User needs to confirm installation; give installer an intent they can use to involve - // user. - final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_INSTALL); - intent.setPackage(mPm.getPackageInstallerPackageName()); - intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); - try { - mRemoteObserver.onUserActionRequired(intent); - } catch (RemoteException ignored) { - } - - // Commit was keeping session marked as active until now; release - // that extra refcount so session appears idle. - closeInternal(false); - return; + private PackageManagerService.ActiveInstallSession makeSessionActiveLocked() + throws PackageManagerException { + if (mRelinquished) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session relinquished"); + } + if (mDestroyed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed"); + } + if (!mSealed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed"); } - // Inherit any packages and native libraries from existing install that - // haven't been overridden. - if (params.mode == SessionParams.MODE_INHERIT_EXISTING) { - try { - final List fromFiles = mResolvedInheritedFiles; - final File toDir = resolveStageDirLocked(); + if (!params.isMultiPackage) { + Preconditions.checkNotNull(mPackageName); + Preconditions.checkNotNull(mSigningDetails); + Preconditions.checkNotNull(mResolvedBaseFile); - if (LOGD) Slog.d(TAG, "Inherited files: " + mResolvedInheritedFiles); - if (!mResolvedInheritedFiles.isEmpty() && mInheritedFilesBase == null) { - throw new IllegalStateException("mInheritedFilesBase == null"); + if (needToAskForPermissionsLocked()) { + // User needs to confirm installation; + // give installer an intent they can use to involve + // user. + final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_INSTALL); + intent.setPackage(mPm.getPackageInstallerPackageName()); + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + try { + mRemoteObserver.onUserActionRequired(intent); + } catch (RemoteException ignored) { } - if (isLinkPossible(fromFiles, toDir)) { - if (!mResolvedInstructionSets.isEmpty()) { - final File oatDir = new File(toDir, "oat"); - createOatDirs(mResolvedInstructionSets, oatDir); + // Commit was keeping session marked as active until now; release + // that extra refcount so session appears idle. + closeInternal(false); + return null; + } + + // Inherit any packages and native libraries from existing install that + // haven't been overridden. + if (params.mode == SessionParams.MODE_INHERIT_EXISTING) { + try { + final List fromFiles = mResolvedInheritedFiles; + final File toDir = resolveStageDirLocked(); + + if (LOGD) Slog.d(TAG, "Inherited files: " + mResolvedInheritedFiles); + if (!mResolvedInheritedFiles.isEmpty() && mInheritedFilesBase == null) { + throw new IllegalStateException("mInheritedFilesBase == null"); } - // pre-create lib dirs for linking if necessary - if (!mResolvedNativeLibPaths.isEmpty()) { - for (String libPath : mResolvedNativeLibPaths) { - // "/lib/arm64" -> ["lib", "arm64"] - final int splitIndex = libPath.lastIndexOf('/'); - if (splitIndex < 0 || splitIndex >= libPath.length() - 1) { - Slog.e(TAG, "Skipping native library creation for linking due to " - + "invalid path: " + libPath); - continue; - } - final String libDirPath = libPath.substring(1, splitIndex); - final File libDir = new File(toDir, libDirPath); - if (!libDir.exists()) { - NativeLibraryHelper.createNativeLibrarySubdir(libDir); - } - final String archDirPath = libPath.substring(splitIndex + 1); - NativeLibraryHelper.createNativeLibrarySubdir( - new File(libDir, archDirPath)); + + if (isLinkPossible(fromFiles, toDir)) { + if (!mResolvedInstructionSets.isEmpty()) { + final File oatDir = new File(toDir, "oat"); + createOatDirs(mResolvedInstructionSets, oatDir); } + // pre-create lib dirs for linking if necessary + if (!mResolvedNativeLibPaths.isEmpty()) { + for (String libPath : mResolvedNativeLibPaths) { + // "/lib/arm64" -> ["lib", "arm64"] + final int splitIndex = libPath.lastIndexOf('/'); + if (splitIndex < 0 || splitIndex >= libPath.length() - 1) { + Slog.e(TAG, + "Skipping native library creation for linking due to " + + "invalid path: " + libPath); + continue; + } + final String libDirPath = libPath.substring(1, splitIndex); + final File libDir = new File(toDir, libDirPath); + if (!libDir.exists()) { + NativeLibraryHelper.createNativeLibrarySubdir(libDir); + } + final String archDirPath = libPath.substring(splitIndex + 1); + NativeLibraryHelper.createNativeLibrarySubdir( + new File(libDir, archDirPath)); + } + } + linkFiles(fromFiles, toDir, mInheritedFilesBase); + } else { + // TODO: this should delegate to DCS so the system process + // avoids holding open FDs into containers. + copyFiles(fromFiles, toDir); } - linkFiles(fromFiles, toDir, mInheritedFilesBase); - } else { - // TODO: this should delegate to DCS so the system process - // avoids holding open FDs into containers. - copyFiles(fromFiles, toDir); + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE, + "Failed to inherit existing install", e); } - } catch (IOException e) { - throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE, - "Failed to inherit existing install", e); } + + // TODO: surface more granular state from dexopt + mInternalProgress = 0.5f; + computeProgressLocked(true); + + // Unpack native libraries + extractNativeLibraries(mResolvedStageDir, params.abiOverride, mayInheritNativeLibs()); } - - // TODO: surface more granular state from dexopt - mInternalProgress = 0.5f; - computeProgressLocked(true); - - // Unpack native libraries - extractNativeLibraries(mResolvedStageDir, params.abiOverride, mayInheritNativeLibs()); - // We've reached point of no return; call into PMS to install the stage. // Regardless of success or failure we always destroy session. final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { @@ -1053,8 +1230,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } mRelinquished = true; - mPm.installStage(mPackageName, stageDir, localObserver, params, - mInstallerPackageName, mInstallerUid, user, mSigningDetails); + final PackageManagerService.ActiveInstallSession activeInstallSession = + new PackageManagerService.ActiveInstallSession(mPackageName, stageDir, + localObserver, params, mInstallerPackageName, mInstallerUid, user, + mSigningDetails); + return activeInstallSession; } private static void maybeRenameFile(File from, File to) throws PackageManagerException { @@ -1556,6 +1736,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + /** + * Adds a child session ID without any safety / sanity checks. This should only be used to + * build a session from XML or similar. + */ + void addChildSessionIdInternal(int sessionId) { + mChildSessionIds.put(sessionId, 0); + } + public void open() throws IOException { if (mActiveCount.getAndIncrement() == 0) { mCallback.onSessionActiveChanged(this, true); @@ -1567,6 +1755,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (!mPrepared) { if (stageDir != null) { prepareStageDir(stageDir); + } else if (params.isMultiPackage) { + // it's all ok } else { throw new IllegalArgumentException("stageDir must be set"); } @@ -1615,6 +1805,81 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); } + @Override + public boolean isMultiPackage() { + return params.isMultiPackage; + } + + @Override + public int[] getChildSessionIds() { + final int[] childSessionIds = mChildSessionIds.copyKeys(); + if (childSessionIds != null) { + return childSessionIds; + } + return EMPTY_CHILD_SESSION_ARRAY; + } + + @Override + public void addChildSessionId(int sessionId) { + final PackageInstallerSession session = mSessionProvider.getSession(sessionId); + if (session == null) { + throw new RemoteException("Unable to add child.", + new PackageManagerException("Child session " + sessionId + " does not exist"), + false, true).rethrowAsRuntimeException(); + } + synchronized (mLock) { + final int indexOfSession = mChildSessionIds.indexOfKey(sessionId); + if (indexOfSession >= 0) { + return; + } + session.setParentSessionId(this.sessionId); + addChildSessionIdInternal(sessionId); + } + } + + @Override + public void removeChildSessionId(int sessionId) { + final PackageInstallerSession session = mSessionProvider.getSession(sessionId); + synchronized (mLock) { + final int indexOfSession = mChildSessionIds.indexOfKey(sessionId); + if (session != null) { + session.setParentSessionId(SessionInfo.INVALID_ID); + } + if (indexOfSession < 0) { + // not added in the first place; no-op + return; + } + mChildSessionIds.removeAt(indexOfSession); + } + } + + /** + * Sets the parent session ID if not already set. + * If {@link SessionInfo#INVALID_ID} is passed, it will be unset. + */ + void setParentSessionId(int parentSessionId) { + synchronized (mLock) { + if (parentSessionId != SessionInfo.INVALID_ID + && mParentSessionId != SessionInfo.INVALID_ID) { + throw new RemoteException("Unable to set parent session.", + new PackageManagerException( + "The parent of " + sessionId + " is" + " already set to " + + mParentSessionId), false, + true).rethrowAsRuntimeException(); + } + this.mParentSessionId = parentSessionId; + } + } + + boolean hasParentSessionId() { + return mParentSessionId != SessionInfo.INVALID_ID; + } + + @Override + public int getParentSessionId() { + return mParentSessionId; + } + private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { final IPackageInstallObserver2 observer; final String packageName; @@ -1704,6 +1969,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { pw.printPair("mBridges", mBridges.size()); pw.printPair("mFinalStatus", mFinalStatus); pw.printPair("mFinalMessage", mFinalMessage); + pw.printPair("params.isMultiPackage", params.isMultiPackage); pw.println(); pw.decreaseIndent(); @@ -1754,6 +2020,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { writeBooleanAttribute(out, ATTR_PREPARED, isPrepared()); writeBooleanAttribute(out, ATTR_SEALED, isSealed()); + writeBooleanAttribute(out, ATTR_MULTI_PACKAGE, params.isMultiPackage); + // TODO(patb,109941548): avoid writing to xml and instead infer / validate this after + // we've read all sessions. + writeIntAttribute(out, ATTR_PARENT_SESSION_ID, mParentSessionId); writeIntAttribute(out, ATTR_MODE, params.mode); writeIntAttribute(out, ATTR_INSTALL_FLAGS, params.installFlags); writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation); @@ -1788,6 +2058,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { params.appIconLastModified = appIconFile.lastModified(); } + final int[] childSessionIds = getChildSessionIds(); + for (int childSessionId : childSessionIds) { + out.startTag(null, TAG_CHILD_SESSION); + writeIntAttribute(out, ATTR_SESSION_ID, childSessionId); + out.endTag(null, TAG_CHILD_SESSION); + } } out.endTag(null, TAG_SESSION); @@ -1832,11 +2108,15 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * @param installerThread Thread to be used for callbacks of this session * @param sessionsDir The directory the sessions are stored in * + * @param sessionProvider * @return The newly created session */ + // TODO(patb,109941548): modify readFromXml to consume to the next tag session tag so we + // can have a complete session for the constructor public static PackageInstallerSession readFromXml(@NonNull XmlPullParser in, @NonNull PackageInstallerService.InternalCallback callback, @NonNull Context context, - @NonNull PackageManagerService pm, Looper installerThread, @NonNull File sessionsDir) + @NonNull PackageManagerService pm, Looper installerThread, @NonNull File sessionsDir, + @NonNull PackageSessionProvider sessionProvider) throws IOException, XmlPullParserException { final int sessionId = readIntAttribute(in, ATTR_SESSION_ID); final int userId = readIntAttribute(in, ATTR_USER_ID); @@ -1849,9 +2129,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID); final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true); final boolean sealed = readBooleanAttribute(in, ATTR_SEALED); + final int parentSessionId = readIntAttribute(in, ATTR_PARENT_SESSION_ID, + SessionInfo.INVALID_ID); final SessionParams params = new SessionParams( SessionParams.MODE_INVALID); + params.isMultiPackage = readBooleanAttribute(in, ATTR_MULTI_PACKAGE, false); params.mode = readIntAttribute(in, ATTR_MODE); params.installFlags = readIntAttribute(in, ATTR_INSTALL_FLAGS); params.installLocation = readIntAttribute(in, ATTR_INSTALL_LOCATION); @@ -1874,9 +2157,16 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath()); params.appIconLastModified = appIconFile.lastModified(); } - - return new PackageInstallerSession(callback, context, pm, + return new PackageInstallerSession(callback, context, pm, sessionProvider, installerThread, sessionId, userId, installerPackageName, installerUid, - params, createdMillis, stageDir, stageCid, prepared, sealed); + params, createdMillis, stageDir, stageCid, prepared, sealed, + EMPTY_CHILD_SESSION_ARRAY, parentSessionId); + } + + /** + * Reads the session ID from a child session tag stored in the provided {@link XmlPullParser} + */ + static int readChildSessionIdFromXml(@NonNull XmlPullParser in) { + return readIntAttribute(in, ATTR_SESSION_ID, SessionInfo.INVALID_ID); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 0e37bcafe5d00..c90f083c1c7ec 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -60,6 +60,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_TEST_ONLY; import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE; import static android.content.pm.PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE; import static android.content.pm.PackageManager.INSTALL_INTERNAL; +import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS; import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK; import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK; @@ -12358,28 +12359,15 @@ public class PackageManagerService extends IPackageManager.Stub return installReason; } - void installStage(String packageName, File stagedDir, - IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams, - String installerPackageName, int installerUid, UserHandle user, - PackageParser.SigningDetails signingDetails) { + void installStage(ActiveInstallSession activeInstallSession) { if (DEBUG_INSTANT) { - if ((sessionParams.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) { - Slog.d(TAG, "Ephemeral install of " + packageName); + if ((activeInstallSession.getSessionParams().installFlags + & PackageManager.INSTALL_INSTANT_APP) != 0) { + Slog.d(TAG, "Ephemeral install of " + activeInstallSession.getPackageName()); } } - final VerificationInfo verificationInfo = new VerificationInfo( - sessionParams.originatingUri, sessionParams.referrerUri, - sessionParams.originatingUid, installerUid); - - final OriginInfo origin = OriginInfo.fromStagedFile(stagedDir); - final Message msg = mHandler.obtainMessage(INIT_COPY); - final int installReason = fixUpInstallReason(installerPackageName, installerUid, - sessionParams.installReason); - final InstallParams params = new InstallParams(origin, null, observer, - sessionParams.installFlags, installerPackageName, sessionParams.volumeUuid, - verificationInfo, user, sessionParams.abiOverride, - sessionParams.grantedRuntimePermissions, signingDetails, installReason); + final InstallParams params = new InstallParams(activeInstallSession); params.setTraceMethod("installStage").setTraceCookie(System.identityHashCode(params)); msg.obj = params; @@ -12391,6 +12379,22 @@ public class PackageManagerService extends IPackageManager.Stub mHandler.sendMessage(msg); } + void installStage(List children) + throws PackageManagerException { + final Message msg = mHandler.obtainMessage(INIT_COPY); + final MultiPackageInstallParams params = + new MultiPackageInstallParams(UserHandle.ALL, children); + params.setTraceMethod("installStageMultiPackage") + .setTraceCookie(System.identityHashCode(params)); + msg.obj = params; + + Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "installStageMultiPackage", + System.identityHashCode(msg.obj)); + Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "queueInstall", + System.identityHashCode(msg.obj)); + mHandler.sendMessage(msg); + } + private void sendPackageAddedForUser(String packageName, PackageSetting pkgSetting, int userId) { final boolean isSystem = isSystemApp(pkgSetting) || isUpdatedSystemApp(pkgSetting); @@ -13568,88 +13572,113 @@ public class PackageManagerService extends IPackageManager.Stub } private void processPendingInstall(final InstallArgs args, final int currentStatus) { - // Queue up an async operation since the package installation may take a little while. - mHandler.post(new Runnable() { - public void run() { - mHandler.removeCallbacks(this); - // Result object to be returned - PackageInstalledInfo res = new PackageInstalledInfo(); - res.setReturnCode(currentStatus); - res.uid = -1; - res.pkg = null; - res.removedInfo = null; - if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { - args.doPreInstall(res.returnCode); - synchronized (mInstallLock) { - installPackageTracedLI(args, res); - } - args.doPostInstall(res.returnCode, res.uid); + if (args.mMultiPackageInstallParams != null) { + args.mMultiPackageInstallParams.tryProcessInstallRequest(args, currentStatus); + } else { + PackageInstalledInfo res = createPackageInstalledInfo(currentStatus); + processInstallRequestsAsync( + res.returnCode == PackageManager.INSTALL_SUCCEEDED, + Collections.singletonList(new InstallRequest(args, res))); + } + } + + // Queue up an async operation since the package installation may take a little while. + private void processInstallRequestsAsync(boolean success, + List installRequests) { + mHandler.post(() -> { + if (success) { + for (InstallRequest request : installRequests) { + request.args.doPreInstall(request.installResult.returnCode); } - - // A restore should be performed at this point if (a) the install - // succeeded, (b) the operation is not an update, and (c) the new - // package has not opted out of backup participation. - final boolean update = res.removedInfo != null - && res.removedInfo.removedPackage != null; - final int flags = (res.pkg == null) ? 0 : res.pkg.applicationInfo.flags; - boolean doRestore = !update - && ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0); - - // Set up the post-install work request bookkeeping. This will be used - // and cleaned up by the post-install event handling regardless of whether - // there's a restore pass performed. Token values are >= 1. - int token; - if (mNextInstallToken < 0) mNextInstallToken = 1; - token = mNextInstallToken++; - - PostInstallData data = new PostInstallData(args, res); - mRunningInstalls.put(token, data); - if (DEBUG_INSTALL) Log.v(TAG, "+ starting restore round-trip " + token); - - if (res.returnCode == PackageManager.INSTALL_SUCCEEDED && doRestore) { - // Pass responsibility to the Backup Manager. It will perform a - // restore if appropriate, then pass responsibility back to the - // Package Manager to run the post-install observer callbacks - // and broadcasts. - IBackupManager bm = IBackupManager.Stub.asInterface( - ServiceManager.getService(Context.BACKUP_SERVICE)); - if (bm != null) { - if (DEBUG_INSTALL) Log.v(TAG, "token " + token - + " to BM for possible restore"); - Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "restore", token); - try { - // TODO: http://b/22388012 - if (bm.isBackupServiceActive(UserHandle.USER_SYSTEM)) { - bm.restoreAtInstall(res.pkg.applicationInfo.packageName, token); - } else { - doRestore = false; - } - } catch (RemoteException e) { - // can't happen; the backup manager is local - } catch (Exception e) { - Slog.e(TAG, "Exception trying to enqueue restore", e); - doRestore = false; - } - } else { - Slog.e(TAG, "Backup Manager not found!"); - doRestore = false; - } + synchronized (mInstallLock) { + installPackagesTracedLI(installRequests); } - - if (!doRestore) { - // No restore possible, or the Backup Manager was mysteriously not - // available -- just fire the post-install work request directly. - if (DEBUG_INSTALL) Log.v(TAG, "No restore - queue post-install for " + token); - - Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "postInstall", token); - - Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0); - mHandler.sendMessage(msg); + for (InstallRequest request : installRequests) { + request.args.doPostInstall( + request.installResult.returnCode, request.installResult.uid); } } + for (InstallRequest request : installRequests) { + resolvePackageInstalledInfo(request.args, + request.installResult); + } }); } + private PackageInstalledInfo createPackageInstalledInfo( + int currentStatus) { + PackageInstalledInfo res = new PackageInstalledInfo(); + res.setReturnCode(currentStatus); + res.uid = -1; + res.pkg = null; + res.removedInfo = null; + return res; + } + + private void resolvePackageInstalledInfo(InstallArgs args, PackageInstalledInfo res) { + // A restore should be performed at this point if (a) the install + // succeeded, (b) the operation is not an update, and (c) the new + // package has not opted out of backup participation. + final boolean update = res.removedInfo != null + && res.removedInfo.removedPackage != null; + final int flags = (res.pkg == null) ? 0 : res.pkg.applicationInfo.flags; + boolean doRestore = !update + && ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0); + + // Set up the post-install work request bookkeeping. This will be used + // and cleaned up by the post-install event handling regardless of whether + // there's a restore pass performed. Token values are >= 1. + int token; + if (mNextInstallToken < 0) mNextInstallToken = 1; + token = mNextInstallToken++; + + PostInstallData data = new PostInstallData(args, res); + mRunningInstalls.put(token, data); + if (DEBUG_INSTALL) Log.v(TAG, "+ starting restore round-trip " + token); + + if (res.returnCode == PackageManager.INSTALL_SUCCEEDED && doRestore) { + // Pass responsibility to the Backup Manager. It will perform a + // restore if appropriate, then pass responsibility back to the + // Package Manager to run the post-install observer callbacks + // and broadcasts. + IBackupManager bm = IBackupManager.Stub.asInterface( + ServiceManager.getService(Context.BACKUP_SERVICE)); + if (bm != null) { + if (DEBUG_INSTALL) { + Log.v(TAG, "token " + token + " to BM for possible restore"); + } + Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "restore", token); + try { + // TODO: http://b/22388012 + if (bm.isBackupServiceActive(UserHandle.USER_SYSTEM)) { + bm.restoreAtInstall(res.pkg.applicationInfo.packageName, token); + } else { + doRestore = false; + } + } catch (RemoteException e) { + // can't happen; the backup manager is local + } catch (Exception e) { + Slog.e(TAG, "Exception trying to enqueue restore", e); + doRestore = false; + } + } else { + Slog.e(TAG, "Backup Manager not found!"); + doRestore = false; + } + } + + if (!doRestore) { + // No restore possible, or the Backup Manager was mysteriously not + // available -- just fire the post-install work request directly. + if (DEBUG_INSTALL) Log.v(TAG, "No restore - queue post-install for " + token); + + Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "postInstall", token); + + Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0); + mHandler.sendMessage(msg); + } + } + /** * Callback from PackageSettings whenever an app is first transitioned out of the * 'stopped' state. Normally we just issue the broadcast, but we can't do that if @@ -13837,7 +13866,83 @@ public class PackageManagerService extends IPackageManager.Stub } } + /** + * Container for a multi-package install which refers to all install sessions and args being + * committed together. + */ + class MultiPackageInstallParams extends HandlerParams { + + private int mRet = INSTALL_SUCCEEDED; + @NonNull + private final ArrayList mChildParams; + @NonNull + private final Map mVerifiedState; + + MultiPackageInstallParams( + @NonNull UserHandle user, + @NonNull List activeInstallSessions) + throws PackageManagerException { + super(user); + if (activeInstallSessions.size() == 0) { + throw new PackageManagerException("No child sessions found!"); + } + mChildParams = new ArrayList<>(activeInstallSessions.size()); + for (int i = 0; i < activeInstallSessions.size(); i++) { + final InstallParams childParams = new InstallParams(activeInstallSessions.get(i)); + childParams.mParentInstallParams = this; + this.mChildParams.add(childParams); + } + this.mVerifiedState = new ArrayMap<>(mChildParams.size()); + } + + @Override + void handleStartCopy() { + for (InstallParams params : mChildParams) { + params.handleStartCopy(); + if (params.mRet != INSTALL_SUCCEEDED) { + mRet = params.mRet; + break; + } + } + } + + @Override + void handleReturnCode() { + for (InstallParams params : mChildParams) { + params.handleReturnCode(); + if (params.mRet != INSTALL_SUCCEEDED) { + mRet = params.mRet; + break; + } + } + } + + void tryProcessInstallRequest(InstallArgs args, int currentStatus) { + mVerifiedState.put(args, currentStatus); + boolean success = true; + if (mVerifiedState.size() != mChildParams.size()) { + return; + } + for (Integer status : mVerifiedState.values()) { + if (status == PackageManager.INSTALL_UNKNOWN) { + return; + } else if (status != PackageManager.INSTALL_SUCCEEDED) { + success = false; + break; + } + } + final List installRequests = new ArrayList<>(mVerifiedState.size()); + for (Map.Entry entry : mVerifiedState.entrySet()) { + installRequests.add(new InstallRequest(entry.getKey(), + createPackageInstalledInfo(entry.getValue()))); + } + processInstallRequestsAsync(success, installRequests); + } + } + class InstallParams extends HandlerParams { + // TODO: see if we can collapse this into ActiveInstallSession + final OriginInfo origin; final MoveInfo move; final IPackageInstallObserver2 observer; @@ -13845,17 +13950,20 @@ public class PackageManagerService extends IPackageManager.Stub final String installerPackageName; final String volumeUuid; private InstallArgs mArgs; - private int mRet; + int mRet; final String packageAbiOverride; final String[] grantedRuntimePermissions; final VerificationInfo verificationInfo; final PackageParser.SigningDetails signingDetails; final int installReason; + @Nullable + MultiPackageInstallParams mParentInstallParams; InstallParams(OriginInfo origin, MoveInfo move, IPackageInstallObserver2 observer, int installFlags, String installerPackageName, String volumeUuid, VerificationInfo verificationInfo, UserHandle user, String packageAbiOverride, - String[] grantedPermissions, PackageParser.SigningDetails signingDetails, int installReason) { + String[] grantedPermissions, PackageParser.SigningDetails signingDetails, + int installReason) { super(user); this.origin = origin; this.move = move; @@ -13870,6 +13978,34 @@ public class PackageManagerService extends IPackageManager.Stub this.installReason = installReason; } + InstallParams(ActiveInstallSession activeInstallSession) { + super(activeInstallSession.getUser()); + if (DEBUG_INSTANT) { + if ((activeInstallSession.getSessionParams().installFlags + & PackageManager.INSTALL_INSTANT_APP) != 0) { + Slog.d(TAG, "Ephemeral install of " + activeInstallSession.getPackageName()); + } + } + verificationInfo = new VerificationInfo( + activeInstallSession.getSessionParams().originatingUri, + activeInstallSession.getSessionParams().referrerUri, + activeInstallSession.getSessionParams().originatingUid, + activeInstallSession.getInstallerUid()); + origin = OriginInfo.fromStagedFile(activeInstallSession.getStagedDir()); + move = null; + installReason = fixUpInstallReason(activeInstallSession.getInstallerPackageName(), + activeInstallSession.getInstallerUid(), + activeInstallSession.getSessionParams().installReason); + observer = activeInstallSession.getObserver(); + installFlags = activeInstallSession.getSessionParams().installFlags; + installerPackageName = activeInstallSession.getInstallerPackageName(); + volumeUuid = activeInstallSession.getSessionParams().volumeUuid; + packageAbiOverride = activeInstallSession.getSessionParams().abiOverride; + grantedRuntimePermissions = + activeInstallSession.getSessionParams().grantedRuntimePermissions; + signingDetails = activeInstallSession.getSigningDetails(); + } + @Override public String toString() { return "InstallParams{" + Integer.toHexString(System.identityHashCode(this)) @@ -14291,6 +14427,7 @@ public class PackageManagerService extends IPackageManager.Stub final int traceCookie; final PackageParser.SigningDetails signingDetails; final int installReason; + @Nullable final MultiPackageInstallParams mMultiPackageInstallParams; // The list of instruction sets supported by this app. This is currently // only used during the rmdex() phase to clean up resources. We can get rid of this @@ -14301,8 +14438,9 @@ public class PackageManagerService extends IPackageManager.Stub int installFlags, String installerPackageName, String volumeUuid, UserHandle user, String[] instructionSets, String abiOverride, String[] installGrantPermissions, - String traceMethod, int traceCookie, PackageParser.SigningDetails signingDetails, - int installReason) { + String traceMethod, int traceCookie, SigningDetails signingDetails, + int installReason, + MultiPackageInstallParams multiPackageInstallParams) { this.origin = origin; this.move = move; this.installFlags = installFlags; @@ -14317,6 +14455,7 @@ public class PackageManagerService extends IPackageManager.Stub this.traceCookie = traceCookie; this.signingDetails = signingDetails; this.installReason = installReason; + this.mMultiPackageInstallParams = multiPackageInstallParams; } abstract int copyApk(); @@ -14412,7 +14551,7 @@ public class PackageManagerService extends IPackageManager.Stub params.getUser(), null /*instructionSets*/, params.packageAbiOverride, params.grantedRuntimePermissions, params.traceMethod, params.traceCookie, params.signingDetails, - params.installReason); + params.installReason, params.mParentInstallParams); if (isFwdLocked()) { throw new IllegalArgumentException("Forward locking only supported in ASEC"); } @@ -14422,7 +14561,7 @@ public class PackageManagerService extends IPackageManager.Stub FileInstallArgs(String codePath, String resourcePath, String[] instructionSets) { super(OriginInfo.fromNothing(), null, null, 0, null, null, null, instructionSets, null, null, null, 0, PackageParser.SigningDetails.UNKNOWN, - PackageManager.INSTALL_REASON_UNKNOWN); + PackageManager.INSTALL_REASON_UNKNOWN, null /* parent */); this.codeFile = (codePath != null) ? new File(codePath) : null; this.resourceFile = (resourcePath != null) ? new File(resourcePath) : null; } @@ -14614,7 +14753,7 @@ public class PackageManagerService extends IPackageManager.Stub params.getUser(), null /* instruction sets */, params.packageAbiOverride, params.grantedRuntimePermissions, params.traceMethod, params.traceCookie, params.signingDetails, - params.installReason); + params.installReason, params.mParentInstallParams); } int copyApk() { @@ -15019,10 +15158,10 @@ public class PackageManagerService extends IPackageManager.Stub } @GuardedBy({"mInstallLock", "mPackages"}) - private void installPackageTracedLI(InstallArgs args, PackageInstalledInfo installResult) { + private void installPackagesTracedLI(List requests) { try { - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackage"); - installPackagesLI(Collections.singletonList(new InstallRequest(args, installResult))); + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages"); + installPackagesLI(requests); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } @@ -15362,7 +15501,7 @@ public class PackageManagerService extends IPackageManager.Stub request.installResult.setError( PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE, "Duplicate package " + result.pkgSetting.pkg.packageName - + " in atomic install request."); + + " in multi-package install request."); return; } } @@ -23434,6 +23573,62 @@ public class PackageManagerService extends IPackageManager.Stub return mProtectedPackages.isPackageStateProtected(userId, packageName); } + + static class ActiveInstallSession { + private final String mPackageName; + private final File mStagedDir; + private final IPackageInstallObserver2 mObserver; + private final PackageInstaller.SessionParams mSessionParams; + private final String mInstallerPackageName; + private final int mInstallerUid; + private final UserHandle mUser; + private final SigningDetails mSigningDetails; + + ActiveInstallSession(String packageName, File stagedDir, IPackageInstallObserver2 observer, + PackageInstaller.SessionParams sessionParams, String installerPackageName, + int installerUid, UserHandle user, SigningDetails signingDetails) { + mPackageName = packageName; + mStagedDir = stagedDir; + mObserver = observer; + mSessionParams = sessionParams; + mInstallerPackageName = installerPackageName; + mInstallerUid = installerUid; + mUser = user; + mSigningDetails = signingDetails; + } + + public String getPackageName() { + return mPackageName; + } + + public File getStagedDir() { + return mStagedDir; + } + + public IPackageInstallObserver2 getObserver() { + return mObserver; + } + + public PackageInstaller.SessionParams getSessionParams() { + return mSessionParams; + } + + public String getInstallerPackageName() { + return mInstallerPackageName; + } + + public int getInstallerUid() { + return mInstallerUid; + } + + public UserHandle getUser() { + return mUser; + } + + public SigningDetails getSigningDetails() { + return mSigningDetails; + } + } } interface PackageSender { diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 38bd1722e61f7..31711e5244459 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -173,6 +173,8 @@ class PackageManagerShellCommand extends ShellCommand { return runSetInstallLocation(); case "get-install-location": return runGetInstallLocation(); + case "install-add-session": + return runInstallAddSession(); case "move-package": return runMovePackage(); case "move-primary-storage": @@ -983,6 +985,23 @@ class PackageManagerShellCommand extends ShellCommand { return doWriteSplit(sessionId, path, sizeBytes, splitName, true /*logSuccess*/); } + private int runInstallAddSession() throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + final int parentSessionId = Integer.parseInt(getNextArg()); + + List otherSessionIds = new ArrayList<>(); + String opt; + while ((opt = getNextArg()) != null) { + otherSessionIds.add(Integer.parseInt(opt)); + } + if (otherSessionIds.size() == 0) { + pw.println("Error: At least two sessions are required."); + return 1; + } + return doInstallAddSession(parentSessionId, ArrayUtils.convertToIntArray(otherSessionIds), + true /*logSuccess*/); + } + private int runInstallRemove() throws RemoteException { final PrintWriter pw = getOutPrintWriter(); @@ -2268,6 +2287,9 @@ class PackageManagerShellCommand extends ShellCommand { case "--apex": sessionParams.installFlags |= PackageManager.INSTALL_APEX; break; + case "--multi-package": + sessionParams.setMultiPackage(); + break; default: throw new IllegalArgumentException("Unknown option " + opt); } @@ -2500,6 +2522,30 @@ class PackageManagerShellCommand extends ShellCommand { } } + private int doInstallAddSession(int parentId, int[] sessionIds, boolean logSuccess) + throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + PackageInstaller.Session session = null; + try { + session = new PackageInstaller.Session( + mInterface.getPackageInstaller().openSession(parentId)); + if (!session.isMultiPackage()) { + getErrPrintWriter().println( + "Error: parent session ID is not a multi-package session"); + return 1; + } + for (int i = 0; i < sessionIds.length; i++) { + session.addChildSessionId(sessionIds[i]); + } + if (logSuccess) { + pw.println("Success"); + } + return 0; + } finally { + IoUtils.closeQuietly(session); + } + } + private int doRemoveSplit(int sessionId, String splitName, boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); @@ -2521,24 +2567,26 @@ class PackageManagerShellCommand extends ShellCommand { } } - private int doCommitSession(int sessionId, boolean logSuccess) throws RemoteException { + private int doCommitSession(int sessionId, boolean logSuccess) + throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); PackageInstaller.Session session = null; try { session = new PackageInstaller.Session( mInterface.getPackageInstaller().openSession(sessionId)); - - // Sanity check that all .dm files match an apk. - // (The installer does not support standalone .dm files and will not process them.) - try { - DexMetadataHelper.validateDexPaths(session.getNames()); - } catch (IllegalStateException | IOException e) { - pw.println("Warning [Could not validate the dex paths: " + e.getMessage() + "]"); + if (!session.isMultiPackage()) { + // Sanity check that all .dm files match an apk. + // (The installer does not support standalone .dm files and will not process them.) + try { + DexMetadataHelper.validateDexPaths(session.getNames()); + } catch (IllegalStateException | IOException e) { + pw.println( + "Warning [Could not validate the dex paths: " + e.getMessage() + "]"); + } } - final LocalIntentReceiver receiver = new LocalIntentReceiver(); session.commit(receiver.getIntentSender()); - final Intent result = receiver.getResult(); final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); @@ -2809,6 +2857,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instantapp] [--full] [--dont-kill]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); + pw.println(" [--multi-package]"); 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(""); @@ -2817,6 +2866,9 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" will be read from stdin. Options are:"); pw.println(" -S: size in bytes of package, required for stdin"); pw.println(""); + pw.println(" install-add-session MULTI_PACKAGE_SESSION_ID CHILD_SESSION_IDs"); + pw.println(" Add one or more session IDs to a multi-package session."); + pw.println(""); pw.println(" install-commit SESSION_ID"); pw.println(" Commit the given active install session, installing the app."); pw.println(""); diff --git a/services/core/java/com/android/server/pm/PackageSessionProvider.java b/services/core/java/com/android/server/pm/PackageSessionProvider.java new file mode 100644 index 0000000000000..af11e77a510be --- /dev/null +++ b/services/core/java/com/android/server/pm/PackageSessionProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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.pm; + +/** Provides access to individual sessions managed by the install service */ +public interface PackageSessionProvider { + + /** + * Get the sessions for the provided session IDs. Null will be returned for session IDs that + * do not exist. + */ + PackageInstallerSession getSession(int sessionId); + +}