blob: 20a621bd808417b77e34bc0f5114be6a50aee64f [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.defcontainer;
import android.app.IntentService;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageCleanItem;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInfoLite;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.PackageLite;
import android.content.pm.PackageParser.PackageParserException;
import android.content.res.ObbInfo;
import android.content.res.ObbScanner;
import android.os.Build;
import android.os.Environment;
import android.os.Environment.UserEnvironment;
import android.os.FileUtils;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.StatFs;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStatVfs;
import android.util.Slog;
import com.android.internal.app.IMediaContainerService;
import com.android.internal.content.NativeLibraryHelper;
import com.android.internal.content.PackageHelper;
import com.android.internal.os.IParcelFileDescriptorFactory;
import com.android.internal.util.ArrayUtils;
import dalvik.system.VMRuntime;
import libcore.io.IoUtils;
import libcore.io.Streams;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Service that offers to inspect and copy files that may reside on removable
* storage. This is designed to prevent the system process from holding onto
* open files that cause the kernel to kill it when the underlying device is
* removed.
*/
public class DefaultContainerService extends IntentService {
private static final String TAG = "DefContainer";
private static final boolean localLOGV = false;
private static final String LIB_DIR_NAME = "lib";
// TODO: migrate native code unpacking to always be a derivative work
private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
/**
* Creates a new container and copies package there.
*
* @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.
* @param containerId the id of the secure container that should be used
* for creating a secure container into which the resource
* will be copied.
* @param key Refers to key used for encrypting the secure container
* @return Returns the new cache path where the resource has been copied
* into
*/
@Override
public String copyPackageToContainer(String packagePath, String containerId, String key,
boolean isExternal, boolean isForwardLocked, String abiOverride) {
if (packagePath == null || containerId == null) {
return null;
}
if (isExternal) {
// Make sure the sdcard is mounted.
String status = Environment.getExternalStorageState();
if (!status.equals(Environment.MEDIA_MOUNTED)) {
Slog.w(TAG, "Make sure sdcard is mounted.");
return null;
}
}
PackageLite pkg = null;
NativeLibraryHelper.Handle handle = null;
try {
final File packageFile = new File(packagePath);
pkg = PackageParser.parsePackageLite(packageFile, 0);
handle = NativeLibraryHelper.Handle.create(pkg);
return copyPackageToContainerInner(pkg, handle, containerId, key, isExternal,
isForwardLocked, abiOverride);
} catch (PackageParserException | IOException e) {
Slog.w(TAG, "Failed to parse package at " + packagePath);
return null;
} finally {
IoUtils.closeQuietly(handle);
}
}
/**
* Copy package to the target location.
*
* @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.
* @return returns status code according to those in
* {@link PackageManager}
*/
@Override
public int copyPackage(String packagePath, IParcelFileDescriptorFactory target) {
if (packagePath == null || target == null) {
return PackageManager.INSTALL_FAILED_INVALID_URI;
}
PackageLite pkg = null;
try {
final File packageFile = new File(packagePath);
pkg = PackageParser.parsePackageLite(packageFile, 0);
return copyPackageInner(pkg, target);
} catch (PackageParserException | IOException | RemoteException e) {
Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);
return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
}
}
/**
* 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(final String packagePath, int flags,
long threshold, String abiOverride) {
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;
try {
pkg = PackageParser.parsePackageLite(packageFile, 0);
} catch (PackageParserException e) {
Slog.w(TAG, "Failed to parse package at " + packagePath);
if (!packageFile.exists()) {
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
} else {
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
}
return ret;
}
ret.packageName = pkg.packageName;
ret.versionCode = pkg.versionCode;
ret.installLocation = pkg.installLocation;
ret.verifiers = pkg.verifiers;
ret.recommendedInstallLocation = recommendAppInstallLocation(pkg, flags, threshold,
abiOverride);
ret.multiArch = pkg.multiArch;
return ret;
}
/**
* Determine if package will fit on internal storage.
*
* @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 boolean checkInternalFreeStorage(String packagePath, boolean isForwardLocked,
long threshold) throws RemoteException {
final File packageFile = new File(packagePath);
final PackageParser.PackageLite pkg;
try {
pkg = PackageParser.parsePackageLite(packageFile, 0);
return isUnderInternalThreshold(pkg, isForwardLocked, threshold);
} catch (PackageParserException | IOException e) {
Slog.w(TAG, "Failed to parse package at " + packagePath);
return false;
}
}
/**
* Determine if package will fit on external storage.
*
* @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 boolean checkExternalFreeStorage(String packagePath, boolean isForwardLocked,
String abiOverride) throws RemoteException {
final File packageFile = new File(packagePath);
final PackageParser.PackageLite pkg;
try {
pkg = PackageParser.parsePackageLite(packageFile, 0);
return isUnderExternalThreshold(pkg, isForwardLocked, abiOverride);
} catch (PackageParserException | IOException e) {
Slog.w(TAG, "Failed to parse package at " + packagePath);
return false;
}
}
@Override
public ObbInfo getObbInfo(String filename) {
try {
return ObbScanner.getObbInfo(filename);
} catch (IOException e) {
Slog.d(TAG, "Couldn't get OBB info for " + filename);
return null;
}
}
@Override
public long calculateDirectorySize(String path) throws RemoteException {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
final File dir = Environment.maybeTranslateEmulatedPathToInternal(new File(path));
if (dir.exists() && dir.isDirectory()) {
final String targetPath = dir.getAbsolutePath();
return MeasurementUtils.measureDirectory(targetPath);
} else {
return 0L;
}
}
@Override
public long[] getFileSystemStats(String path) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
final StructStatVfs stat = Os.statvfs(path);
final long totalSize = stat.f_blocks * stat.f_bsize;
final long availSize = stat.f_bavail * stat.f_bsize;
return new long[] { totalSize, availSize };
} catch (ErrnoException e) {
throw new IllegalStateException(e);
}
}
@Override
public void clearDirectory(String path) throws RemoteException {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
final File directory = new File(path);
if (directory.exists() && directory.isDirectory()) {
eraseFiles(directory);
}
}
/**
* Calculate estimated footprint of given package post-installation.
*
* @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 long calculateInstalledSize(String packagePath, boolean isForwardLocked,
String abiOverride) throws RemoteException {
final File packageFile = new File(packagePath);
final PackageParser.PackageLite pkg;
try {
pkg = PackageParser.parsePackageLite(packageFile, 0);
return calculateContainerSize(pkg, isForwardLocked, abiOverride) * 1024 * 1024;
} catch (PackageParserException | IOException e) {
/*
* Okay, something failed, so let's just estimate it to be 2x
* the file size. Note this will be 0 if the file doesn't exist.
*/
return packageFile.length() * 2;
}
}
};
public DefaultContainerService() {
super("DefaultContainerService");
setIntentRedelivery(true);
}
@Override
protected void onHandleIntent(Intent intent) {
if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
final IPackageManager pm = IPackageManager.Stub.asInterface(
ServiceManager.getService("package"));
PackageCleanItem item = null;
try {
while ((item = pm.nextPackageToClean(item)) != null) {
final UserEnvironment userEnv = new UserEnvironment(item.userId);
eraseFiles(userEnv.buildExternalStorageAppDataDirs(item.packageName));
eraseFiles(userEnv.buildExternalStorageAppMediaDirs(item.packageName));
if (item.andCode) {
eraseFiles(userEnv.buildExternalStorageAppObbDirs(item.packageName));
}
}
} catch (RemoteException e) {
}
}
}
void eraseFiles(File[] paths) {
for (File path : paths) {
eraseFiles(path);
}
}
void eraseFiles(File path) {
if (path.isDirectory()) {
String[] files = path.list();
if (files != null) {
for (String file : files) {
eraseFiles(new File(path, file));
}
}
}
path.delete();
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private String copyPackageToContainerInner(PackageLite pkg, NativeLibraryHelper.Handle handle,
String newCid, String key, boolean isExternal, boolean isForwardLocked,
String abiOverride) {
// TODO: extend to support copying all split APKs
if (!ArrayUtils.isEmpty(pkg.splitNames)) {
throw new UnsupportedOperationException("Copying split APKs not yet supported");
}
final String resFileName = "pkg.apk";
final String publicResFileName = "res.zip";
if (pkg.multiArch) {
// TODO: Support multiArch installs on ASEC.
throw new IllegalArgumentException("multiArch not supported on ASEC installs.");
}
// The .apk file
final String codePath = pkg.baseCodePath;
final File codeFile = new File(codePath);
final String[] abis;
try {
abis = calculateAbiList(handle, abiOverride, pkg.multiArch);
} catch (IOException ioe) {
Slog.w(TAG, "Problem determining app ABIS: " + ioe);
return null;
}
// Calculate size of container needed to hold base APK.
final int sizeMb;
try {
sizeMb = calculateContainerSize(pkg, handle, isForwardLocked, abis);
} catch (IOException e) {
Slog.w(TAG, "Problem when trying to copy " + codeFile.getPath());
return null;
}
// Create new container
final String newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid(),
isExternal);
if (newCachePath == null) {
Slog.e(TAG, "Failed to create container " + newCid);
return null;
}
if (localLOGV) {
Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
}
final File resFile = new File(newCachePath, resFileName);
if (FileUtils.copyFile(new File(codePath), resFile)) {
if (localLOGV) {
Slog.i(TAG, "Copied " + codePath + " to " + resFile);
}
} else {
Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile);
// Clean up container
PackageHelper.destroySdDir(newCid);
return null;
}
try {
Os.chmod(resFile.getAbsolutePath(), 0640);
} catch (ErrnoException e) {
Slog.e(TAG, "Could not chown APK: " + e.getMessage());
PackageHelper.destroySdDir(newCid);
return null;
}
if (isForwardLocked) {
File publicZipFile = new File(newCachePath, publicResFileName);
try {
PackageHelper.extractPublicFiles(resFile.getAbsolutePath(), publicZipFile);
if (localLOGV) {
Slog.i(TAG, "Copied resources to " + publicZipFile);
}
} catch (IOException e) {
Slog.e(TAG, "Could not chown public APK " + publicZipFile.getAbsolutePath() + ": "
+ e.getMessage());
PackageHelper.destroySdDir(newCid);
return null;
}
try {
Os.chmod(publicZipFile.getAbsolutePath(), 0644);
} catch (ErrnoException e) {
Slog.e(TAG, "Could not chown public resource file: " + e.getMessage());
PackageHelper.destroySdDir(newCid);
return null;
}
}
final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
if (sharedLibraryDir.mkdir()) {
int ret = PackageManager.INSTALL_SUCCEEDED;
if (abis != null) {
// TODO(multiArch): Support multi-arch installs on asecs. Note that we are NOT
// using an ISA specific subdir here for now.
final String abi = abis[0];
ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(handle,
sharedLibraryDir, abi);
if (ret != PackageManager.INSTALL_SUCCEEDED) {
Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath());
PackageHelper.destroySdDir(newCid);
return null;
}
}
} else {
Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath());
PackageHelper.destroySdDir(newCid);
return null;
}
if (!PackageHelper.finalizeSdDir(newCid)) {
Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
// Clean up container
PackageHelper.destroySdDir(newCid);
return null;
}
if (localLOGV) {
Slog.i(TAG, "Finalized container " + newCid);
}
if (PackageHelper.isContainerMounted(newCid)) {
if (localLOGV) {
Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath);
}
// Force a gc to avoid being killed.
Runtime.getRuntime().gc();
PackageHelper.unMountSdDir(newCid);
} else {
if (localLOGV) {
Slog.i(TAG, "Container " + newCid + " not mounted");
}
}
return newCachePath;
}
private int copyPackageInner(PackageLite pkg, IParcelFileDescriptorFactory target)
throws IOException, RemoteException {
copyFile(pkg.baseCodePath, "base.apk", target);
if (!ArrayUtils.isEmpty(pkg.splitNames)) {
for (int i = 0; i < pkg.splitNames.length; i++) {
copyFile(pkg.splitCodePaths[i], "split_" + pkg.splitNames[i] + ".apk", target);
}
}
return PackageManager.INSTALL_SUCCEEDED;
}
private void copyFile(String sourcePath, String targetName,
IParcelFileDescriptorFactory target) throws IOException, RemoteException {
Slog.d(TAG, "Copying " + sourcePath + " to " + targetName);
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(sourcePath);
out = new ParcelFileDescriptor.AutoCloseOutputStream(
target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE));
Streams.copy(in, out);
} finally {
IoUtils.closeQuietly(out);
IoUtils.closeQuietly(in);
}
}
private static final int PREFER_INTERNAL = 1;
private static final int PREFER_EXTERNAL = 2;
private int recommendAppInstallLocation(PackageLite pkg, int flags, long threshold,
String abiOverride) {
int prefer;
boolean checkBoth = false;
final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
check_inner : {
/*
* Explicit install flags should override the manifest settings.
*/
if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
prefer = PREFER_INTERNAL;
break check_inner;
} else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
prefer = PREFER_EXTERNAL;
break check_inner;
}
/* No install flags. Check for manifest option. */
if (pkg.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
prefer = PREFER_INTERNAL;
break check_inner;
} else if (pkg.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
prefer = PREFER_EXTERNAL;
checkBoth = true;
break check_inner;
} else if (pkg.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
// We default to preferring internal storage.
prefer = PREFER_INTERNAL;
checkBoth = true;
break check_inner;
}
// Pick user preference
int installPreference = Settings.Global.getInt(getApplicationContext()
.getContentResolver(),
Settings.Global.DEFAULT_INSTALL_LOCATION,
PackageHelper.APP_INSTALL_AUTO);
if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
prefer = PREFER_INTERNAL;
break check_inner;
} else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
prefer = PREFER_EXTERNAL;
break check_inner;
}
/*
* Fall back to default policy of internal-only if nothing else is
* specified.
*/
prefer = PREFER_INTERNAL;
}
final boolean emulated = Environment.isExternalStorageEmulated();
boolean fitsOnInternal = false;
if (checkBoth || prefer == PREFER_INTERNAL) {
try {
fitsOnInternal = isUnderInternalThreshold(pkg, isForwardLocked, threshold);
} catch (IOException e) {
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
}
}
boolean fitsOnSd = false;
if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
try {
fitsOnSd = isUnderExternalThreshold(pkg, isForwardLocked, abiOverride);
} catch (IOException e) {
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
}
}
if (prefer == PREFER_INTERNAL) {
if (fitsOnInternal) {
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
}
} else if (!emulated && prefer == PREFER_EXTERNAL) {
if (fitsOnSd) {
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
}
}
if (checkBoth) {
if (fitsOnInternal) {
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
} else if (!emulated && fitsOnSd) {
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
}
}
/*
* If they requested to be on the external media by default, return that
* the media was unavailable. Otherwise, indicate there was insufficient
* storage space available.
*/
if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
&& !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
} else {
return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
}
}
/**
* Measure a file to see if it fits within the free space threshold.
*
* @param threshold byte threshold to compare against
* @return true if file fits under threshold
* @throws FileNotFoundException when APK does not exist
*/
private boolean isUnderInternalThreshold(PackageLite pkg, boolean isForwardLocked,
long threshold) throws IOException {
long sizeBytes = 0;
for (String codePath : pkg.getAllCodePaths()) {
sizeBytes += new File(codePath).length();
if (isForwardLocked) {
sizeBytes += PackageHelper.extractPublicFiles(codePath, null);
}
}
final StatFs stat = new StatFs(Environment.getDataDirectory().getPath());
final long availBytes = stat.getAvailableBytes();
return (availBytes - sizeBytes) > threshold;
}
/**
* Measure a file to see if it fits in the external free space.
*
* @return true if file fits
* @throws IOException when file does not exist
*/
private boolean isUnderExternalThreshold(PackageLite pkg, boolean isForwardLocked,
String abiOverride) throws IOException {
if (Environment.isExternalStorageEmulated()) {
return false;
}
final int sizeMb = calculateContainerSize(pkg, isForwardLocked, abiOverride);
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;
}
private int calculateContainerSize(PackageLite pkg, boolean isForwardLocked, String abiOverride)
throws IOException {
NativeLibraryHelper.Handle handle = null;
try {
handle = NativeLibraryHelper.Handle.create(pkg);
return calculateContainerSize(pkg, handle, isForwardLocked,
calculateAbiList(handle, abiOverride, pkg.multiArch));
} finally {
IoUtils.closeQuietly(handle);
}
}
private String[] calculateAbiList(NativeLibraryHelper.Handle handle, String abiOverride,
boolean isMultiArch) throws IOException {
if (isMultiArch) {
final int abi32 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_32_BIT_ABIS);
final int abi64 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_64_BIT_ABIS);
if (abi32 >= 0 && abi64 >= 0) {
return new String[] { Build.SUPPORTED_64_BIT_ABIS[abi64], Build.SUPPORTED_32_BIT_ABIS[abi32] };
} else if (abi64 >= 0) {
return new String[] { Build.SUPPORTED_64_BIT_ABIS[abi64] };
} else if (abi32 >= 0) {
return new String[] { Build.SUPPORTED_32_BIT_ABIS[abi32] };
}
if (abi64 != PackageManager.NO_NATIVE_LIBRARIES || abi32 != PackageManager.NO_NATIVE_LIBRARIES) {
throw new IOException("Error determining ABI list: errorCode=[" + abi32 + "," + abi64 + "]");
}
} else {
String[] abiList = Build.SUPPORTED_ABIS;
if (abiOverride != null) {
abiList = new String[] { abiOverride };
} else if (Build.SUPPORTED_64_BIT_ABIS.length > 0 &&
NativeLibraryHelper.hasRenderscriptBitcode(handle)) {
abiList = Build.SUPPORTED_32_BIT_ABIS;
}
final int abi = NativeLibraryHelper.findSupportedAbi(handle,abiList);
if (abi >= 0) {
return new String[]{Build.SUPPORTED_ABIS[abi]};
}
if (abi != PackageManager.NO_NATIVE_LIBRARIES) {
throw new IOException("Error determining ABI list: errorCode=" + abi);
}
}
return null;
}
/**
* Calculate the container size for a package.
*
* @return size in megabytes (2^20 bytes)
* @throws IOException when there is a problem reading the file
*/
private int calculateContainerSize(PackageLite pkg, NativeLibraryHelper.Handle handle,
boolean isForwardLocked, String[] abis) throws IOException {
// Calculate size of container needed to hold APKs.
long sizeBytes = 0;
for (String codePath : pkg.getAllCodePaths()) {
sizeBytes += new File(codePath).length();
if (isForwardLocked) {
sizeBytes += PackageHelper.extractPublicFiles(codePath, null);
}
}
// Check all the native files that need to be copied and add that to the
// container size.
if (abis != null) {
sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(handle, abis);
}
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;
}
}