remove splits
Individual splits can now be removed for an application. The
application will be terminated if it's running when a split is
removed.
To remove a split, either use either "uninstall":
$ adb shell cmd package uninstall <PACKAGE> <SPLIT>
or "install-remove":
$ adb shell cmd package install-create -r -p <PACKAGE>
$ adb shell cmd package install-remove <SESSION> <SPLIT>
$ adb shell cmd package install-commit <SESSION>
For "install-remove" you must use '-r' and '-p' when creating
the session.
Bug: 27547051
Change-Id: I4d71a19ad45e39f6622d9ab6791ea8c4230a79e0
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl
index aee3ba7..2a3fac3 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -29,6 +29,8 @@
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 700a40d..ec536e0 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -795,6 +795,27 @@
}
/**
+ * Removes a split.
+ * <p>
+ * 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.
+ * <p>
+ * 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
* result of the commit will be reported through the given callback.
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index b84ffa3..ef53905 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.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.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 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 @@
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 @@
@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) {
@@ -346,6 +369,32 @@
}
@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 {
return openWriteInternal(name, offsetBytes, lengthBytes);
@@ -608,22 +657,28 @@
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<String> 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<String> 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 @@
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 @@
}
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 @@
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 @@
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 @@
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 abee007..d3bc02b 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 @@
return runInstallCommit();
case "install-create":
return runInstallCreate();
+ case "install-remove":
+ return runInstallRemove();
case "install-write":
return runInstallWrite();
case "compile":
@@ -136,11 +138,12 @@
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 @@
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 {
@@ -627,12 +643,18 @@
}
}
- 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;
@@ -670,6 +692,36 @@
}
}
+ 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() {
@@ -909,7 +961,7 @@
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)) {
@@ -965,6 +1017,27 @@
}
}
+ 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;