diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index aee3ba77f1f08..2a3fac341e24a 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -29,6 +29,8 @@ interface IPackageInstallerSession { ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes); ParcelFileDescriptor openRead(String name); + void removeSplit(String splitName); + void close(); void commit(in IntentSender statusReceiver); void abandon(); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 700a40db1f5fc..ec536e020d042 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -794,6 +794,27 @@ public class PackageInstaller { } } + /** + * Removes a split. + *

+ * Split removals occur prior to adding new APKs. If upgrading a feature + * split, it is not expected nor desirable to remove the split prior to + * upgrading. + *

+ * When split removal is bundled with new APKs, the packageName must be + * identical. + */ + public void removeSplit(@NonNull String splitName) throws IOException { + try { + mSession.removeSplit(splitName); + } catch (RuntimeException e) { + ExceptionUtils.maybeUnwrapIOException(e); + throw e; + } 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 diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index b84ffa39a8031..ef53905d55bba 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -34,6 +34,7 @@ import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageInstallObserver2; import android.content.pm.IPackageInstallerSession; +import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; @@ -58,6 +59,7 @@ import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.system.StructStat; +import android.text.TextUtils; import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.MathUtils; @@ -77,6 +79,7 @@ import com.android.server.pm.PackageInstallerService.PackageInstallObserverAdapt import java.io.File; import java.io.FileDescriptor; +import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -86,6 +89,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String TAG = "PackageInstaller"; private static final boolean LOGD = true; + private static final String REMOVE_SPLIT_MARKER_EXTENSION = ".removed"; private static final int MSG_COMMIT = 0; @@ -171,6 +175,25 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private File mInheritedFilesBase; + private static final FileFilter sAddedFilter = new FileFilter() { + @Override + public boolean accept(File file) { + // Installers can't stage directories, so it's fine to ignore + // entries like "lost+found". + if (file.isDirectory()) return false; + if (file.getName().endsWith(REMOVE_SPLIT_MARKER_EXTENSION)) return false; + return true; + } + }; + private static final FileFilter sRemovedFilter = new FileFilter() { + @Override + public boolean accept(File file) { + if (file.isDirectory()) return false; + if (!file.getName().endsWith(REMOVE_SPLIT_MARKER_EXTENSION)) return false; + return true; + } + }; + private final Handler.Callback mHandlerCallback = new Handler.Callback() { @Override public boolean handleMessage(Message msg) { @@ -345,6 +368,32 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + @Override + public void removeSplit(String splitName) { + if (TextUtils.isEmpty(params.appPackageName)) { + throw new IllegalStateException("Must specify package name to remove a split"); + } + try { + createRemoveSplitMarker(splitName); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + + private void createRemoveSplitMarker(String splitName) throws IOException { + try { + final String markerName = splitName + REMOVE_SPLIT_MARKER_EXTENSION; + if (!FileUtils.isValidExtFilename(markerName)) { + throw new IllegalArgumentException("Invalid marker: " + markerName); + } + final File target = new File(resolveStageDir(), markerName); + target.createNewFile(); + Os.chmod(target.getAbsolutePath(), 0 /*mode*/); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + @Override public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { try { @@ -608,22 +657,28 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mResolvedStagedFiles.clear(); mResolvedInheritedFiles.clear(); - final File[] files = mResolvedStageDir.listFiles(); - if (ArrayUtils.isEmpty(files)) { - throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged"); + final File[] removedFiles = mResolvedStageDir.listFiles(sRemovedFilter); + final List removeSplitList = new ArrayList<>(); + if (!ArrayUtils.isEmpty(removedFiles)) { + for (File removedFile : removedFiles) { + final String fileName = removedFile.getName(); + final String splitName = fileName.substring( + 0, fileName.length() - REMOVE_SPLIT_MARKER_EXTENSION.length()); + removeSplitList.add(splitName); + } } + final File[] addedFiles = mResolvedStageDir.listFiles(sAddedFilter); + if (ArrayUtils.isEmpty(addedFiles) && removeSplitList.size() == 0) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged"); + } // Verify that all staged packages are internally consistent final ArraySet stagedSplits = new ArraySet<>(); - for (File file : files) { - - // Installers can't stage directories, so it's fine to ignore - // entries like "lost+found". - if (file.isDirectory()) continue; - + for (File addedFile : addedFiles) { final ApkLite apk; try { - apk = PackageParser.parseApkLite(file, PackageParser.PARSE_COLLECT_CERTIFICATES); + apk = PackageParser.parseApkLite( + addedFile, PackageParser.PARSE_COLLECT_CERTIFICATES); } catch (PackageParserException e) { throw PackageManagerException.from(e); } @@ -642,7 +697,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mSignatures = apk.signatures; } - assertApkConsistent(String.valueOf(file), apk); + assertApkConsistent(String.valueOf(addedFile), apk); // Take this opportunity to enforce uniform naming final String targetName; @@ -657,8 +712,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } final File targetFile = new File(mResolvedStageDir, targetName); - if (!file.equals(targetFile)) { - file.renameTo(targetFile); + if (!addedFile.equals(targetFile)) { + addedFile.renameTo(targetFile); } // Base is coming from session @@ -669,6 +724,27 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mResolvedStagedFiles.add(targetFile); } + if (removeSplitList.size() > 0) { + // validate split names marked for removal + final int flags = mSignatures == null ? PackageManager.GET_SIGNATURES : 0; + final PackageInfo pkg = mPm.getPackageInfo(params.appPackageName, flags, userId); + for (String splitName : removeSplitList) { + if (!ArrayUtils.contains(pkg.splitNames, splitName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Split not found: " + splitName); + } + } + + // ensure we've got appropriate package name, version code and signatures + if (mPackageName == null) { + mPackageName = pkg.packageName; + mVersionCode = pkg.versionCode; + } + if (mSignatures == null) { + mSignatures = pkg.signatures; + } + } + if (params.mode == SessionParams.MODE_FULL_INSTALL) { // Full installs must include a base package if (!stagedSplits.contains(null)) { @@ -707,8 +783,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { for (int i = 0; i < existing.splitNames.length; i++) { final String splitName = existing.splitNames[i]; final File splitFile = new File(existing.splitCodePaths[i]); - - if (!stagedSplits.contains(splitName)) { + final boolean splitRemoved = removeSplitList.contains(splitName); + if (!stagedSplits.contains(splitName) && !splitRemoved) { mResolvedInheritedFiles.add(splitFile); } } @@ -748,6 +824,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package " + apk.packageName + " inconsistent with " + mPackageName); } + if (params.appPackageName != null && !params.appPackageName.equals(apk.packageName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + + " specified package " + params.appPackageName + + " inconsistent with " + apk.packageName); + } if (mVersionCode != apk.versionCode) { throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " version code " + apk.versionCode + " inconsistent with " diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 28d34a84250f2..d77168c505f75 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -95,6 +95,8 @@ class PackageManagerShellCommand extends ShellCommand { return runInstallCommit(); case "install-create": return runInstallCreate(); + case "install-remove": + return runInstallRemove(); case "install-write": return runInstallWrite(); case "compile": @@ -136,11 +138,12 @@ class PackageManagerShellCommand extends ShellCommand { pw.println("Error: must either specify a package size or an APK file"); return 1; } - if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk", + if (doWriteSplit(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk", false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { return 1; } - if (doCommitSession(sessionId, false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { + if (doCommitSession(sessionId, false /*logSuccess*/) + != PackageInstaller.STATUS_SUCCESS) { return 1; } abandonSession = false; @@ -225,7 +228,20 @@ class PackageManagerShellCommand extends ShellCommand { final int sessionId = Integer.parseInt(getNextArg()); final String splitName = getNextArg(); final String path = getNextArg(); - return doWriteSession(sessionId, path, sizeBytes, splitName, true /*logSuccess*/); + return doWriteSplit(sessionId, path, sizeBytes, splitName, true /*logSuccess*/); + } + + private int runInstallRemove() throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + + final int sessionId = Integer.parseInt(getNextArg()); + + final String splitName = getNextArg(); + if (splitName == null) { + pw.println("Error: split name not specified"); + return 1; + } + return doRemoveSplit(sessionId, splitName, true /*logSuccess*/); } private int runCompile() throws RemoteException { @@ -666,12 +682,18 @@ class PackageManagerShellCommand extends ShellCommand { } } - String packageName = getNextArg(); + final String packageName = getNextArg(); if (packageName == null) { pw.println("Error: package name not specified"); return 1; } + // if a split is specified, just remove it and not the whole package + final String splitName = getNextArg(); + if (splitName != null) { + return runRemoveSplit(packageName, splitName); + } + userId = translateUserId(userId, "runUninstall"); if (userId == UserHandle.USER_ALL) { userId = UserHandle.USER_SYSTEM; @@ -709,6 +731,36 @@ class PackageManagerShellCommand extends ShellCommand { } } + private int runRemoveSplit(String packageName, String splitName) throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + final SessionParams sessionParams = new SessionParams(SessionParams.MODE_INHERIT_EXISTING); + sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; + sessionParams.appPackageName = packageName; + final int sessionId = + doCreateSession(sessionParams, null /*installerPackageName*/, UserHandle.USER_ALL); + boolean abandonSession = true; + try { + if (doRemoveSplit(sessionId, splitName, false /*logSuccess*/) + != PackageInstaller.STATUS_SUCCESS) { + return 1; + } + if (doCommitSession(sessionId, false /*logSuccess*/) + != PackageInstaller.STATUS_SUCCESS) { + return 1; + } + abandonSession = false; + pw.println("Success"); + return 0; + } finally { + if (abandonSession) { + try { + doAbandonSession(sessionId, false /*logSuccess*/); + } catch (Exception ignore) { + } + } + } + } + private Intent parseIntentAndUser() throws URISyntaxException { mTargetUser = UserHandle.USER_CURRENT; Intent intent = Intent.parseCommandArgs(this, new Intent.CommandOptionHandler() { @@ -948,7 +1000,7 @@ class PackageManagerShellCommand extends ShellCommand { return sessionId; } - private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName, + private int doWriteSplit(int sessionId, String inPath, long sizeBytes, String splitName, boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); if ("-".equals(inPath)) { @@ -1004,6 +1056,27 @@ class PackageManagerShellCommand extends ShellCommand { } } + private int doRemoveSplit(int sessionId, String splitName, boolean logSuccess) + throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + PackageInstaller.Session session = null; + try { + session = new PackageInstaller.Session( + mInterface.getPackageInstaller().openSession(sessionId)); + session.removeSplit(splitName); + + if (logSuccess) { + pw.println("Success"); + } + return 0; + } catch (IOException e) { + pw.println("Error: failed to remove split; " + e.getMessage()); + return 1; + } finally { + IoUtils.closeQuietly(session); + } + } + private int doCommitSession(int sessionId, boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); PackageInstaller.Session session = null;