Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eb9b0539 authored by Todd Kennedy's avatar Todd Kennedy
Browse files

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
parent 8b030cce
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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();
+21 −0
Original line number Diff line number Diff line
@@ -794,6 +794,27 @@ public class PackageInstaller {
            }
        }

        /**
         * 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
+96 −15
Original line number Diff line number Diff line
@@ -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<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 @@ 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 "
+78 −5
Original line number Diff line number Diff line
@@ -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 {
@@ -627,12 +643,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;
@@ -670,6 +692,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() {
@@ -909,7 +961,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)) {
@@ -965,6 +1017,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;