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

Commit e42bc52d authored by Noah Zimmt's avatar Noah Zimmt
Browse files

Reduce calls to installd when resolving volume

Optimizes PackageHelper#resolveInstallVolume to aggressively avoid
querying volume storage statistics unless absolutely neeeded.

The previous implementation queries storage data for each volume on the
device before evaluating which volume to select. The new implementation
defers querying storage stats for a given volume until it is absolutely
neccessary.

For example, if the application is already installed to a particular
volume, only that volume needs to be queried for available space.

In addition, querying for available space on a volume is done in two
steps. The first step does not take into account freeable cache; only
if this pessimistic assessment of available space fails do we call
installd to obtain cache information.

Bug: 138450974
Test: atest frameworks/base/core/tests/coretests/src/android/content/pm/PackageHelperTests.java
&& atest cts/tests/tests/os/src/android/os/storage/cts/StorageManagerTest.java
Change-Id: I403318de364d41019cb3d43e93b91cdd06fe1b9c
parent b2558071
Loading
Loading
Loading
Loading
+60 −27
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
@@ -166,6 +166,23 @@ public class PackageHelper {
                params.sizeBytes, testableInterface);
    }

    private static boolean checkFitOnVolume(StorageManager storageManager, String volumePath,
            SessionParams params) throws IOException {
        if (volumePath == null) {
            return false;
        }
        final int installFlags = translateAllocateFlags(params.installFlags);
        final UUID target = storageManager.getUuidForPath(new File(volumePath));
        final long availBytes = storageManager.getAllocatableBytes(target,
                installFlags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY);
        if (params.sizeBytes <= availBytes) {
            return true;
        }
        final long cacheClearable = storageManager.getAllocatableBytes(target,
                installFlags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY);
        return params.sizeBytes <= availBytes + cacheClearable;
    }

    @VisibleForTesting
    public static String resolveInstallVolume(Context context, SessionParams params,
            TestableInterface testInterface) throws IOException {
@@ -178,35 +195,23 @@ public class PackageHelper {
        ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context,
                params.appPackageName);

        // Figure out best candidate volume, and also if we fit on internal
        final ArraySet<String> allCandidates = new ArraySet<>();
        boolean fitsOnInternal = false;
        VolumeInfo bestCandidate = null;
        long bestCandidateAvailBytes = Long.MIN_VALUE;
        final ArrayMap<String, String> volumePaths = new ArrayMap<>();
        String internalVolumePath = null;
        for (VolumeInfo vol : storageManager.getVolumes()) {
            if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
                final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id);
                final UUID target = storageManager.getUuidForPath(new File(vol.path));
                final long availBytes = storageManager.getAllocatableBytes(target,
                        translateAllocateFlags(params.installFlags));
                if (isInternalStorage) {
                    fitsOnInternal = (params.sizeBytes <= availBytes);
                    internalVolumePath = vol.path;
                }
                if (!isInternalStorage || allow3rdPartyOnInternal) {
                    if (availBytes >= params.sizeBytes) {
                        allCandidates.add(vol.fsUuid);
                    }
                    if (availBytes >= bestCandidateAvailBytes) {
                        bestCandidate = vol;
                        bestCandidateAvailBytes = availBytes;
                    }
                    volumePaths.put(vol.fsUuid, vol.path);
                }
            }
        }

        // System apps always forced to internal storage
        if (existingInfo != null && existingInfo.isSystemApp()) {
            if (fitsOnInternal) {
            if (checkFitOnVolume(storageManager, internalVolumePath, params)) {
                return StorageManager.UUID_PRIVATE_INTERNAL;
            } else {
                throw new IOException("Not enough space on existing volume "
@@ -228,7 +233,7 @@ public class PackageHelper {
                throw new IOException("Not allowed to install non-system apps on internal storage");
            }

            if (fitsOnInternal) {
            if (checkFitOnVolume(storageManager, internalVolumePath, params)) {
                return StorageManager.UUID_PRIVATE_INTERNAL;
            } else {
                throw new IOException("Requested internal only, but not enough space");
@@ -237,10 +242,14 @@ public class PackageHelper {

        // If app already exists somewhere, we must stay on that volume
        if (existingInfo != null) {
            if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)
                    && fitsOnInternal) {
                return StorageManager.UUID_PRIVATE_INTERNAL;
            } else if (allCandidates.contains(existingInfo.volumeUuid)) {
            String existingVolumePath = null;
            if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)) {
                existingVolumePath = internalVolumePath;
            } else if (volumePaths.containsKey(existingInfo.volumeUuid)) {
                existingVolumePath = volumePaths.get(existingInfo.volumeUuid);
            }

            if (checkFitOnVolume(storageManager, existingVolumePath, params)) {
                return existingInfo.volumeUuid;
            } else {
                throw new IOException("Not enough space on existing volume "
@@ -250,13 +259,37 @@ public class PackageHelper {

        // We're left with new installations with either preferring external or auto, so just pick
        // volume with most space
        if (bestCandidate != null) {
            return bestCandidate.fsUuid;
        if (volumePaths.size() == 1) {
            if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) {
                return volumePaths.keyAt(0);
            }
        } else {
            String bestCandidate = null;
            long bestCandidateAvailBytes = Long.MIN_VALUE;
            for (String vol : volumePaths.keySet()) {
                final String volumePath = volumePaths.get(vol);
                final UUID target = storageManager.getUuidForPath(new File(volumePath));

                // We need to take into account freeable cached space, because we're choosing the
                // best candidate amongst a list, not just checking if we fit at all.
                final long availBytes = storageManager.getAllocatableBytes(target,
                        translateAllocateFlags(params.installFlags));

                if (availBytes >= bestCandidateAvailBytes) {
                    bestCandidate = vol;
                    bestCandidateAvailBytes = availBytes;
                }
            }

            if (bestCandidateAvailBytes >= params.sizeBytes) {
                return bestCandidate;
            }

        }

        throw new IOException("No special requests, but no room on allowed volumes. "
                + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
    }
    }

    public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException {
        final StorageManager storage = context.getSystemService(StorageManager.class);