diff options
6 files changed, 465 insertions, 84 deletions
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 0b3c7654f4fb..e9546353d5f3 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -39,6 +39,9 @@ interface IPackageInstallerSession { void transfer(in String packageName, in IntentSender statusReceiver); void abandon(); + void addFile(String name, long lengthBytes, in byte[] metadata); + void removeFile(String name); + boolean isMultiPackage(); int[] getChildSessionIds(); void addChildSessionId(in int sessionId); @@ -46,5 +49,4 @@ interface IPackageInstallerSession { int getParentSessionId(); boolean isStaged(); - void addFile(in String name, long size, in byte[] metadata); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 8f5143518cbe..898631e9a3b1 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -231,6 +231,15 @@ public class PackageInstaller { public static final String EXTRA_CALLBACK = "android.content.pm.extra.CALLBACK"; /** + * Streaming installation pending. + * Caller should make sure DataLoader is able to prepare image and reinitiate the operation. + * + * @see #EXTRA_SESSION_ID + * {@hide} + */ + public static final int STATUS_PENDING_STREAMING = -2; + + /** * User action is currently required to proceed. You can launch the intent * activity described by {@link Intent#EXTRA_INTENT} to involve the user and * continue. @@ -1059,13 +1068,56 @@ public class PackageInstaller { } } + + /** + * Adds a file to session. On commit this file will be pulled from dataLoader. + * + * @param name arbitrary, unique name of your choosing to identify the + * APK being written. You can open a file again for + * additional writes (such as after a reboot) by using the + * same name. This name is only meaningful within the context + * of a single install session. + * @param lengthBytes total size of the file being written. + * The system may clear various caches as needed to allocate + * this space. + * @param metadata additional info use by dataLoader to pull data for the file. + * @throws SecurityException if called after the session has been + * sealed or abandoned + * @throws IllegalStateException if called for non-callback session + * {@hide} + */ + public void addFile(@NonNull String name, long lengthBytes, @NonNull byte[] metadata) { + try { + mSession.addFile(name, lengthBytes, metadata); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a file. + * + * @param name name of a file, e.g. split. + * @throws SecurityException if called after the session has been + * sealed or abandoned + * @throws IllegalStateException if called for non-callback session + * {@hide} + */ + public void removeFile(@NonNull String name) { + try { + mSession.removeFile(name); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Attempt to commit everything staged in this session. This may require * user intervention, and so it may not happen immediately. The final * result of the commit will be reported through the given callback. * <p> - * Once this method is called, the session is sealed and no additional - * mutations may be performed on the session. If the device reboots + * Once this method is called, the session is sealed and no additional mutations may be + * performed on the session. In case of device reboot or data loader transient failure * before the session has been finalized, you may commit the session again. * <p> * If the installer is the device owner or the affiliated profile owner, there will be no @@ -1220,27 +1272,6 @@ public class PackageInstaller { } /** - * Configure files for an installation session. - * - * Currently only for Incremental installation session. Once this method is called, - * the files and their paths, as specified in the parameters, will be created and properly - * configured in the Incremental File System. - * - * TODO(b/136132412): update this and InstallationFile class with latest API design. - * - * @throws IllegalStateException if {@link SessionParams#incrementalParams} is null. - * - * @hide - */ - public void addFile(@NonNull String name, long size, @NonNull byte[] metadata) { - try { - mSession.addFile(name, size, metadata); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** * Release this session object. You can open the session again if it * hasn't been finalized. */ @@ -1429,6 +1460,9 @@ public class PackageInstaller { public long requiredInstalledVersionCode = PackageManager.VERSION_CODE_HIGHEST; /** {@hide} */ public IncrementalDataLoaderParams incrementalParams; + /** TODO(b/146080380): add a class name to make it fully compatible with ComponentName. + * {@hide} */ + public String dataLoaderPackageName; /** * Construct parameters for a new package install session. @@ -1468,6 +1502,7 @@ public class PackageInstaller { incrementalParams = new IncrementalDataLoaderParams( dataLoaderParamsParcel); } + dataLoaderPackageName = source.readString(); } /** {@hide} */ @@ -1492,6 +1527,7 @@ public class PackageInstaller { ret.isStaged = isStaged; ret.requiredInstalledVersionCode = requiredInstalledVersionCode; ret.incrementalParams = incrementalParams; + ret.dataLoaderPackageName = dataLoaderPackageName; return ret; } @@ -1831,6 +1867,20 @@ public class PackageInstaller { this.incrementalParams = incrementalParams; } + /** + * Set the data provider params for the session. + * This also switches installation into callback mode and disallow direct writes into + * staging folder. + * TODO(b/146080380): unify dataprovider params with Incremental. + * + * @param dataLoaderPackageName name of the dataLoader package + * {@hide} + */ + @RequiresPermission(Manifest.permission.INSTALL_PACKAGES) + public void setDataLoaderPackageName(String dataLoaderPackageName) { + this.dataLoaderPackageName = dataLoaderPackageName; + } + /** {@hide} */ public void dump(IndentingPrintWriter pw) { pw.printPair("mode", mode); @@ -1851,6 +1901,7 @@ public class PackageInstaller { pw.printPair("isMultiPackage", isMultiPackage); pw.printPair("isStaged", isStaged); pw.printPair("requiredInstalledVersionCode", requiredInstalledVersionCode); + pw.printPair("dataLoaderPackageName", dataLoaderPackageName); pw.println(); } @@ -1885,6 +1936,7 @@ public class PackageInstaller { } else { dest.writeParcelable(null, flags); } + dest.writeString(dataLoaderPackageName); } public static final Parcelable.Creator<SessionParams> diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index dceca0a749e3..a54534b461f1 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -637,7 +637,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this, mInstallThread.getLooper(), mStagingManager, sessionId, userId, callingUid, installSource, params, createdMillis, - stageDir, stageCid, false, false, false, null, SessionInfo.INVALID_ID, + stageDir, stageCid, null, false, false, false, null, SessionInfo.INVALID_ID, false, false, false, SessionInfo.STAGED_SESSION_NO_ERROR, ""); synchronized (mSessions) { @@ -1014,12 +1014,28 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + static void sendPendingStreaming(Context context, IntentSender target, int sessionId, + Throwable cause) { + final Intent intent = new Intent(); + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_STREAMING); + if (cause != null && !TextUtils.isEmpty(cause.getMessage())) { + intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, + "Staging Image Not Ready [" + cause.getMessage() + "]"); + } else { + intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, "Staging Image Not Ready"); + } + try { + target.sendIntent(context, 0, intent, null, null); + } catch (SendIntentException ignored) { + } + } + static void sendOnUserActionRequired(Context context, IntentSender target, int sessionId, Intent intent) { final Intent fillIn = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); - fillIn.putExtra(PackageInstaller.EXTRA_STATUS, - PackageInstaller.STATUS_PENDING_USER_ACTION); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_USER_ACTION); fillIn.putExtra(Intent.EXTRA_INTENT, intent); try { target.sendIntent(context, 0, fillIn, null, null); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index d6ca86af34c7..286d291836ec 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -21,6 +21,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; +import static android.content.pm.PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE; import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SPLIT; import static android.content.pm.PackageParser.APEX_FILE_EXTENSION; import static android.content.pm.PackageParser.APK_FILE_EXTENSION; @@ -30,11 +31,13 @@ import static android.system.OsConstants.O_WRONLY; import static com.android.internal.util.XmlUtils.readBitmapAttribute; import static com.android.internal.util.XmlUtils.readBooleanAttribute; +import static com.android.internal.util.XmlUtils.readByteArrayAttribute; import static com.android.internal.util.XmlUtils.readIntAttribute; import static com.android.internal.util.XmlUtils.readLongAttribute; import static com.android.internal.util.XmlUtils.readStringAttribute; import static com.android.internal.util.XmlUtils.readUriAttribute; import static com.android.internal.util.XmlUtils.writeBooleanAttribute; +import static com.android.internal.util.XmlUtils.writeByteArrayAttribute; import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.internal.util.XmlUtils.writeLongAttribute; import static com.android.internal.util.XmlUtils.writeStringAttribute; @@ -123,6 +126,7 @@ import java.io.FileDescriptor; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; @@ -142,6 +146,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"; + static final String TAG_SESSION_FILE = "sessionFile"; private static final String TAG_GRANTED_RUNTIME_PERMISSION = "granted-runtime-permission"; private static final String TAG_WHITELISTED_RESTRICTED_PERMISSION = "whitelisted-restricted-permission"; @@ -183,6 +188,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_VOLUME_UUID = "volumeUuid"; private static final String ATTR_NAME = "name"; private static final String ATTR_INSTALL_REASON = "installRason"; + private static final String ATTR_DATA_LOADER_PACKAGE_NAME = "dataLoaderPackageName"; + private static final String ATTR_LENGTH_BYTES = "lengthBytes"; + private static final String ATTR_METADATA = "metadata"; private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill"; private static final int[] EMPTY_CHILD_SESSION_ARRAY = {}; @@ -278,6 +286,29 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private int mParentSessionId; + static class FileInfo { + public final String name; + public final Long lengthBytes; + public final byte[] metadata; + + public static FileInfo added(String name, Long lengthBytes, byte[] metadata) { + return new FileInfo(name, lengthBytes, metadata); + } + + public static FileInfo removed(String name) { + return new FileInfo(name, -1L, null); + } + + FileInfo(String name, Long lengthBytes, byte[] metadata) { + this.name = name; + this.lengthBytes = lengthBytes; + this.metadata = metadata; + } + } + + @GuardedBy("mLock") + private ArrayList<FileInfo> mFiles = new ArrayList<>(); + @GuardedBy("mLock") private boolean mStagedSessionApplied; @GuardedBy("mLock") @@ -313,6 +344,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private boolean mVerityFound; + // TODO(b/146080380): merge file list with Callback installation. private IncrementalFileStorages mIncrementalFileStorages; private static final FileFilter sAddedFilter = new FileFilter() { @@ -344,7 +376,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { IntentSender statusReceiver; switch (msg.what) { case MSG_SEAL: - handleSeal((IntentSender) msg.obj); + statusReceiver = (IntentSender) msg.obj; + + handleSeal(statusReceiver); break; case MSG_COMMIT: handleCommit(); @@ -378,6 +412,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } }; + private boolean isDataLoaderInstallation() { + return !TextUtils.isEmpty(params.dataLoaderPackageName); + } + /** * @return {@code true} iff the installing is app an device owner or affiliated profile owner. */ @@ -435,7 +473,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { PackageSessionProvider sessionProvider, Looper looper, StagingManager stagingManager, int sessionId, int userId, int installerUid, @NonNull InstallSource installSource, SessionParams params, long createdMillis, - File stageDir, String stageCid, boolean prepared, boolean committed, boolean sealed, + File stageDir, String stageCid, FileInfo[] files, boolean prepared, + boolean committed, boolean sealed, @Nullable int[] childSessionIds, int parentSessionId, boolean isReady, boolean isFailed, boolean isApplied, int stagedSessionErrorCode, String stagedSessionErrorMessage) { @@ -464,6 +503,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } this.mParentSessionId = parentSessionId; + if (files != null) { + for (FileInfo file : files) { + mFiles.add(file); + } + } + if (!params.isMultiPackage && (stageDir == null) == (stageCid == null)) { throw new IllegalArgumentException( "Exactly one of stageDir or stageCid stage must be set"); @@ -592,15 +637,19 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + @GuardedBy("mLock") + private void setClientProgressLocked(float progress) { + // Always publish first staging movement + final boolean forcePublish = (mClientProgress == 0); + mClientProgress = progress; + computeProgressLocked(forcePublish); + } + @Override public void setClientProgress(float progress) { synchronized (mLock) { assertCallerIsOwnerOrRootLocked(); - - // Always publish first staging movement - final boolean forcePublish = (mClientProgress == 0); - mClientProgress = progress; - computeProgressLocked(forcePublish); + setClientProgressLocked(progress); } } @@ -608,8 +657,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { public void addClientProgress(float progress) { synchronized (mLock) { assertCallerIsOwnerOrRootLocked(); - - setClientProgress(mClientProgress + progress); + setClientProgressLocked(mClientProgress + progress); } } @@ -637,7 +685,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private String[] getNamesLocked() { - return stageDir.list(); + if (!isDataLoaderInstallation()) { + return stageDir.list(); + } + return mFiles.stream().map(fileInfo -> fileInfo.name).toArray(String[]::new); } private static File[] filterFiles(File parent, String[] names, FileFilter filter) { @@ -659,6 +710,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public void removeSplit(String splitName) { + if (isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot remove splits in a callback installation session."); + } if (TextUtils.isEmpty(params.appPackageName)) { throw new IllegalStateException("Must specify package name to remove a split"); } @@ -693,8 +748,31 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + private void assertCanWrite(boolean reverseMode) { + if (isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot write regular files in a callback installation session."); + } + synchronized (mLock) { + assertCallerIsOwnerOrRootLocked(); + assertPreparedAndNotSealedLocked("assertCanWrite"); + } + if (reverseMode) { + switch (Binder.getCallingUid()) { + case android.os.Process.SHELL_UID: + case android.os.Process.ROOT_UID: + case android.os.Process.SYSTEM_UID: + break; + default: + throw new SecurityException( + "Reverse mode only supported from shell or system"); + } + } + } + @Override public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { + assertCanWrite(false); try { return doWriteInternal(name, offsetBytes, lengthBytes, null); } catch (IOException e) { @@ -705,6 +783,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public void write(String name, long offsetBytes, long lengthBytes, ParcelFileDescriptor fd) { + assertCanWrite(fd != null); try { doWriteInternal(name, offsetBytes, lengthBytes, fd); } catch (IOException e) { @@ -720,9 +799,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final RevocableFileDescriptor fd; final FileBridge bridge; synchronized (mLock) { - assertCallerIsOwnerOrRootLocked(); - assertPreparedAndNotSealedLocked("openWrite"); - if (PackageInstaller.ENABLE_REVOCABLE_FD) { fd = new RevocableFileDescriptor(); bridge = null; @@ -765,16 +841,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } if (incomingFd != null) { - switch (Binder.getCallingUid()) { - case android.os.Process.SHELL_UID: - case android.os.Process.ROOT_UID: - case android.os.Process.SYSTEM_UID: - break; - default: - throw new SecurityException( - "Reverse mode only supported from shell or system"); - } - // In "reverse" mode, we're streaming data ourselves from the // incoming FD, which means we never have to hand out our // sensitive internal FD. We still rely on a "bridge" being @@ -786,7 +852,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (params.sizeBytes > 0) { final long delta = progress - last.value; last.value = progress; - addClientProgress((float) delta / (float) params.sizeBytes); + synchronized (mLock) { + setClientProgressLocked(mClientProgress + + (float) delta / (float) params.sizeBytes); + } } }); } finally { @@ -821,6 +890,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public ParcelFileDescriptor openRead(String name) { + if (isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot read regular files in a callback installation session."); + } synchronized (mLock) { assertCallerIsOwnerOrRootLocked(); assertPreparedAndNotCommittedOrDestroyedLocked("openRead"); @@ -926,6 +999,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return; } } + mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); } @@ -988,6 +1062,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + /** {@hide} */ + private class StreamingException extends Exception { + StreamingException(Throwable cause) { + super(cause); + } + } + + /** * Sanity checks to make sure it's ok to commit the session. */ @@ -1039,12 +1121,22 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } wasSealed = mSealed; - if (!mSealed) { + try { + if (!mSealed) { + sealLocked(childSessions); + } + try { - sealAndValidateLocked(childSessions); - } catch (PackageManagerException e) { + streamAndValidateLocked(); + } catch (StreamingException e) { + // In case of streaming failure we don't want to fail or commit the session. + // Just return from this method and allow caller to commit again. + PackageInstallerService.sendPendingStreaming(mContext, mRemoteStatusReceiver, + sessionId, e); return false; } + } catch (PackageManagerException e) { + return false; } // Client staging is fully done at this point @@ -1131,17 +1223,26 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } /** - * Seal the session to prevent further modification and validate the contents of it. + * Convenience wrapper, see {@link #sealLocked(List<PackageInstallerSession>) seal} and + * {@link #streamAndValidateLocked()}. + */ + @GuardedBy("mLock") + private void sealAndValidateLocked(List<PackageInstallerSession> childSessions) + throws PackageManagerException, StreamingException { + sealLocked(childSessions); + streamAndValidateLocked(); + } + + /** + * Seal the session to prevent further modification. * * <p>The session will be sealed after calling this method even if it failed. * - * @param childSessions the child sessions of a multipackage that will be checked for - * consistency. Can be null if session is not multipackage. * @throws PackageManagerException if the session was sealed but something went wrong. If the * session was sealed this is the only possible exception. */ @GuardedBy("mLock") - private void sealAndValidateLocked(List<PackageInstallerSession> childSessions) + private void sealLocked(List<PackageInstallerSession> childSessions) throws PackageManagerException { try { assertNoWriteFileTransfersOpenLocked(); @@ -1152,7 +1253,26 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (childSessions != null) { assertMultiPackageConsistencyLocked(childSessions); } + } catch (PackageManagerException e) { + throw onSessionVerificationFailure(e); + } catch (Throwable e) { + // Convert all exceptions into package manager exceptions as only those are handled + // in the code above. + throw onSessionVerificationFailure(new PackageManagerException(e)); + } + } + /** + * Prepare DataLoader and stream content for DataLoader sessions. + * Validate the contents of all session. + * + * @throws StreamingException if streaming failed. + * @throws PackageManagerException if validation failed. + */ + @GuardedBy("mLock") + private void streamAndValidateLocked() + throws PackageManagerException, StreamingException { + try { // Read transfers from the original owner stay open, but as the session's data cannot // be modified anymore, there is no leak of information. For staged sessions, further // validation is performed by the staging manager. @@ -1161,6 +1281,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { params.appPackageName, PackageManager.GET_SIGNATURES | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId); + prepareDataLoader(); + if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) { validateApexInstallLocked(); } else { @@ -1173,6 +1295,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } catch (PackageManagerException e) { throw onSessionVerificationFailure(e); + } catch (StreamingException e) { + throw e; } catch (Throwable e) { // Convert all exceptions into package manager exceptions as only those are handled // in the code above. @@ -1208,6 +1332,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { synchronized (mLock) { try { sealAndValidateLocked(childSessions); + } catch (StreamingException e) { + Slog.e(TAG, "Streaming failed", e); } catch (PackageManagerException e) { Slog.e(TAG, "Package not valid", e); } @@ -1277,6 +1403,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { sealAndValidateLocked(childSessions); + } catch (StreamingException e) { + throw new IllegalArgumentException("Streaming failed", e); } catch (PackageManagerException e) { throw new IllegalArgumentException("Package is not valid", e); } @@ -2224,6 +2352,132 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @Override + public void addFile(String name, long lengthBytes, byte[] metadata) { + if (mIncrementalFileStorages != null) { + try { + mIncrementalFileStorages.addFile(new InstallationFile(name, lengthBytes, metadata)); + } catch (IOException ex) { + throw new IllegalStateException( + "Failed to add and configure Incremental File: " + name, ex); + } + } + if (!isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot add files to non-callback installation session."); + } + // Use installer provided name for now; we always rename later + if (!FileUtils.isValidExtFilename(name)) { + throw new IllegalArgumentException("Invalid name: " + name); + } + + synchronized (mLock) { + assertCallerIsOwnerOrRootLocked(); + assertPreparedAndNotSealedLocked("addFile"); + + mFiles.add(FileInfo.added(name, lengthBytes, metadata)); + } + } + + @Override + public void removeFile(String name) { + if (!isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot add files to non-callback installation session."); + } + if (TextUtils.isEmpty(params.appPackageName)) { + throw new IllegalStateException("Must specify package name to remove a split"); + } + + synchronized (mLock) { + assertCallerIsOwnerOrRootLocked(); + assertPreparedAndNotSealedLocked("removeFile"); + + mFiles.add(FileInfo.removed(getRemoveMarkerName(name))); + } + } + + /** + * Makes sure files are present in staging location. + */ + private void prepareDataLoader() + throws PackageManagerException, StreamingException { + if (!isDataLoaderInstallation()) { + return; + } + + FilesystemConnector connector = new FilesystemConnector(); + + FileInfo[] addedFiles = mFiles.stream().filter( + file -> sAddedFilter.accept(new File(file.name))).toArray(FileInfo[]::new); + String[] removedFiles = mFiles.stream().filter( + file -> sRemovedFilter.accept(new File(file.name))).map( + file -> file.name.substring(0, + file.name.length() - REMOVE_MARKER_EXTENSION.length())).toArray( + String[]::new); + + DataLoader dataLoader = new DataLoader(); + try { + dataLoader.onCreate(connector); + + if (!dataLoader.onPrepareImage(addedFiles, removedFiles)) { + throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "Failed to prepare image."); + } + } catch (IOException e) { + throw new StreamingException(e); + } finally { + dataLoader.onDestroy(); + } + } + + // TODO(b/146080380): implement DataLoader using Incremental infrastructure. + class FilesystemConnector { + void writeData(FileInfo fileInfo, long offset, long lengthBytes, + ParcelFileDescriptor incomingFd) throws IOException { + doWriteInternal(fileInfo.name, offset, lengthBytes, incomingFd); + } + } + + static class DataLoader { + private ParcelFileDescriptor mInFd = null; + private FilesystemConnector mConnector = null; + + void onCreate(FilesystemConnector connector) throws IOException { + mConnector = connector; + } + + void onDestroy() { + IoUtils.closeQuietly(mInFd); + } + + private static final String STDIN_PATH = "-"; + boolean onPrepareImage(FileInfo[] addedFiles, String[] removedFiles) throws IOException { + for (FileInfo fileInfo : addedFiles) { + String filePath = new String(fileInfo.metadata, StandardCharsets.UTF_8); + if (STDIN_PATH.equals(filePath) || TextUtils.isEmpty(filePath)) { + if (mInFd == null) { + Slog.e(TAG, "Invalid stdin file descriptor."); + return false; + } + ParcelFileDescriptor inFd = ParcelFileDescriptor.dup(mInFd.getFileDescriptor()); + mConnector.writeData(fileInfo, 0, fileInfo.lengthBytes, inFd); + } else { + File localFile = new File(filePath); + ParcelFileDescriptor incomingFd = null; + try { + incomingFd = ParcelFileDescriptor.open(localFile, + ParcelFileDescriptor.MODE_READ_ONLY); + mConnector.writeData(fileInfo, 0, localFile.length(), incomingFd); + } finally { + IoUtils.closeQuietly(incomingFd); + } + } + } + return true; + } + } + + @Override public int[] getChildSessionIds() { final int[] childSessionIds = mChildSessionIds.copyKeys(); if (childSessionIds != null) { @@ -2295,20 +2549,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return mParentSessionId; } - @Override - public void addFile(@NonNull String name, long size, @NonNull byte[] metadata) { - if (mIncrementalFileStorages == null) { - throw new IllegalStateException( - "Cannot add Incremental File to a non-Incremental session."); - } - try { - mIncrementalFileStorages.addFile(new InstallationFile(name, size, metadata)); - } catch (IOException ex) { - throw new IllegalStateException( - "Failed to add and configure Incremental File: " + name, ex); - } - } - private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { final IntentSender statusReceiver; final String packageName; @@ -2321,7 +2561,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } if (statusReceiver != null) { - // Execute observer.onPackageInstalled on different tread as we don't want callers + // Execute observer.onPackageInstalled on different thread as we don't want callers // inside the system server have to worry about catching the callbacks while they are // calling into the session final SomeArgs args = SomeArgs.obtain(); @@ -2595,6 +2835,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid); writeIntAttribute(out, ATTR_INSTALL_REASON, params.installReason); + writeStringAttribute(out, ATTR_DATA_LOADER_PACKAGE_NAME, params.dataLoaderPackageName); + writeGrantedRuntimePermissionsLocked(out, params.grantedRuntimePermissions); writeWhitelistedRestrictedPermissionsLocked(out, params.whitelistedRestrictedPermissions); @@ -2624,6 +2866,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { writeIntAttribute(out, ATTR_SESSION_ID, childSessionId); out.endTag(null, TAG_CHILD_SESSION); } + for (FileInfo fileInfo : mFiles) { + out.startTag(null, TAG_SESSION_FILE); + writeStringAttribute(out, ATTR_NAME, fileInfo.name); + writeLongAttribute(out, ATTR_LENGTH_BYTES, fileInfo.lengthBytes); + writeByteArrayAttribute(out, ATTR_METADATA, fileInfo.metadata); + out.endTag(null, TAG_SESSION_FILE); + } } out.endTag(null, TAG_SESSION); @@ -2697,6 +2946,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID); params.installReason = readIntAttribute(in, ATTR_INSTALL_REASON); + params.dataLoaderPackageName = readStringAttribute(in, ATTR_DATA_LOADER_PACKAGE_NAME); + final File appIconFile = buildAppIconFile(sessionId, sessionsDir); if (appIconFile.exists()) { params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath()); @@ -2723,6 +2974,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { List<String> grantedRuntimePermissions = new ArrayList<>(); List<String> whitelistedRestrictedPermissions = new ArrayList<>(); List<Integer> childSessionIds = new ArrayList<>(); + List<FileInfo> files = new ArrayList<>(); int outerDepth = in.getDepth(); int type; while ((type = in.next()) != XmlPullParser.END_DOCUMENT @@ -2740,6 +2992,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (TAG_CHILD_SESSION.equals(in.getName())) { childSessionIds.add(readIntAttribute(in, ATTR_SESSION_ID, SessionInfo.INVALID_ID)); } + if (TAG_SESSION_FILE.equals(in.getName())) { + files.add(new FileInfo(readStringAttribute(in, ATTR_NAME), + readLongAttribute(in, ATTR_LENGTH_BYTES, -1), + readByteArrayAttribute(in, ATTR_METADATA))); + } } if (grantedRuntimePermissions.size() > 0) { @@ -2758,11 +3015,16 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { childSessionIdsArray = EMPTY_CHILD_SESSION_ARRAY; } + FileInfo[] fileInfosArray = null; + if (!files.isEmpty()) { + fileInfosArray = files.stream().toArray(FileInfo[]::new); + } + InstallSource installSource = InstallSource.create(installInitiatingPackageName, installOriginatingPackageName, installerPackageName, false); return new PackageInstallerSession(callback, context, pm, sessionProvider, installerThread, stagingManager, sessionId, userId, installerUid, - installSource, params, createdMillis, stageDir, stageCid, + installSource, params, createdMillis, stageDir, stageCid, fileInfosArray, prepared, committed, sealed, childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, stagedSessionErrorCode, stagedSessionErrorMessage); } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index fff404f3ede6..dfffbd6cabef 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -114,6 +114,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -135,6 +136,8 @@ class PackageManagerShellCommand extends ShellCommand { private final static String ART_PROFILE_SNAPSHOT_DEBUG_LOCATION = "/data/misc/profman/"; private static final int DEFAULT_WAIT_MS = 60 * 1000; + private static final String PM_SHELL_DATALOADER = "com.android.pm.dataloader"; + final IPackageManager mInterface; final IPermissionManager mPermissionManager; final private WeakHashMap<String, Resources> mResourceCache = @@ -175,6 +178,8 @@ class PackageManagerShellCommand extends ShellCommand { return runQueryIntentReceivers(); case "install": return runInstall(); + case "install-streaming": + return runStreamingInstall(); case "install-abandon": case "install-destroy": return runInstallAbandon(); @@ -1152,9 +1157,21 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + private int runStreamingInstall() throws RemoteException { + final InstallParams params = makeInstallParams(); + if (TextUtils.isEmpty(params.sessionParams.dataLoaderPackageName)) { + params.sessionParams.setDataLoaderPackageName(PM_SHELL_DATALOADER); + } + return doRunInstall(params); + } + private int runInstall() throws RemoteException { + return doRunInstall(makeInstallParams()); + } + + private int doRunInstall(final InstallParams params) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); - final InstallParams params = makeInstallParams(); + final boolean streaming = !TextUtils.isEmpty(params.sessionParams.dataLoaderPackageName); ArrayList<String> inPaths = getRemainingArgs(); if (inPaths.isEmpty()) { @@ -1181,17 +1198,30 @@ class PackageManagerShellCommand extends ShellCommand { return 1; } - setParamsSize(params, inPaths); + if (!streaming) { + setParamsSize(params, inPaths); + } + final int sessionId = doCreateSession(params.sessionParams, params.installerPackageName, params.userId); boolean abandonSession = true; try { for (String inPath : inPaths) { - String splitName = hasSplits ? (new File(inPath)).getName() - : "base." + (isApex ? "apex" : "apk"); - if (doWriteSplit(sessionId, inPath, params.sessionParams.sizeBytes, splitName, - false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { - return 1; + if (streaming) { + String name = new File(inPath).getName(); + byte[] metadata = inPath.getBytes(StandardCharsets.UTF_8); + if (doAddFile(sessionId, name, params.sessionParams.sizeBytes, metadata, + false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { + return 1; + } + } else { + String splitName = hasSplits ? new File(inPath).getName() + : "base." + (isApex ? "apex" : "apk"); + + if (doWriteSplit(sessionId, inPath, params.sessionParams.sizeBytes, splitName, + false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { + return 1; + } } } if (doCommitSession(sessionId, false /*logSuccess*/) @@ -2927,11 +2957,32 @@ class PackageManagerShellCommand extends ShellCommand { return sessionId; } + private int doAddFile(int sessionId, String name, long sizeBytes, byte[] metadata, + boolean logSuccess) throws RemoteException { + PackageInstaller.Session session = new PackageInstaller.Session( + mInterface.getPackageInstaller().openSession(sessionId)); + try { + session.addFile(name, sizeBytes, metadata); + + if (logSuccess) { + getOutPrintWriter().println("Success"); + } + + return 0; + } finally { + IoUtils.closeQuietly(session); + } + } + private int doWriteSplit(int sessionId, String inPath, long sizeBytes, String splitName, boolean logSuccess) throws RemoteException { PackageInstaller.Session session = null; try { + session = new PackageInstaller.Session( + mInterface.getPackageInstaller().openSession(sessionId)); + final PrintWriter pw = getOutPrintWriter(); + final ParcelFileDescriptor fd; if (STDIN_PATH.equals(inPath)) { fd = ParcelFileDescriptor.dup(getInFileDescriptor()); @@ -2953,8 +3004,6 @@ class PackageManagerShellCommand extends ShellCommand { return 1; } - session = new PackageInstaller.Session( - mInterface.getPackageInstaller().openSession(sessionId)); session.write(splitName, 0, sizeBytes, fd); if (logSuccess) { @@ -3000,7 +3049,6 @@ class PackageManagerShellCommand extends ShellCommand { try { session = new PackageInstaller.Session( mInterface.getPackageInstaller().openSession(sessionId)); - for (String splitName : splitNames) { session.removeSplit(splitName); } diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java index b751308a4bb4..c478ec472e61 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java @@ -173,6 +173,7 @@ public class PackageInstallerSessionTest { /* createdMillis */ 0L, /* stageDir */ mTmpDir, /* stageCid */ null, + /* files */ null, /* prepared */ true, /* committed */ true, /* sealed */ false, // Setting to true would trigger some PM logic. |