Change the thread priority for all disk measurement and statfs calls to background priority. Also move the measurement fully into the measurement task since it makes more sense. Bug: 6332097 Change-Id: Iafc2151313ad9b14117daf67e933dccd32f68d54
822 lines
31 KiB
Java
822 lines
31 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.app.IntentService;
|
|
import android.content.Intent;
|
|
import android.content.pm.MacAuthenticatedInputStream;
|
|
import android.content.pm.ContainerEncryptionParams;
|
|
import android.content.pm.IPackageManager;
|
|
import android.content.pm.LimitedLengthInputStream;
|
|
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.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.util.DisplayMetrics;
|
|
import android.util.Slog;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.security.DigestException;
|
|
import java.security.GeneralSecurityException;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.CipherInputStream;
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
|
|
import libcore.io.ErrnoException;
|
|
import libcore.io.IoUtils;
|
|
import libcore.io.Libcore;
|
|
import libcore.io.Streams;
|
|
import libcore.io.StructStatFs;
|
|
|
|
/*
|
|
* 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, final String publicResFileName,
|
|
boolean isExternal, boolean isForwardLocked) {
|
|
if (packageURI == null || cid == null) {
|
|
return null;
|
|
}
|
|
|
|
return copyResourceInner(packageURI, cid, key, resFileName, publicResFileName,
|
|
isExternal, isForwardLocked);
|
|
}
|
|
|
|
/**
|
|
* Copy specified resource to output stream
|
|
*
|
|
* @param packageURI the uri of resource to be copied. Should be a file
|
|
* uri
|
|
* @param encryptionParams parameters describing the encryption used for
|
|
* this file
|
|
* @param outStream Remote file descriptor to be used for copying
|
|
* @return returns status code according to those in
|
|
* {@link PackageManager}
|
|
*/
|
|
public int copyResource(final Uri packageURI, ContainerEncryptionParams encryptionParams,
|
|
ParcelFileDescriptor outStream) {
|
|
if (packageURI == null || outStream == null) {
|
|
return PackageManager.INSTALL_FAILED_INVALID_URI;
|
|
}
|
|
|
|
ParcelFileDescriptor.AutoCloseOutputStream autoOut
|
|
= new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
|
|
|
|
try {
|
|
copyFile(packageURI, autoOut, encryptionParams);
|
|
return PackageManager.INSTALL_SUCCEEDED;
|
|
} catch (FileNotFoundException e) {
|
|
Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: "
|
|
+ e.getMessage());
|
|
return PackageManager.INSTALL_FAILED_INVALID_URI;
|
|
} catch (IOException e) {
|
|
Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: "
|
|
+ e.getMessage());
|
|
return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
|
|
} catch (DigestException e) {
|
|
Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " Security: "
|
|
+ e.getMessage());
|
|
return PackageManager.INSTALL_FAILED_INVALID_APK;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 String packagePath, int flags,
|
|
long threshold) {
|
|
PackageInfoLite ret = new PackageInfoLite();
|
|
|
|
if (packagePath == null) {
|
|
Slog.i(TAG, "Invalid package file " + packagePath);
|
|
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
|
|
return ret;
|
|
}
|
|
|
|
DisplayMetrics metrics = new DisplayMetrics();
|
|
metrics.setToDefaults();
|
|
|
|
PackageParser.PackageLite pkg = PackageParser.parsePackageLite(packagePath, 0);
|
|
if (pkg == null) {
|
|
Slog.w(TAG, "Failed to parse package");
|
|
|
|
final File apkFile = new File(packagePath);
|
|
if (!apkFile.exists()) {
|
|
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
|
|
} else {
|
|
ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
ret.packageName = pkg.packageName;
|
|
ret.installLocation = pkg.installLocation;
|
|
ret.verifiers = pkg.verifiers;
|
|
|
|
ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation,
|
|
packagePath, flags, threshold);
|
|
|
|
return ret;
|
|
}
|
|
|
|
@Override
|
|
public boolean checkInternalFreeStorage(Uri packageUri, boolean isForwardLocked,
|
|
long threshold) throws RemoteException {
|
|
final File apkFile = new File(packageUri.getPath());
|
|
try {
|
|
return isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
|
|
} catch (IOException e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean checkExternalFreeStorage(Uri packageUri, boolean isForwardLocked)
|
|
throws RemoteException {
|
|
final File apkFile = new File(packageUri.getPath());
|
|
try {
|
|
return isUnderExternalThreshold(apkFile, isForwardLocked);
|
|
} catch (IOException e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
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 directory = new File(path);
|
|
if (directory.exists() && directory.isDirectory()) {
|
|
return MeasurementUtils.measureDirectory(path);
|
|
} else {
|
|
return 0L;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long[] getFileSystemStats(String path) {
|
|
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
|
|
|
try {
|
|
final StructStatFs stat = Libcore.os.statfs(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);
|
|
}
|
|
}
|
|
};
|
|
|
|
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));
|
|
eraseFiles(Environment.getExternalStorageAppObbDirectory(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,
|
|
String publicResFileName, boolean isExternal, boolean isForwardLocked) {
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// The .apk file
|
|
String codePath = packageURI.getPath();
|
|
File codeFile = new File(codePath);
|
|
|
|
// Calculate size of container needed to hold base APK.
|
|
final int sizeMb;
|
|
try {
|
|
sizeMb = calculateContainerSize(codeFile, isForwardLocked);
|
|
} 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 {
|
|
Libcore.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 {
|
|
Libcore.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 = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir);
|
|
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 static void copyToFile(InputStream inputStream, OutputStream out) throws IOException {
|
|
byte[] buffer = new byte[16384];
|
|
int bytesRead;
|
|
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
|
out.write(buffer, 0, bytesRead);
|
|
}
|
|
}
|
|
|
|
private void copyFile(Uri pPackageURI, OutputStream outStream,
|
|
ContainerEncryptionParams encryptionParams) throws FileNotFoundException, IOException,
|
|
DigestException {
|
|
String scheme = pPackageURI.getScheme();
|
|
InputStream inStream = null;
|
|
try {
|
|
if (scheme == null || scheme.equals("file")) {
|
|
final InputStream is = new FileInputStream(new File(pPackageURI.getPath()));
|
|
inStream = new BufferedInputStream(is);
|
|
} else if (scheme.equals("content")) {
|
|
final ParcelFileDescriptor fd;
|
|
try {
|
|
fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
|
|
} catch (FileNotFoundException e) {
|
|
Slog.e(TAG, "Couldn't open file descriptor from download service. "
|
|
+ "Failed with exception " + e);
|
|
throw e;
|
|
}
|
|
|
|
if (fd == null) {
|
|
Slog.e(TAG, "Provider returned no file descriptor for " +
|
|
pPackageURI.toString());
|
|
throw new FileNotFoundException("provider returned no file descriptor");
|
|
} else {
|
|
if (localLOGV) {
|
|
Slog.i(TAG, "Opened file descriptor from download service.");
|
|
}
|
|
inStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
|
|
}
|
|
} else {
|
|
Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
|
|
throw new FileNotFoundException("Package URI is not 'file:' or 'content:'");
|
|
}
|
|
|
|
/*
|
|
* If this resource is encrypted, get the decrypted stream version
|
|
* of it.
|
|
*/
|
|
ApkContainer container = new ApkContainer(inStream, encryptionParams);
|
|
|
|
try {
|
|
/*
|
|
* 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.
|
|
*/
|
|
copyToFile(container.getInputStream(), outStream);
|
|
|
|
if (!container.isAuthenticated()) {
|
|
throw new DigestException();
|
|
}
|
|
} catch (GeneralSecurityException e) {
|
|
throw new DigestException("A problem occured copying the file.");
|
|
}
|
|
} finally {
|
|
IoUtils.closeQuietly(inStream);
|
|
}
|
|
}
|
|
|
|
private static class ApkContainer {
|
|
private static final int MAX_AUTHENTICATED_DATA_SIZE = 16384;
|
|
|
|
private final InputStream mInStream;
|
|
|
|
private MacAuthenticatedInputStream mAuthenticatedStream;
|
|
|
|
private byte[] mTag;
|
|
|
|
public ApkContainer(InputStream inStream, ContainerEncryptionParams encryptionParams)
|
|
throws IOException {
|
|
if (encryptionParams == null) {
|
|
mInStream = inStream;
|
|
} else {
|
|
mInStream = getDecryptedStream(inStream, encryptionParams);
|
|
mTag = encryptionParams.getMacTag();
|
|
}
|
|
}
|
|
|
|
public boolean isAuthenticated() {
|
|
if (mAuthenticatedStream == null) {
|
|
return true;
|
|
}
|
|
|
|
return mAuthenticatedStream.isTagEqual(mTag);
|
|
}
|
|
|
|
private Mac getMacInstance(ContainerEncryptionParams encryptionParams) throws IOException {
|
|
final Mac m;
|
|
try {
|
|
final String macAlgo = encryptionParams.getMacAlgorithm();
|
|
|
|
if (macAlgo != null) {
|
|
m = Mac.getInstance(macAlgo);
|
|
m.init(encryptionParams.getMacKey(), encryptionParams.getMacSpec());
|
|
} else {
|
|
m = null;
|
|
}
|
|
|
|
return m;
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new IOException(e);
|
|
} catch (InvalidKeyException e) {
|
|
throw new IOException(e);
|
|
} catch (InvalidAlgorithmParameterException e) {
|
|
throw new IOException(e);
|
|
}
|
|
}
|
|
|
|
public InputStream getInputStream() {
|
|
return mInStream;
|
|
}
|
|
|
|
private InputStream getDecryptedStream(InputStream inStream,
|
|
ContainerEncryptionParams encryptionParams) throws IOException {
|
|
final Cipher c;
|
|
try {
|
|
c = Cipher.getInstance(encryptionParams.getEncryptionAlgorithm());
|
|
c.init(Cipher.DECRYPT_MODE, encryptionParams.getEncryptionKey(),
|
|
encryptionParams.getEncryptionSpec());
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new IOException(e);
|
|
} catch (NoSuchPaddingException e) {
|
|
throw new IOException(e);
|
|
} catch (InvalidKeyException e) {
|
|
throw new IOException(e);
|
|
} catch (InvalidAlgorithmParameterException e) {
|
|
throw new IOException(e);
|
|
}
|
|
|
|
final long encStart = encryptionParams.getEncryptedDataStart();
|
|
final long end = encryptionParams.getDataEnd();
|
|
if (end < encStart) {
|
|
throw new IOException("end <= encStart");
|
|
}
|
|
|
|
final Mac mac = getMacInstance(encryptionParams);
|
|
if (mac != null) {
|
|
final long macStart = encryptionParams.getAuthenticatedDataStart();
|
|
if (macStart >= Integer.MAX_VALUE) {
|
|
throw new IOException("macStart >= Integer.MAX_VALUE");
|
|
}
|
|
|
|
final long furtherOffset;
|
|
if (macStart >= 0 && encStart >= 0 && macStart < encStart) {
|
|
/*
|
|
* If there is authenticated data at the beginning, read
|
|
* that into our MAC first.
|
|
*/
|
|
final long authenticatedLengthLong = encStart - macStart;
|
|
if (authenticatedLengthLong > MAX_AUTHENTICATED_DATA_SIZE) {
|
|
throw new IOException("authenticated data is too long");
|
|
}
|
|
final int authenticatedLength = (int) authenticatedLengthLong;
|
|
|
|
final byte[] authenticatedData = new byte[(int) authenticatedLength];
|
|
|
|
Streams.readFully(inStream, authenticatedData, (int) macStart,
|
|
authenticatedLength);
|
|
mac.update(authenticatedData, 0, authenticatedLength);
|
|
|
|
furtherOffset = 0;
|
|
} else {
|
|
/*
|
|
* No authenticated data at the beginning. Just skip the
|
|
* required number of bytes to the beginning of the stream.
|
|
*/
|
|
if (encStart > 0) {
|
|
furtherOffset = encStart;
|
|
} else {
|
|
furtherOffset = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If there is data at the end of the stream we want to ignore,
|
|
* wrap this in a LimitedLengthInputStream.
|
|
*/
|
|
if (furtherOffset >= 0 && end > furtherOffset) {
|
|
inStream = new LimitedLengthInputStream(inStream, furtherOffset, end - encStart);
|
|
} else if (furtherOffset > 0) {
|
|
inStream.skip(furtherOffset);
|
|
}
|
|
|
|
mAuthenticatedStream = new MacAuthenticatedInputStream(inStream, mac);
|
|
|
|
inStream = mAuthenticatedStream;
|
|
} else {
|
|
if (encStart >= 0) {
|
|
if (end > encStart) {
|
|
inStream = new LimitedLengthInputStream(inStream, encStart, end - encStart);
|
|
} else {
|
|
inStream.skip(encStart);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new CipherInputStream(inStream, c);
|
|
}
|
|
|
|
}
|
|
|
|
private static final int PREFER_INTERNAL = 1;
|
|
private static final int PREFER_EXTERNAL = 2;
|
|
|
|
private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
|
|
long threshold) {
|
|
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 (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
|
|
prefer = PREFER_INTERNAL;
|
|
break check_inner;
|
|
} else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
|
|
prefer = PREFER_EXTERNAL;
|
|
checkBoth = true;
|
|
break check_inner;
|
|
} else if (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.System.getInt(getApplicationContext()
|
|
.getContentResolver(),
|
|
Settings.Secure.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();
|
|
|
|
final File apkFile = new File(archiveFilePath);
|
|
|
|
boolean fitsOnInternal = false;
|
|
if (checkBoth || prefer == PREFER_INTERNAL) {
|
|
try {
|
|
fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
|
|
} catch (IOException e) {
|
|
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
|
|
}
|
|
}
|
|
|
|
boolean fitsOnSd = false;
|
|
if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
|
|
try {
|
|
fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked);
|
|
} 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 apkFile file to check
|
|
* @param threshold byte threshold to compare against
|
|
* @return true if file fits under threshold
|
|
* @throws FileNotFoundException when APK does not exist
|
|
*/
|
|
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;
|
|
}
|
|
|
|
|
|
/**
|
|
* Measure a file to see if it fits in the external free space.
|
|
*
|
|
* @param apkFile file to check
|
|
* @return true if file fits
|
|
* @throws IOException when file does not exist
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Calculate the container size for an APK. Takes into account the
|
|
*
|
|
* @param apkFile file from which to calculate size
|
|
* @return size in megabytes (2^20 bytes)
|
|
* @throws IOException when there is a problem reading the file
|
|
*/
|
|
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;
|
|
}
|
|
}
|