客户反馈把apk installLocation设置为preferExternal后,如果sdcard路径已满,不会自动安装到内部data分区,而是安装失败。
installLocation是apk用来指定安装位置的属性,设置为preferExternal表示程序优先安装到sdcard中,如果sdcard容量不足,则安装到data分区中。而在系统中,真正用来指定安装位置的变量是recommendedInstallLocation。recommendedInstallLocation的值就是recommendAppInstallLocation函数的返回值。
public PackageInfoLite getMinimalPackageInfo(final String packagePath, int flags,
long threshold) {
...
ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation,
packagePath, flags, threshold);
return ret;
}
在recommendAppInstallLocation函数中对data和sdcard两个安装分区的可用容量进行判断。首先是对data分区的判断
boolean fitsOnInternal = false;
if (checkBoth || prefer == PREFER_INTERNAL) {
try {
fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
} catch (IOException e) {
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
}
}
fitsOnInternal就是data分区是否可用的标志,isUnderInternalThreshold函数中进行具体的可用容量判断
private boolean isUnderInternalThreshold(File apkFile, boolean isForwardLocked, long threshold)
throws IOException {
long size = apkFile.length();
if (size == 0 && !apkFile.exists()) {
throw new FileNotFoundException();
}
if (isForwardLocked) {
size += PackageHelper.extractPublicFiles(apkFile.getAbsolutePath(), null);
}
final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
final long availInternalSize = (long) internalStats.getAvailableBlocks()
* (long) internalStats.getBlockSize();
return (availInternalSize - size) > threshold;
}
然后对sdcard的可用容量进行判断
boolean fitsOnSd = false;
if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
try {
fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked);
} catch (IOException e) {
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
}
}
跟data分区的步骤大同小异,fitsOnSd为sdcard分区是否可用的标志。isUnderExternalThreshold函数进行具体的判断
private boolean isUnderExternalThreshold(File apkFile, boolean isForwardLocked)
throws IOException {
if (Environment.isExternalStorageEmulated()) {
return false;
}
final int sizeMb = calculateContainerSize(apkFile, isForwardLocked);
final int availSdMb;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
availSdMb = sdStats.getAvailableBlocks() * blocksToMb;
} else {
availSdMb = -1;
}
return availSdMb > sizeMb;
}
问题就是出在上面的isUnderExternalThreshold函数中,这个函数的步骤为如下
1、sizeMb为要安装的apk文件的本身大小和依赖的lib库大小之和,其值最小为2
private int calculateContainerSize(File apkFile, boolean forwardLocked) throws IOException {
// Calculate size of container needed to hold base APK.
long sizeBytes = apkFile.length();
if (sizeBytes == 0 && !apkFile.exists()) {
throw new FileNotFoundException();
}
// Check all the native files that need to be copied and add that to the
// container size.
sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile);
if (forwardLocked) {
sizeBytes += PackageHelper.extractPublicFiles(apkFile.getPath(), null);
}
int sizeMb = (int) (sizeBytes >> 20);
if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) {
sizeMb++;
}
/*
* Add buffer size because we don't have a good way to determine the
* real FAT size. Your FAT size varies with how many directory entries
* you need, how big the whole filesystem is, and other such headaches.
*/
sizeMb++;
return sizeMb;
}
2、availSdMb为sdcard分区所剩余的可用空间大小,这里的计算有点问题
final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
availSdMb = sdStats.getAvailableBlocks() * blocksToMb;
blocksToMb表示1mb有多少块,sdStats.getAvailableBlocks()返回sdcard的可用块的数目,所以正确的availSdMb计算公式应为sdStats.getAvailableBlocks()/blocksToMb
所以修改下这里的代码为
final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
availSdMb = sdStats.getAvailableBlocks() / blocksToMb;
就ok了。