blob: 8e030e504965cc9a5c7c771a375895e29ddb4a94 [file] [log] [blame]
package com.android.defcontainer;
import com.android.internal.app.IMediaContainerService;
import com.android.internal.content.PackageHelper;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.Package;
import android.net.Uri;
import android.os.Debug;
import android.os.Environment;
import android.os.IBinder;
import android.os.storage.IMountService;
import android.os.storage.StorageResultCode;
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.app.Service;
import android.util.DisplayMetrics;
import android.util.Log;
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.io.OutputStream;
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 = false;
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 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
* @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 containerId,
final String key, final String resFileName) {
if (packageURI == null || containerId == null) {
return null;
}
return copyResourceInner(packageURI, containerId, 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
* PackageHelper.RECOMMEND_INSTALL_INTERNAL to install on internal storage
* PackageHelper.RECOMMEND_INSTALL_EXTERNAL to install on external media
* PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE for storage errors
* PackageHelper.RECOMMEND_FAILED_INVALID_APK for parse errors.
*/
public int getRecommendedInstallLocation(final Uri fileUri) {
if (!fileUri.getScheme().equals("file")) {
Log.w(TAG, "Falling back to installing on internal storage only");
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
}
final String archiveFilePath = fileUri.getPath();
PackageParser packageParser = new PackageParser(archiveFilePath);
File sourceFile = new File(archiveFilePath);
DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
PackageParser.Package pkg = packageParser.parsePackage(sourceFile, archiveFilePath, metrics, 0);
if (pkg == null) {
Log.w(TAG, "Failed to parse package");
return PackageHelper.RECOMMEND_FAILED_INVALID_APK;
}
int loc = recommendAppInstallLocation(pkg);
if (loc == PackageManager.INSTALL_EXTERNAL) {
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
} else if (loc == ERR_LOC) {
Log.i(TAG, "Failed to install insufficient storage");
return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
} else {
// Implies install on internal storage.
return 0;
}
}
};
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 IMountService getMountService() {
return IMountService.Stub.asInterface(ServiceManager.getService("mount"));
}
private String copyResourceInner(Uri packageURI, String newCacheId, String key, String resFileName) {
// Create new container at newCachePath
String codePath = packageURI.getPath();
String newCachePath = null;
final int CREATE_FAILED = 1;
final int COPY_FAILED = 2;
final int FINALIZE_FAILED = 3;
final int PASS = 4;
int errCode = CREATE_FAILED;
// Create new container
if ((newCachePath = createSdDir(packageURI, newCacheId, key)) != null) {
if (localLOGV) Log.i(TAG, "Created container for " + newCacheId
+ " at path : " + newCachePath);
File resFile = new File(newCachePath, resFileName);
errCode = COPY_FAILED;
// Copy file from codePath
if (FileUtils.copyFile(new File(codePath), resFile)) {
if (localLOGV) Log.i(TAG, "Copied " + codePath + " to " + resFile);
errCode = FINALIZE_FAILED;
if (finalizeSdDir(newCacheId)) {
errCode = PASS;
}
}
}
// Print error based on errCode
String errMsg = "";
switch (errCode) {
case CREATE_FAILED:
errMsg = "CREATE_FAILED";
break;
case COPY_FAILED:
errMsg = "COPY_FAILED";
if (localLOGV) Log.i(TAG, "Destroying " + newCacheId +
" at path " + newCachePath + " after " + errMsg);
destroySdDir(newCacheId);
break;
case FINALIZE_FAILED:
errMsg = "FINALIZE_FAILED";
if (localLOGV) Log.i(TAG, "Destroying " + newCacheId +
" at path " + newCachePath + " after " + errMsg);
destroySdDir(newCacheId);
break;
default:
errMsg = "PASS";
if (localLOGV) Log.i(TAG, "Unmounting " + newCacheId +
" at path " + newCachePath + " after " + errMsg);
unMountSdDir(newCacheId);
break;
}
if (errCode != PASS) {
return null;
}
return newCachePath;
}
private String createSdDir(final Uri packageURI,
String containerId, String sdEncKey) {
File tmpPackageFile = new File(packageURI.getPath());
// Create mount point via MountService
IMountService mountService = getMountService();
long len = tmpPackageFile.length();
int mbLen = (int) (len/(1024*1024));
if ((len - (mbLen * 1024 * 1024)) > 0) {
mbLen++;
}
if (localLOGV) Log.i(TAG, "mbLen=" + mbLen);
String cachePath = null;
int ownerUid = Process.myUid();
try {
int rc = mountService.createSecureContainer(
containerId, mbLen, "vfat", sdEncKey, ownerUid);
if (rc != StorageResultCode.OperationSucceeded) {
Log.e(TAG, String.format("Container creation failed (%d)", rc));
// XXX: This destroy should not be necessary
rc = mountService.destroySecureContainer(containerId);
if (rc != StorageResultCode.OperationSucceeded) {
Log.e(TAG, String.format("Container creation-cleanup failed (%d)", rc));
return null;
}
// XXX: Does this ever actually succeed?
rc = mountService.createSecureContainer(
containerId, mbLen, "vfat", sdEncKey, ownerUid);
if (rc != StorageResultCode.OperationSucceeded) {
Log.e(TAG, String.format("Container creation retry failed (%d)", rc));
}
}
cachePath = mountService.getSecureContainerPath(containerId);
if (localLOGV) Log.i(TAG, "Trying to create secure container for "
+ containerId + ", cachePath =" + cachePath);
return cachePath;
} catch(RemoteException e) {
Log.e(TAG, "MountService not running?");
return null;
}
}
private boolean destroySdDir(String containerId) {
try {
// We need to destroy right away
getMountService().destroySecureContainer(containerId);
return true;
} catch (IllegalStateException e) {
Log.i(TAG, "Failed to destroy container : " + containerId);
} catch(RemoteException e) {
Log.e(TAG, "MountService not running?");
}
return false;
}
private boolean finalizeSdDir(String containerId){
try {
getMountService().finalizeSecureContainer(containerId);
return true;
} catch (IllegalStateException e) {
Log.i(TAG, "Failed to finalize container for pkg : " + containerId);
} catch(RemoteException e) {
Log.e(TAG, "MountService not running?");
}
return false;
}
private boolean unMountSdDir(String containerId) {
try {
getMountService().unmountSecureContainer(containerId);
return true;
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to unmount id: " + containerId + " with exception " + e);
} catch(RemoteException e) {
Log.e(TAG, "MountService not running?");
}
return false;
}
private String mountSdDir(String containerId, String key) {
try {
int rc = getMountService().mountSecureContainer(containerId, key, Process.myUid());
if (rc == StorageResultCode.OperationSucceeded) {
return getMountService().getSecureContainerPath(containerId);
} else {
Log.e(TAG, String.format("Failed to mount id %s with rc %d ", containerId, rc));
}
} catch(RemoteException e) {
Log.e(TAG, "MountService not running?");
}
return null;
}
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) {
if (pPackageURI.getScheme().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 (pPackageURI.getScheme().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;
public int recommendAppInstallLocation(Package pkg) {
// Initial implementation:
// 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%
if (pkg == null) {
return ERR_LOC;
}
StatFs internalFlashStats = new StatFs(Environment.getDataDirectory().getPath());
StatFs sdcardStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
long totalInternalFlashSize = (long)internalFlashStats.getBlockCount() *
(long)internalFlashStats.getBlockSize();
long availInternalFlashSize = (long)internalFlashStats.getAvailableBlocks() *
(long)internalFlashStats.getBlockSize();
long availSDSize = (long)sdcardStats.getAvailableBlocks() *
(long)sdcardStats.getBlockSize();
double pctNandFree = (double)availInternalFlashSize / (double)totalInternalFlashSize;
final String archiveFilePath = pkg.mScanPath;
File apkFile = new File(archiveFilePath);
long pkgLen = apkFile.length();
boolean auto = true;
// To make final copy
long reqInstallSize = pkgLen;
// For dex files
long reqInternalSize = 1 * pkgLen;
boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalFlashSize);
boolean fitsOnSd = (reqInstallSize < availSDSize) && intThresholdOk &&
(reqInternalSize < availInternalFlashSize);
boolean fitsOnInt = intThresholdOk && intAvailOk;
// Consider application flags preferences as well...
boolean installOnlyOnSd = (pkg.installLocation ==
PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL);
boolean installOnlyInternal = (pkg.installLocation ==
PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY);
if (installOnlyInternal) {
// If set explicitly in manifest,
// let that override everything else
auto = false;
} else if (installOnlyOnSd){
// Check if this can be accommodated on the sdcard
if (fitsOnSd) {
auto = false;
}
} else {
// Check if user option is enabled
boolean setInstallLoc = Settings.System.getInt(getApplicationContext()
.getContentResolver(),
Settings.System.SET_INSTALL_LOCATION, 0) != 0;
if (setInstallLoc) {
// Pick user preference
int installPreference = Settings.System.getInt(getApplicationContext()
.getContentResolver(),
Settings.System.DEFAULT_INSTALL_LOCATION,
PackageInfo.INSTALL_LOCATION_AUTO);
if (installPreference == 1) {
installOnlyInternal = true;
auto = false;
} else if (installPreference == 2) {
installOnlyOnSd = true;
auto = false;
}
}
}
if (!auto) {
if (installOnlyOnSd) {
return fitsOnSd ? PackageManager.INSTALL_EXTERNAL : ERR_LOC;
} else if (installOnlyInternal){
// Check on internal flash
return fitsOnInt ? 0 : ERR_LOC;
}
}
// Try to install internally
if (fitsOnInt) {
return 0;
}
// Try the sdcard now.
if (fitsOnSd) {
return PackageManager.INSTALL_EXTERNAL;
}
// Return error code
return ERR_LOC;
}
}