diff --git a/core/api/current.txt b/core/api/current.txt index 19123bba2b5d8..825e73bd4aacc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -11876,6 +11876,7 @@ package android.content.pm { public class PackageInstaller { method public void abandonSession(int); method public void checkInstallConstraints(@NonNull java.util.List, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer); + method public void commitSessionAfterInstallConstraintsAreMet(int, @NonNull android.content.IntentSender, @NonNull android.content.pm.PackageInstaller.InstallConstraints, long); method public int createSession(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException; method @Deprecated @Nullable public android.content.pm.PackageInstaller.SessionInfo getActiveStagedSession(); method @NonNull public java.util.List getActiveStagedSessions(); @@ -11920,6 +11921,7 @@ package android.content.pm { field public static final int STATUS_FAILURE_INCOMPATIBLE = 7; // 0x7 field public static final int STATUS_FAILURE_INVALID = 4; // 0x4 field public static final int STATUS_FAILURE_STORAGE = 6; // 0x6 + field public static final int STATUS_FAILURE_TIMEOUT = 8; // 0x8 field public static final int STATUS_PENDING_USER_ACTION = -1; // 0xffffffff field public static final int STATUS_SUCCESS = 0; // 0x0 } diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index c9a56324de794..9dd9c0f757d52 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -46,6 +46,8 @@ interface IPackageInstallerSession { void commit(in IntentSender statusReceiver, boolean forTransferred); void transfer(in String packageName); void abandon(); + void seal(); + List fetchPackageNames(); DataLoaderParamsParcel getDataLoaderParams(); void addFile(int location, String name, long lengthBytes, in byte[] metadata, in byte[] signature); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 812b5b3145828..df1340d318d8a 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -44,8 +44,11 @@ import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.ActivityManager; +import android.app.ActivityThread; import android.app.AppGlobals; import android.compat.annotation.UnsupportedAppUsage; +import android.content.IIntentReceiver; +import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager.DeleteFlags; @@ -59,9 +62,11 @@ import android.graphics.Bitmap; import android.icu.util.ULocale; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.os.FileBridge; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; @@ -226,8 +231,8 @@ public class PackageInstaller { * {@link #STATUS_PENDING_USER_ACTION}, {@link #STATUS_SUCCESS}, * {@link #STATUS_FAILURE}, {@link #STATUS_FAILURE_ABORTED}, * {@link #STATUS_FAILURE_BLOCKED}, {@link #STATUS_FAILURE_CONFLICT}, - * {@link #STATUS_FAILURE_INCOMPATIBLE}, {@link #STATUS_FAILURE_INVALID}, or - * {@link #STATUS_FAILURE_STORAGE}. + * {@link #STATUS_FAILURE_INCOMPATIBLE}, {@link #STATUS_FAILURE_INVALID}, + * {@link #STATUS_FAILURE_STORAGE}, or {@link #STATUS_FAILURE_TIMEOUT}. *

* More information about a status may be available through additional * extras; see the individual status documentation for details. @@ -440,6 +445,13 @@ public class PackageInstaller { */ public static final int STATUS_FAILURE_INCOMPATIBLE = 7; + /** + * The operation failed because it didn't complete within the specified timeout. + * + * @see #EXTRA_STATUS_MESSAGE + */ + public static final int STATUS_FAILURE_TIMEOUT = 8; + /** * Default value, non-streaming installation session. * @@ -1015,6 +1027,61 @@ public class PackageInstaller { } } + /** + * Commit the session when all constraints are satisfied. This is a convenient method to + * combine {@link #waitForInstallConstraints(List, InstallConstraints, IntentSender, long)} + * and {@link Session#commit(IntentSender)}. + *

+ * Once this method is called, the session is sealed and no additional mutations + * may be performed on the session. In the case of timeout, you may commit the + * session again using this method or {@link Session#commit(IntentSender)} for retries. + * + * @param statusReceiver Called when the state of the session changes. Intents + * sent to this receiver contain {@link #EXTRA_STATUS}. + * Refer to the individual status codes on how to handle them. + * @param constraints The requirements to satisfy before committing the session. + * @param timeoutMillis The maximum time to wait, in milliseconds until the + * constraints are satisfied. The caller will be notified via + * {@code statusReceiver} if timeout happens before commit. + */ + public void commitSessionAfterInstallConstraintsAreMet(int sessionId, + @NonNull IntentSender statusReceiver, @NonNull InstallConstraints constraints, + @DurationMillisLong long timeoutMillis) { + try { + var session = mInstaller.openSession(sessionId); + session.seal(); + var packageNames = session.fetchPackageNames(); + var intentSender = new IntentSender((IIntentSender) new IIntentSender.Stub() { + @Override + public void send(int code, Intent intent, String resolvedType, + IBinder allowlistToken, IIntentReceiver finishedReceiver, + String requiredPermission, Bundle options) { + var result = intent.getParcelableExtra( + PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT, + InstallConstraintsResult.class); + try { + if (result.isAllConstraintsSatisfied()) { + session.commit(statusReceiver, false); + } else { + // timeout + final Intent fillIn = new Intent(); + fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS, STATUS_FAILURE_TIMEOUT); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, + "Install constraints not satisfied within timeout"); + statusReceiver.sendIntent( + ActivityThread.currentApplication(), 0, fillIn, null, null); + } + } catch (Exception ignore) { + } + } + }); + waitForInstallConstraints(packageNames, constraints, intentSender, timeoutMillis); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Events for observing session lifecycle. *

diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 8ea77889acc79..350f5ef7439c8 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -1842,6 +1842,60 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { dispatchSessionSealed(); } + @Override + public void seal() { + assertNotChild("seal"); + assertCallerIsOwnerOrRoot(); + try { + sealInternal(); + for (var child : getChildSessions()) { + child.sealInternal(); + } + } catch (PackageManagerException e) { + throw new IllegalStateException("Package is not valid", e); + } + } + + private void sealInternal() throws PackageManagerException { + synchronized (mLock) { + sealLocked(); + } + } + + @Override + public List fetchPackageNames() { + assertNotChild("fetchPackageNames"); + assertCallerIsOwnerOrRoot(); + var sessions = getSelfOrChildSessions(); + var result = new ArrayList(sessions.size()); + for (var s : sessions) { + result.add(s.fetchPackageName()); + } + return result; + } + + private String fetchPackageName() { + assertSealed("fetchPackageName"); + synchronized (mLock) { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final List addedFiles = getAddedApksLocked(); + for (File addedFile : addedFiles) { + final ParseResult result = + ApkLiteParseUtils.parseApkLite(input.reset(), addedFile, 0); + if (result.isError()) { + throw new IllegalStateException( + "Can't parse package for session=" + sessionId, result.getException()); + } + final ApkLite apk = result.getResult(); + var packageName = apk.getPackageName(); + if (packageName != null) { + return packageName; + } + } + throw new IllegalStateException("Can't fetch package name for session=" + sessionId); + } + } + /** * Kicks off the install flow. The first step is to persist 'sealed' flags * to prevent mutations of hard links created later. @@ -2095,6 +2149,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + @NonNull + private List getSelfOrChildSessions() { + return isMultiPackage() ? getChildSessions() : Collections.singletonList(this); + } + /** * Seal the session to prevent further modification. *