| /* |
| * 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); |
| } |
| } |
| |
| @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); |
| } |
| } |
| }; |
| |
| 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; |
| } |
| } |