When measuring a package's usage, put the external size in the PackageStats as well. This will allow programs using a lot of space on the external card to be held accountable. Bug: 3308791 Change-Id: If2df07bdbf6ffb31577074fad8f4a87ca4e89086
544 lines
22 KiB
Java
544 lines
22 KiB
Java
/*
|
|
* 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 com.android.internal.app.IMediaContainerService;
|
|
import com.android.internal.content.NativeLibraryHelper;
|
|
import com.android.internal.content.PackageHelper;
|
|
|
|
import android.content.Intent;
|
|
import android.content.pm.IPackageManager;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageInfoLite;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.PackageParser;
|
|
import android.content.res.ObbInfo;
|
|
import android.content.res.ObbScanner;
|
|
import android.net.Uri;
|
|
import android.os.Environment;
|
|
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.app.IntentService;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Log;
|
|
import android.util.Pair;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.zip.ZipEntry;
|
|
import java.util.zip.ZipException;
|
|
import java.util.zip.ZipFile;
|
|
|
|
import android.os.FileUtils;
|
|
import android.provider.Settings;
|
|
|
|
/*
|
|
* This service copies a downloaded apk to a file passed in as
|
|
* a ParcelFileDescriptor or to a newly created container specified
|
|
* by parameters. The DownloadManager gives access to this process
|
|
* based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
|
|
* permission to access apks downloaded via the download manager.
|
|
*/
|
|
public class DefaultContainerService extends IntentService {
|
|
private static final String TAG = "DefContainer";
|
|
private static final boolean localLOGV = true;
|
|
|
|
private static final String LIB_DIR_NAME = "lib";
|
|
|
|
private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
|
|
/*
|
|
* Creates a new container and copies resource there.
|
|
* @param paackageURI the uri of resource to be copied. Can be either
|
|
* a content uri or a file uri
|
|
* @param cid 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
|
|
* @param resFileName Name of the target resource file(relative to newly
|
|
* created secure container)
|
|
* @return Returns the new cache path where the resource has been copied into
|
|
*
|
|
*/
|
|
public String copyResourceToContainer(final Uri packageURI,
|
|
final String cid,
|
|
final String key, final String resFileName) {
|
|
if (packageURI == null || cid == null) {
|
|
return null;
|
|
}
|
|
return copyResourceInner(packageURI, cid, key, resFileName);
|
|
}
|
|
|
|
/*
|
|
* Copy specified resource to output stream
|
|
* @param packageURI the uri of resource to be copied. Should be a
|
|
* file uri
|
|
* @param outStream Remote file descriptor to be used for copying
|
|
* @return Returns true if copy succeded or false otherwise.
|
|
*/
|
|
public boolean copyResource(final Uri packageURI,
|
|
ParcelFileDescriptor outStream) {
|
|
if (packageURI == null || outStream == null) {
|
|
return false;
|
|
}
|
|
ParcelFileDescriptor.AutoCloseOutputStream
|
|
autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
|
|
return copyFile(packageURI, autoOut);
|
|
}
|
|
|
|
/*
|
|
* Determine the recommended install location for package
|
|
* specified by file uri location.
|
|
* @param fileUri the uri of resource to be copied. Should be a
|
|
* file uri
|
|
* @return Returns PackageInfoLite object containing
|
|
* the package info and recommended app location.
|
|
*/
|
|
public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags) {
|
|
PackageInfoLite ret = new PackageInfoLite();
|
|
if (fileUri == null) {
|
|
Log.i(TAG, "Invalid package uri " + fileUri);
|
|
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
|
|
return ret;
|
|
}
|
|
String scheme = fileUri.getScheme();
|
|
if (scheme != null && !scheme.equals("file")) {
|
|
Log.w(TAG, "Falling back to installing on internal storage only");
|
|
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
|
|
return ret;
|
|
}
|
|
String archiveFilePath = fileUri.getPath();
|
|
PackageParser packageParser = new PackageParser(archiveFilePath);
|
|
File sourceFile = new File(archiveFilePath);
|
|
DisplayMetrics metrics = new DisplayMetrics();
|
|
metrics.setToDefaults();
|
|
PackageParser.PackageLite pkg = packageParser.parsePackageLite(
|
|
archiveFilePath, 0);
|
|
// Nuke the parser reference right away and force a gc
|
|
packageParser = null;
|
|
Runtime.getRuntime().gc();
|
|
if (pkg == null) {
|
|
Log.w(TAG, "Failed to parse package");
|
|
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
|
|
return ret;
|
|
}
|
|
ret.packageName = pkg.packageName;
|
|
ret.installLocation = pkg.installLocation;
|
|
ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, archiveFilePath, flags);
|
|
return ret;
|
|
}
|
|
|
|
public boolean checkFreeStorage(boolean external, Uri fileUri) {
|
|
return checkFreeStorageInner(external, fileUri);
|
|
}
|
|
|
|
public ObbInfo getObbInfo(String filename) {
|
|
try {
|
|
return ObbScanner.getObbInfo(filename);
|
|
} catch (IOException e) {
|
|
Log.d(TAG, "Couldn't get OBB info for " + filename);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long calculateDirectorySize(String path) throws RemoteException {
|
|
final File directory = new File(path);
|
|
if (directory.exists() && directory.isDirectory()) {
|
|
return MeasurementUtils.measureDirectory(path);
|
|
} else {
|
|
return 0L;
|
|
}
|
|
}
|
|
};
|
|
|
|
public DefaultContainerService() {
|
|
super("DefaultContainerService");
|
|
setIntentRedelivery(true);
|
|
}
|
|
|
|
@Override
|
|
protected void onHandleIntent(Intent intent) {
|
|
if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
|
|
IPackageManager pm = IPackageManager.Stub.asInterface(
|
|
ServiceManager.getService("package"));
|
|
String pkg = null;
|
|
try {
|
|
while ((pkg=pm.nextPackageToClean(pkg)) != null) {
|
|
eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
|
|
eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
|
|
}
|
|
} catch (RemoteException e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public IBinder onBind(Intent intent) {
|
|
return mBinder;
|
|
}
|
|
|
|
private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) {
|
|
// Make sure the sdcard is mounted.
|
|
String status = Environment.getExternalStorageState();
|
|
if (!status.equals(Environment.MEDIA_MOUNTED)) {
|
|
Log.w(TAG, "Make sure sdcard is mounted.");
|
|
return null;
|
|
}
|
|
|
|
// The .apk file
|
|
String codePath = packageURI.getPath();
|
|
File codeFile = new File(codePath);
|
|
|
|
// Calculate size of container needed to hold base APK.
|
|
long sizeBytes = codeFile.length();
|
|
|
|
// Check all the native files that need to be copied and add that to the container size.
|
|
ZipFile zipFile;
|
|
List<Pair<ZipEntry, String>> nativeFiles;
|
|
try {
|
|
zipFile = new ZipFile(codeFile);
|
|
|
|
nativeFiles = new LinkedList<Pair<ZipEntry, String>>();
|
|
|
|
NativeLibraryHelper.listPackageNativeBinariesLI(zipFile, nativeFiles);
|
|
|
|
final int N = nativeFiles.size();
|
|
for (int i = 0; i < N; i++) {
|
|
final Pair<ZipEntry, String> entry = nativeFiles.get(i);
|
|
|
|
/*
|
|
* Note that PackageHelper.createSdDir adds a 1MB padding on
|
|
* our claimed size, so we don't have to worry about block
|
|
* alignment here.
|
|
*/
|
|
sizeBytes += entry.first.getSize();
|
|
}
|
|
} catch (ZipException e) {
|
|
Log.w(TAG, "Failed to extract data from package file", e);
|
|
return null;
|
|
} catch (IOException e) {
|
|
Log.w(TAG, "Failed to cache package shared libs", e);
|
|
return null;
|
|
}
|
|
|
|
// Create new container
|
|
String newCachePath = null;
|
|
if ((newCachePath = PackageHelper.createSdDir(sizeBytes, newCid, key, Process.myUid())) == null) {
|
|
Log.e(TAG, "Failed to create container " + newCid);
|
|
return null;
|
|
}
|
|
if (localLOGV)
|
|
Log.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
|
|
File resFile = new File(newCachePath, resFileName);
|
|
if (!FileUtils.copyFile(new File(codePath), resFile)) {
|
|
Log.e(TAG, "Failed to copy " + codePath + " to " + resFile);
|
|
// Clean up container
|
|
PackageHelper.destroySdDir(newCid);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
|
|
sharedLibraryDir.mkdir();
|
|
|
|
final int N = nativeFiles.size();
|
|
for (int i = 0; i < N; i++) {
|
|
final Pair<ZipEntry, String> entry = nativeFiles.get(i);
|
|
|
|
InputStream is = zipFile.getInputStream(entry.first);
|
|
try {
|
|
File destFile = new File(sharedLibraryDir, entry.second);
|
|
if (!FileUtils.copyToFile(is, destFile)) {
|
|
throw new IOException("Couldn't copy native binary "
|
|
+ entry.first.getName() + " to " + entry.second);
|
|
}
|
|
} finally {
|
|
is.close();
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "Couldn't copy native file to container", e);
|
|
PackageHelper.destroySdDir(newCid);
|
|
return null;
|
|
}
|
|
|
|
if (localLOGV) Log.i(TAG, "Copied " + codePath + " to " + resFile);
|
|
if (!PackageHelper.finalizeSdDir(newCid)) {
|
|
Log.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
|
|
// Clean up container
|
|
PackageHelper.destroySdDir(newCid);
|
|
}
|
|
if (localLOGV) Log.i(TAG, "Finalized container " + newCid);
|
|
if (PackageHelper.isContainerMounted(newCid)) {
|
|
if (localLOGV) Log.i(TAG, "Unmounting " + newCid +
|
|
" at path " + newCachePath);
|
|
// Force a gc to avoid being killed.
|
|
Runtime.getRuntime().gc();
|
|
PackageHelper.unMountSdDir(newCid);
|
|
} else {
|
|
if (localLOGV) Log.i(TAG, "Container " + newCid + " not mounted");
|
|
}
|
|
return newCachePath;
|
|
}
|
|
|
|
public static boolean copyToFile(InputStream inputStream, FileOutputStream out) {
|
|
try {
|
|
byte[] buffer = new byte[4096];
|
|
int bytesRead;
|
|
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
|
out.write(buffer, 0, bytesRead);
|
|
}
|
|
return true;
|
|
} catch (IOException e) {
|
|
Log.i(TAG, "Exception : " + e + " when copying file");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static boolean copyToFile(File srcFile, FileOutputStream out) {
|
|
InputStream inputStream = null;
|
|
try {
|
|
inputStream = new FileInputStream(srcFile);
|
|
return copyToFile(inputStream, out);
|
|
} catch (IOException e) {
|
|
return false;
|
|
} finally {
|
|
try { if (inputStream != null) inputStream.close(); } catch (IOException e) {}
|
|
}
|
|
}
|
|
|
|
private boolean copyFile(Uri pPackageURI, FileOutputStream outStream) {
|
|
String scheme = pPackageURI.getScheme();
|
|
if (scheme == null || scheme.equals("file")) {
|
|
final File srcPackageFile = new File(pPackageURI.getPath());
|
|
// We copy the source package file to a temp file and then rename it to the
|
|
// destination file in order to eliminate a window where the package directory
|
|
// scanner notices the new package file but it's not completely copied yet.
|
|
if (!copyToFile(srcPackageFile, outStream)) {
|
|
Log.e(TAG, "Couldn't copy file: " + srcPackageFile);
|
|
return false;
|
|
}
|
|
} else if (scheme.equals("content")) {
|
|
ParcelFileDescriptor fd = null;
|
|
try {
|
|
fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
|
|
} catch (FileNotFoundException e) {
|
|
Log.e(TAG, "Couldn't open file descriptor from download service. Failed with exception " + e);
|
|
return false;
|
|
}
|
|
if (fd == null) {
|
|
Log.e(TAG, "Couldn't open file descriptor from download service (null).");
|
|
return false;
|
|
} else {
|
|
if (localLOGV) {
|
|
Log.v(TAG, "Opened file descriptor from download service.");
|
|
}
|
|
ParcelFileDescriptor.AutoCloseInputStream
|
|
dlStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
|
|
// We copy the source package file to a temp file and then rename it to the
|
|
// destination file in order to eliminate a window where the package directory
|
|
// scanner notices the new package file but it's not completely copied yet.
|
|
if (!copyToFile(dlStream, outStream)) {
|
|
Log.e(TAG, "Couldn't copy " + pPackageURI + " to temp file.");
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
Log.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Constants related to app heuristics
|
|
// No-installation limit for internal flash: 10% or less space available
|
|
private static final double LOW_NAND_FLASH_TRESHOLD = 0.1;
|
|
|
|
// SD-to-internal app size threshold: currently set to 1 MB
|
|
private static final long INSTALL_ON_SD_THRESHOLD = (1024 * 1024);
|
|
private static final int ERR_LOC = -1;
|
|
|
|
private int recommendAppInstallLocation(int installLocation,
|
|
String archiveFilePath, int flags) {
|
|
boolean checkInt = false;
|
|
boolean checkExt = false;
|
|
boolean checkBoth = false;
|
|
check_inner : {
|
|
// Check flags.
|
|
if ((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) {
|
|
// Check for forward locked app
|
|
checkInt = true;
|
|
break check_inner;
|
|
} else if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
|
|
// Explicit flag to install internally.
|
|
// Check internal storage and return
|
|
checkInt = true;
|
|
break check_inner;
|
|
} else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
|
|
// Explicit flag to install externally.
|
|
// Check external storage and return
|
|
checkExt = true;
|
|
break check_inner;
|
|
}
|
|
// Check for manifest option
|
|
if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
|
|
checkInt = true;
|
|
break check_inner;
|
|
} else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
|
|
checkExt = true;
|
|
checkBoth = true;
|
|
break check_inner;
|
|
} else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
|
|
checkInt = true;
|
|
checkBoth = true;
|
|
break check_inner;
|
|
}
|
|
// Pick user preference
|
|
int installPreference = Settings.System.getInt(getApplicationContext()
|
|
.getContentResolver(),
|
|
Settings.Secure.DEFAULT_INSTALL_LOCATION,
|
|
PackageHelper.APP_INSTALL_AUTO);
|
|
if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
|
|
checkInt = true;
|
|
break check_inner;
|
|
} else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
|
|
checkExt = true;
|
|
break check_inner;
|
|
}
|
|
// Fall back to default policy if nothing else is specified.
|
|
checkInt = true;
|
|
}
|
|
|
|
// Package size = code size + cache size + data size
|
|
// If code size > 1 MB, install on SD card.
|
|
// Else install on internal NAND flash, unless space on NAND is less than 10%
|
|
String status = Environment.getExternalStorageState();
|
|
long availSDSize = -1;
|
|
boolean mediaAvailable = false;
|
|
if (!Environment.isExternalStorageEmulated() && status.equals(Environment.MEDIA_MOUNTED)) {
|
|
StatFs sdStats = new StatFs(
|
|
Environment.getExternalStorageDirectory().getPath());
|
|
availSDSize = (long)sdStats.getAvailableBlocks() *
|
|
(long)sdStats.getBlockSize();
|
|
mediaAvailable = true;
|
|
}
|
|
StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
|
|
long totalInternalSize = (long)internalStats.getBlockCount() *
|
|
(long)internalStats.getBlockSize();
|
|
long availInternalSize = (long)internalStats.getAvailableBlocks() *
|
|
(long)internalStats.getBlockSize();
|
|
|
|
double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
|
|
|
|
File apkFile = new File(archiveFilePath);
|
|
long pkgLen = apkFile.length();
|
|
|
|
// To make final copy
|
|
long reqInstallSize = pkgLen;
|
|
// For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
|
|
long reqInternalSize = 0;
|
|
boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
|
|
boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
|
|
boolean fitsOnSd = false;
|
|
if (mediaAvailable && (reqInstallSize < availSDSize)) {
|
|
// If we do not have an internal size requirement
|
|
// don't do a threshold check.
|
|
if (reqInternalSize == 0) {
|
|
fitsOnSd = true;
|
|
} else if ((reqInternalSize < availInternalSize) && intThresholdOk) {
|
|
fitsOnSd = true;
|
|
}
|
|
}
|
|
boolean fitsOnInt = intThresholdOk && intAvailOk;
|
|
if (checkInt) {
|
|
// Check for internal memory availability
|
|
if (fitsOnInt) {
|
|
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
|
|
}
|
|
} else if (checkExt) {
|
|
if (fitsOnSd) {
|
|
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
|
|
}
|
|
}
|
|
if (checkBoth) {
|
|
// Check for internal first
|
|
if (fitsOnInt) {
|
|
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
|
|
}
|
|
// Check for external next
|
|
if (fitsOnSd) {
|
|
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
|
|
}
|
|
}
|
|
if ((checkExt || checkBoth) && !mediaAvailable) {
|
|
return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
|
|
}
|
|
return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
|
|
}
|
|
|
|
private boolean checkFreeStorageInner(boolean external, Uri packageURI) {
|
|
File apkFile = new File(packageURI.getPath());
|
|
long size = apkFile.length();
|
|
if (external) {
|
|
String status = Environment.getExternalStorageState();
|
|
long availSDSize = -1;
|
|
if (status.equals(Environment.MEDIA_MOUNTED)) {
|
|
StatFs sdStats = new StatFs(
|
|
Environment.getExternalStorageDirectory().getPath());
|
|
availSDSize = (long)sdStats.getAvailableBlocks() *
|
|
(long)sdStats.getBlockSize();
|
|
}
|
|
return availSDSize > size;
|
|
}
|
|
StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
|
|
long totalInternalSize = (long)internalStats.getBlockCount() *
|
|
(long)internalStats.getBlockSize();
|
|
long availInternalSize = (long)internalStats.getAvailableBlocks() *
|
|
(long)internalStats.getBlockSize();
|
|
|
|
double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
|
|
// To make final copy
|
|
long reqInstallSize = size;
|
|
// For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
|
|
long reqInternalSize = 0;
|
|
boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
|
|
boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
|
|
return intThresholdOk && intAvailOk;
|
|
}
|
|
}
|