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;