Space analysis required for APK installation of Android PM mechanism series

Posted by JeanieTallis on Fri, 28 Jan 2022 11:55:47 +0100

preface

In the first three parts of the PM mechanism series, we focused on the whole process of installation, without many details.
In this article, we will be specific to many details. This article focuses on one problem:
How much space is needed to install APK? The error install will not be reported_ FAILED_ INSUFFICIENT_ STORAGE? It can improve our installation success rate.

1. Sequence diagram of analysis results

DCS:  DefaultContainerService
SMS:  StorageManagerService
SM: StorageManager
The call relation diagram is as follows, which will be analyzed later.

2. Start installation

After the installation procedures in the first three chapters, you can clearly know that the core code is installed in the handleStartCopy method at the beginning. The code is as follows:
/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

public void handleStartCopy() throws RemoteException {
       ...
        // If we're already staged, we've firmly committed to an install location
        if (origin.staged) {//During installation, the origin of the parameter is passed in Staged is true, file= null
            if (origin.file != null) {
                installFlags |= PackageManager.INSTALL_INTERNAL; //So I'll go here and assign the value to installFlags,
                installFlags &= ~PackageManager.INSTALL_EXTERNAL;//Internal installation logic
            } else {
                throw new IllegalStateException("Invalid stage location");
            }
        }
       /*Determine the installation position of APK. onSd: install to SD card, onInt: internal storage, i.e. Data partition, ephemeral: install to temporary storage (Instant Apps installation)
       According to the assignment of installFlags above, onSd=false;onInt=true;ephemeral=false;
       */      
       final boolean onSd = (installFlags & PackageManager.INSTALL_EXTERNAL) != 0;
       final boolean onInt = (installFlags & PackageManager.INSTALL_INTERNAL) != 0;
       final boolean ephemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
       PackageInfoLite pkgLite = null;
       if (onInt && onSd) {
         // APK cannot be installed on SD card and Data partition at the same time
           Slog.w(TAG, "Conflicting flags specified for installing on both internal and external");
           ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
         //Installation flags conflict. Instant Apps cannot be installed into SD card
       } else if (onSd && ephemeral) {
           Slog.w(TAG,  "Conflicting flags specified for installing ephemeral on external");
           ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
       } else { //Will eventually come here
            //Get a small amount of information about APK
           pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,
                   packageAbiOverride);//1
           if (DEBUG_EPHEMERAL && ephemeral) {
               Slog.v(TAG, "pkgLite for install: " + pkgLite);
           }
       ...
       if (ret == PackageManager.INSTALL_SUCCEEDED) {
            //Determine the installation position
           int loc = pkgLite.recommendedInstallLocation;
           if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION) {
               ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
           } else if (loc == PackageHelper.RECOMMEND_FAILED_ALREADY_EXISTS) {
               ret = PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
           }  else if (loc == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {
             ret = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; //2
           } 
           ...
           }else{
             loc = installLocationPolicy(pkgLite);
             ...
           }
       }
       //Create InstallArgs object from InstallParams
       final InstallArgs args = createInstallArgs(this);
       mArgs = args;
       if (ret == PackageManager.INSTALL_SUCCEEDED) {
              ...
           if (!origin.existing && requiredUid != -1
                   && isVerificationEnabled(
                         verifierUser.getIdentifier(), installFlags, installerUid)) {
                 ...
           } else{
               ret = args.copyApk(mContainerService, true);
           }
       }
       mRet = ret;
   }

There is a lot of code for the handleStartCopy method, and the key part is intercepted here.

  1. In note 1, the getMinimalPackageInfo method of DefaultContainerService is called across processes through IMediaContainerService. This method lightly parses APK and obtains a small amount of information of APK. The reason for lightweight parsing is that there is no need to obtain all information of APK here, and a small amount of information of APK will be encapsulated in packageinplite. In the previous article, we mentioned that there is a lot of information here, including the judgment of installation space involved in this article.
  2. Note 2, if loc = = packagehelper RECOMMEND_ FAILED_ INSUFFICIENT_ Storage, it will return to INSTALL_FAILED_INSUFFICIENT_STORAGE. Therefore, it is necessary to analyze from the return value of loc. Let's take a look at the getMinimalPackageInfo method.

/frameworks/base/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java

/**
 * Parse given package and return minimal details.
 *
 * @param packagePath absolute path to the package to be copied. Can be
 *            a single monolithic APK file or a cluster directory
 *            containing one or more APKs.
 */
@Override
public PackageInfoLite getMinimalPackageInfo(String packagePath, int flags,
        String abiOverride) {
    final Context context = DefaultContainerService.this;

    PackageInfoLite ret = new PackageInfoLite();
    if (packagePath == null) {
        Slog.i(TAG, "Invalid package file " + packagePath);
        ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
        return ret;
    }

    final File packageFile = new File(packagePath);
    final PackageParser.PackageLite pkg;
    final long sizeBytes;
    try {
        pkg = PackageParser.parsePackageLite(packageFile, 0);
        sizeBytes = PackageHelper.calculateInstalledSize(pkg, abiOverride); //1
    } catch (PackageParserException | IOException e) {
        Slog.w(TAG, "Failed to parse package at " + packagePath + ": " + e);

        if (!packageFile.exists()) {
            ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
        } else {
            ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
        }

        return ret;
    }

    final int recommendedInstallLocation;
    final long token = Binder.clearCallingIdentity();
    try {
        recommendedInstallLocation = PackageHelper.resolveInstallLocation(context,
                pkg.packageName, pkg.installLocation, sizeBytes, flags);//2
    } finally {
        Binder.restoreCallingIdentity(token);
    }

    ret.packageName = pkg.packageName;
    ret.splitNames = pkg.splitNames;
    ret.versionCode = pkg.versionCode;
    ret.versionCodeMajor = pkg.versionCodeMajor;
    ret.baseRevisionCode = pkg.baseRevisionCode;
    ret.splitRevisionCodes = pkg.splitRevisionCodes;
    ret.installLocation = pkg.installLocation;
    ret.verifiers = pkg.verifiers;
    ret.recommendedInstallLocation = recommendedInstallLocation;
    ret.multiArch = pkg.multiArch;

    return ret;
}
  1. Note 1: calculate the space required to install apk.
  2. Note 2 is the core code. The recommended storage location is calculated according to the space required by apk. The parameter flags is the internal storage we assigned before.

Let's take a look at the resolveInstallLocation method
/frameworks/base/core/java/com/android/internal/content/PackageHelper.java

@Deprecated
public static int resolveInstallLocation(Context context, String packageName,
        int installLocation, long sizeBytes, int installFlags) {
    final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);
    params.appPackageName = packageName;
    params.installLocation = installLocation;
    params.sizeBytes = sizeBytes;
    params.installFlags = installFlags;
    try {
        return resolveInstallLocation(context, params); //1
    } catch (IOException e) {
        throw new IllegalStateException(e);
    }
}

/**
 * Given a requested {@link PackageInfo#installLocation} and calculated
 * install size, pick the actual location to install the app.
 */
public static int resolveInstallLocation(Context context, SessionParams params)
        throws IOException {
    ApplicationInfo existingInfo = null;
    try {
        existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName,
                PackageManager.MATCH_ANY_USER);
    } catch (NameNotFoundException ignored) {
    }

    final int prefer;
    final boolean checkBoth;
    boolean ephemeral = false;
    if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        ephemeral = true;
        checkBoth = false;
    } else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { //2
        prefer = RECOMMEND_INSTALL_INTERNAL;
        checkBoth = false;
    } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {
        prefer = RECOMMEND_INSTALL_EXTERNAL;
        checkBoth = false;
    } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        checkBoth = false;
    } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
        prefer = RECOMMEND_INSTALL_EXTERNAL;
        checkBoth = true;
    } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
        // When app is already installed, prefer same medium
        if (existingInfo != null) {
            // TODO: distinguish if this is external ASEC
            if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
                prefer = RECOMMEND_INSTALL_EXTERNAL;
            } else {
                prefer = RECOMMEND_INSTALL_INTERNAL;
            }
        } else {
            prefer = RECOMMEND_INSTALL_INTERNAL;
        }
        checkBoth = true;
    } else {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        checkBoth = false;
    }

    boolean fitsOnInternal = false;
    if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {
        fitsOnInternal = fitsOnInternal(context, params); //3
    }

    boolean fitsOnExternal = false;
    if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {
        fitsOnExternal = fitsOnExternal(context, params);
    }

    if (prefer == RECOMMEND_INSTALL_INTERNAL) {  //4
        // The ephemeral case will either fit and return EPHEMERAL, or will not fit
        // and will fall through to return INSUFFICIENT_STORAGE
        if (fitsOnInternal) {
            return (ephemeral)
                    ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL
                    : PackageHelper.RECOMMEND_INSTALL_INTERNAL; //5
        }
    } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {
        if (fitsOnExternal) {
            return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
        }
    }

    if (checkBoth) {
        if (fitsOnInternal) {
            return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
        } else if (fitsOnExternal) {
            return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
        }
    }

    return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;  //6
}
  1. Note 1: put all the parameters we passed in into SessionParams and execute resolveInstallLocation
  2. According to the flags value passed in before, the code will operate to note 2. Assignment here:
    prefer = RECOMMEND_INSTALL_INTERNAL;
    checkBoth = false;
  3. According to the previous assignment, you will go to note 3 and call fitsOnInternal() method.
    If fitsOnInternal is true, it will go to comment 5 and return RECOMMEND_INSTALL_INTERNAL
    If fitsOnInternal is false, it will go to comment 6 and return RECOMMEND_FAILED_INSUFFICIENT_STORAGE.

Let's take a look at the fitsOnInternal() method
/frameworks/base/core/java/com/android/internal/content/PackageHelper.java

public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException {
    final StorageManager storage = context.getSystemService(StorageManager.class);
    final UUID target = storage.getUuidForPath(Environment.getDataDirectory());
    return (params.sizeBytes <= storage.getAllocatableBytes(target,
            translateAllocateFlags(params.installFlags)));  //1
}
  1. It is to judge whether the space required for installation is less than the storage size that the system can allocate.
  2. The translateAllocateFlags method is as follows, because installFlags does not set INSTALL_ALLOCATE_AGGRESSIVE identifier, so 0 is returned.
    public static int translateAllocateFlags(int installFlags) {
       if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) {
           return StorageManager.FLAG_ALLOCATE_AGGRESSIVE;
       } else {
           return 0;
       }
    }
    

The getallocatable bytes method of StorageManager will be called next
/frameworks/base/core/java/android/os/storage/StorageManager.java

public long getAllocatableBytes(@NonNull UUID storageUuid,
     @RequiresPermission @AllocateFlags int flags) throws IOException {
    try {
        return mStorageManager.getAllocatableBytes(convert(storageUuid), flags,
                mContext.getOpPackageName()); //1
    } catch (ParcelableException e) {
        e.maybeRethrow(IOException.class);
        throw new RuntimeException(e);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}
  1. Note 1 continue to call the getallocatable bytes method of StorageManagerService
    /frameworks/base/services/core/java/com/android/server/StorageManagerService.java
 @Override
public long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) {
    flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage);

    final StorageManager storage = mContext.getSystemService(StorageManager.class);
    final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);
    final long token = Binder.clearCallingIdentity();
    try {
        // In general, apps can allocate as much space as they want, except
        // we never let them eat into either the minimum cache space or into
        // the low disk warning space. To avoid user confusion, this logic
        // should be kept in sync with getFreeBytes().
        final File path = storage.findPathForUuid(volumeUuid);

        final long usable = path.getUsableSpace();//1
        final long lowReserved = storage.getStorageLowBytes(path);//2
        final long fullReserved = storage.getStorageFullBytes(path);

        if (stats.isQuotaSupported(volumeUuid)) {
            final long cacheTotal = stats.getCacheBytes(volumeUuid);
            final long cacheReserved = storage.getStorageCacheBytes(path, flags);
            final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);

            if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
                return Math.max(0, (usable + cacheClearable) - fullReserved);
            } else {
                return Math.max(0, (usable + cacheClearable) - lowReserved);
            }
        } else {
            // When we don't have fast quota information, we ignore cached
            // data and only consider unused bytes.
            if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
                return Math.max(0, usable - fullReserved);
            } else {
                return Math.max(0, usable - lowReserved);//3
            }
        }
    } catch (IOException e) {
        throw new ParcelableException(e);
    } finally {
        Binder.restoreCallingIdentity(token);
    }
}
  1. In note 1, the available space of the zone will be calculated first
  2. Note 2 will find out the minimum storage required for system operation
  3. Note 3 will find the size of the space that can be allocated

Let's take a look at the getStorageLowBytes method
/frameworks/base/core/java/android/os/storage/StorageManager.java

private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);

public long getStorageLowBytes(File path) {
    final long lowPercent = Settings.Global.getInt(mResolver,
            Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
    final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;
    final long maxLowBytes = Settings.Global.getLong(mResolver,
            Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
    return Math.min(lowBytes, maxLowBytes);
}

We found out. The default threshold set by the system is 5%, and the final return value is the minimum value between 5% of getTotalSpace and 500M.

summary

So far, it's all over. We found that as long as the system space is less than math Min (5% of gettotalspace, 500m) + packagehelper Calculateinstalledsize (PKG, abioverride), the system will report insufficient space.

Topics: Android