Home | History | Annotate | Download | only in defcontainer
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.defcontainer;
     18 
     19 import android.app.IntentService;
     20 import android.content.Intent;
     21 import android.content.pm.ContainerEncryptionParams;
     22 import android.content.pm.IPackageManager;
     23 import android.content.pm.LimitedLengthInputStream;
     24 import android.content.pm.MacAuthenticatedInputStream;
     25 import android.content.pm.PackageCleanItem;
     26 import android.content.pm.PackageInfo;
     27 import android.content.pm.PackageInfoLite;
     28 import android.content.pm.PackageManager;
     29 import android.content.pm.PackageParser;
     30 import android.content.res.ObbInfo;
     31 import android.content.res.ObbScanner;
     32 import android.net.Uri;
     33 import android.os.Environment;
     34 import android.os.Environment.UserEnvironment;
     35 import android.os.FileUtils;
     36 import android.os.IBinder;
     37 import android.os.ParcelFileDescriptor;
     38 import android.os.Process;
     39 import android.os.RemoteException;
     40 import android.os.ServiceManager;
     41 import android.os.StatFs;
     42 import android.os.SystemClock;
     43 import android.provider.Settings;
     44 import android.util.DisplayMetrics;
     45 import android.util.Log;
     46 import android.util.Slog;
     47 
     48 import com.android.internal.app.IMediaContainerService;
     49 import com.android.internal.content.NativeLibraryHelper;
     50 import com.android.internal.content.PackageHelper;
     51 
     52 import java.io.BufferedInputStream;
     53 import java.io.File;
     54 import java.io.FileInputStream;
     55 import java.io.FileNotFoundException;
     56 import java.io.IOException;
     57 import java.io.InputStream;
     58 import java.io.OutputStream;
     59 import java.security.DigestException;
     60 import java.security.GeneralSecurityException;
     61 import java.security.InvalidAlgorithmParameterException;
     62 import java.security.InvalidKeyException;
     63 import java.security.NoSuchAlgorithmException;
     64 
     65 import javax.crypto.Cipher;
     66 import javax.crypto.CipherInputStream;
     67 import javax.crypto.Mac;
     68 import javax.crypto.NoSuchPaddingException;
     69 
     70 import libcore.io.ErrnoException;
     71 import libcore.io.IoUtils;
     72 import libcore.io.Libcore;
     73 import libcore.io.Streams;
     74 import libcore.io.StructStatVfs;
     75 
     76 /*
     77  * This service copies a downloaded apk to a file passed in as
     78  * a ParcelFileDescriptor or to a newly created container specified
     79  * by parameters. The DownloadManager gives access to this process
     80  * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
     81  * permission to access apks downloaded via the download manager.
     82  */
     83 public class DefaultContainerService extends IntentService {
     84     private static final String TAG = "DefContainer";
     85     private static final boolean localLOGV = false;
     86 
     87     private static final String LIB_DIR_NAME = "lib";
     88 
     89     private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
     90         /**
     91          * Creates a new container and copies resource there.
     92          * @param paackageURI the uri of resource to be copied. Can be either
     93          * a content uri or a file uri
     94          * @param cid the id of the secure container that should
     95          * be used for creating a secure container into which the resource
     96          * will be copied.
     97          * @param key Refers to key used for encrypting the secure container
     98          * @param resFileName Name of the target resource file(relative to newly
     99          * created secure container)
    100          * @return Returns the new cache path where the resource has been copied into
    101          *
    102          */
    103         public String copyResourceToContainer(final Uri packageURI, final String cid,
    104                 final String key, final String resFileName, final String publicResFileName,
    105                 boolean isExternal, boolean isForwardLocked) {
    106             if (packageURI == null || cid == null) {
    107                 return null;
    108             }
    109 
    110             return copyResourceInner(packageURI, cid, key, resFileName, publicResFileName,
    111                     isExternal, isForwardLocked);
    112         }
    113 
    114         /**
    115          * Copy specified resource to output stream
    116          *
    117          * @param packageURI the uri of resource to be copied. Should be a file
    118          *            uri
    119          * @param encryptionParams parameters describing the encryption used for
    120          *            this file
    121          * @param outStream Remote file descriptor to be used for copying
    122          * @return returns status code according to those in
    123          *         {@link PackageManager}
    124          */
    125         public int copyResource(final Uri packageURI, ContainerEncryptionParams encryptionParams,
    126                 ParcelFileDescriptor outStream) {
    127             if (packageURI == null || outStream == null) {
    128                 return PackageManager.INSTALL_FAILED_INVALID_URI;
    129             }
    130 
    131             ParcelFileDescriptor.AutoCloseOutputStream autoOut
    132                     = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
    133 
    134             try {
    135                 copyFile(packageURI, autoOut, encryptionParams);
    136                 return PackageManager.INSTALL_SUCCEEDED;
    137             } catch (FileNotFoundException e) {
    138                 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: "
    139                         + e.getMessage());
    140                 return PackageManager.INSTALL_FAILED_INVALID_URI;
    141             } catch (IOException e) {
    142                 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: "
    143                         + e.getMessage());
    144                 return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
    145             } catch (DigestException e) {
    146                 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " Security: "
    147                                 + e.getMessage());
    148                 return PackageManager.INSTALL_FAILED_INVALID_APK;
    149             } finally {
    150                 IoUtils.closeQuietly(autoOut);
    151             }
    152         }
    153 
    154         /**
    155          * Determine the recommended install location for package
    156          * specified by file uri location.
    157          * @param fileUri the uri of resource to be copied. Should be a
    158          * file uri
    159          * @return Returns PackageInfoLite object containing
    160          * the package info and recommended app location.
    161          */
    162         public PackageInfoLite getMinimalPackageInfo(final String packagePath, int flags,
    163                 long threshold) {
    164             PackageInfoLite ret = new PackageInfoLite();
    165 
    166             if (packagePath == null) {
    167                 Slog.i(TAG, "Invalid package file " + packagePath);
    168                 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
    169                 return ret;
    170             }
    171 
    172             DisplayMetrics metrics = new DisplayMetrics();
    173             metrics.setToDefaults();
    174 
    175             PackageParser.PackageLite pkg = PackageParser.parsePackageLite(packagePath, 0);
    176             if (pkg == null) {
    177                 Slog.w(TAG, "Failed to parse package");
    178 
    179                 final File apkFile = new File(packagePath);
    180                 if (!apkFile.exists()) {
    181                     ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
    182                 } else {
    183                     ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
    184                 }
    185 
    186                 return ret;
    187             }
    188 
    189             ret.packageName = pkg.packageName;
    190             ret.versionCode = pkg.versionCode;
    191             ret.installLocation = pkg.installLocation;
    192             ret.verifiers = pkg.verifiers;
    193 
    194             ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation,
    195                     packagePath, flags, threshold);
    196 
    197             return ret;
    198         }
    199 
    200         @Override
    201         public boolean checkInternalFreeStorage(Uri packageUri, boolean isForwardLocked,
    202                 long threshold) throws RemoteException {
    203             final File apkFile = new File(packageUri.getPath());
    204             try {
    205                 return isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
    206             } catch (IOException e) {
    207                 return true;
    208             }
    209         }
    210 
    211         @Override
    212         public boolean checkExternalFreeStorage(Uri packageUri, boolean isForwardLocked)
    213                 throws RemoteException {
    214             final File apkFile = new File(packageUri.getPath());
    215             try {
    216                 return isUnderExternalThreshold(apkFile, isForwardLocked);
    217             } catch (IOException e) {
    218                 return true;
    219             }
    220         }
    221 
    222         public ObbInfo getObbInfo(String filename) {
    223             try {
    224                 return ObbScanner.getObbInfo(filename);
    225             } catch (IOException e) {
    226                 Slog.d(TAG, "Couldn't get OBB info for " + filename);
    227                 return null;
    228             }
    229         }
    230 
    231         @Override
    232         public long calculateDirectorySize(String path) throws RemoteException {
    233             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    234 
    235             final File dir = Environment.maybeTranslateEmulatedPathToInternal(new File(path));
    236             if (dir.exists() && dir.isDirectory()) {
    237                 final String targetPath = dir.getAbsolutePath();
    238                 return MeasurementUtils.measureDirectory(targetPath);
    239             } else {
    240                 return 0L;
    241             }
    242         }
    243 
    244         @Override
    245         public long[] getFileSystemStats(String path) {
    246             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    247 
    248             try {
    249                 final StructStatVfs stat = Libcore.os.statvfs(path);
    250                 final long totalSize = stat.f_blocks * stat.f_bsize;
    251                 final long availSize = stat.f_bavail * stat.f_bsize;
    252                 return new long[] { totalSize, availSize };
    253             } catch (ErrnoException e) {
    254                 throw new IllegalStateException(e);
    255             }
    256         }
    257 
    258         @Override
    259         public void clearDirectory(String path) throws RemoteException {
    260             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    261 
    262             final File directory = new File(path);
    263             if (directory.exists() && directory.isDirectory()) {
    264                 eraseFiles(directory);
    265             }
    266         }
    267 
    268         @Override
    269         public long calculateInstalledSize(String packagePath, boolean isForwardLocked)
    270                 throws RemoteException {
    271             final File packageFile = new File(packagePath);
    272             try {
    273                 return calculateContainerSize(packageFile, isForwardLocked) * 1024 * 1024;
    274             } catch (IOException e) {
    275                 /*
    276                  * Okay, something failed, so let's just estimate it to be 2x
    277                  * the file size. Note this will be 0 if the file doesn't exist.
    278                  */
    279                 return packageFile.length() * 2;
    280             }
    281         }
    282     };
    283 
    284     public DefaultContainerService() {
    285         super("DefaultContainerService");
    286         setIntentRedelivery(true);
    287     }
    288 
    289     @Override
    290     protected void onHandleIntent(Intent intent) {
    291         if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
    292             final IPackageManager pm = IPackageManager.Stub.asInterface(
    293                     ServiceManager.getService("package"));
    294             PackageCleanItem item = null;
    295             try {
    296                 while ((item = pm.nextPackageToClean(item)) != null) {
    297                     final UserEnvironment userEnv = new UserEnvironment(item.userId);
    298                     eraseFiles(userEnv.buildExternalStorageAppDataDirs(item.packageName));
    299                     eraseFiles(userEnv.buildExternalStorageAppMediaDirs(item.packageName));
    300                     if (item.andCode) {
    301                         eraseFiles(userEnv.buildExternalStorageAppObbDirs(item.packageName));
    302                     }
    303                 }
    304             } catch (RemoteException e) {
    305             }
    306         }
    307     }
    308 
    309     void eraseFiles(File[] paths) {
    310         for (File path : paths) {
    311             eraseFiles(path);
    312         }
    313     }
    314 
    315     void eraseFiles(File path) {
    316         if (path.isDirectory()) {
    317             String[] files = path.list();
    318             if (files != null) {
    319                 for (String file : files) {
    320                     eraseFiles(new File(path, file));
    321                 }
    322             }
    323         }
    324         path.delete();
    325     }
    326 
    327     public IBinder onBind(Intent intent) {
    328         return mBinder;
    329     }
    330 
    331     private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName,
    332             String publicResFileName, boolean isExternal, boolean isForwardLocked) {
    333 
    334         if (isExternal) {
    335             // Make sure the sdcard is mounted.
    336             String status = Environment.getExternalStorageState();
    337             if (!status.equals(Environment.MEDIA_MOUNTED)) {
    338                 Slog.w(TAG, "Make sure sdcard is mounted.");
    339                 return null;
    340             }
    341         }
    342 
    343         // The .apk file
    344         String codePath = packageURI.getPath();
    345         File codeFile = new File(codePath);
    346 
    347         // Calculate size of container needed to hold base APK.
    348         final int sizeMb;
    349         try {
    350             sizeMb = calculateContainerSize(codeFile, isForwardLocked);
    351         } catch (IOException e) {
    352             Slog.w(TAG, "Problem when trying to copy " + codeFile.getPath());
    353             return null;
    354         }
    355 
    356         // Create new container
    357         final String newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid(),
    358                 isExternal);
    359         if (newCachePath == null) {
    360             Slog.e(TAG, "Failed to create container " + newCid);
    361             return null;
    362         }
    363 
    364         if (localLOGV) {
    365             Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
    366         }
    367 
    368         final File resFile = new File(newCachePath, resFileName);
    369         if (FileUtils.copyFile(new File(codePath), resFile)) {
    370             if (localLOGV) {
    371                 Slog.i(TAG, "Copied " + codePath + " to " + resFile);
    372             }
    373         } else {
    374             Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile);
    375             // Clean up container
    376             PackageHelper.destroySdDir(newCid);
    377             return null;
    378         }
    379 
    380         try {
    381             Libcore.os.chmod(resFile.getAbsolutePath(), 0640);
    382         } catch (ErrnoException e) {
    383             Slog.e(TAG, "Could not chown APK: " + e.getMessage());
    384             PackageHelper.destroySdDir(newCid);
    385             return null;
    386         }
    387 
    388         if (isForwardLocked) {
    389             File publicZipFile = new File(newCachePath, publicResFileName);
    390             try {
    391                 PackageHelper.extractPublicFiles(resFile.getAbsolutePath(), publicZipFile);
    392                 if (localLOGV) {
    393                     Slog.i(TAG, "Copied resources to " + publicZipFile);
    394                 }
    395             } catch (IOException e) {
    396                 Slog.e(TAG, "Could not chown public APK " + publicZipFile.getAbsolutePath() + ": "
    397                         + e.getMessage());
    398                 PackageHelper.destroySdDir(newCid);
    399                 return null;
    400             }
    401 
    402             try {
    403                 Libcore.os.chmod(publicZipFile.getAbsolutePath(), 0644);
    404             } catch (ErrnoException e) {
    405                 Slog.e(TAG, "Could not chown public resource file: " + e.getMessage());
    406                 PackageHelper.destroySdDir(newCid);
    407                 return null;
    408             }
    409         }
    410 
    411         final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
    412         if (sharedLibraryDir.mkdir()) {
    413             int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir);
    414             if (ret != PackageManager.INSTALL_SUCCEEDED) {
    415                 Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath());
    416                 PackageHelper.destroySdDir(newCid);
    417                 return null;
    418             }
    419         } else {
    420             Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath());
    421             PackageHelper.destroySdDir(newCid);
    422             return null;
    423         }
    424 
    425         if (!PackageHelper.finalizeSdDir(newCid)) {
    426             Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
    427             // Clean up container
    428             PackageHelper.destroySdDir(newCid);
    429             return null;
    430         }
    431 
    432         if (localLOGV) {
    433             Slog.i(TAG, "Finalized container " + newCid);
    434         }
    435 
    436         if (PackageHelper.isContainerMounted(newCid)) {
    437             if (localLOGV) {
    438                 Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath);
    439             }
    440 
    441             // Force a gc to avoid being killed.
    442             Runtime.getRuntime().gc();
    443             PackageHelper.unMountSdDir(newCid);
    444         } else {
    445             if (localLOGV) {
    446                 Slog.i(TAG, "Container " + newCid + " not mounted");
    447             }
    448         }
    449 
    450         return newCachePath;
    451     }
    452 
    453     private static void copyToFile(InputStream inputStream, OutputStream out) throws IOException {
    454         byte[] buffer = new byte[16384];
    455         int bytesRead;
    456         while ((bytesRead = inputStream.read(buffer)) >= 0) {
    457             out.write(buffer, 0, bytesRead);
    458         }
    459     }
    460 
    461     private void copyFile(Uri pPackageURI, OutputStream outStream,
    462             ContainerEncryptionParams encryptionParams) throws FileNotFoundException, IOException,
    463             DigestException {
    464         String scheme = pPackageURI.getScheme();
    465         InputStream inStream = null;
    466         try {
    467             if (scheme == null || scheme.equals("file")) {
    468                 final InputStream is = new FileInputStream(new File(pPackageURI.getPath()));
    469                 inStream = new BufferedInputStream(is);
    470             } else if (scheme.equals("content")) {
    471                 final ParcelFileDescriptor fd;
    472                 try {
    473                     fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
    474                 } catch (FileNotFoundException e) {
    475                     Slog.e(TAG, "Couldn't open file descriptor from download service. "
    476                             + "Failed with exception " + e);
    477                     throw e;
    478                 }
    479 
    480                 if (fd == null) {
    481                     Slog.e(TAG, "Provider returned no file descriptor for " +
    482                             pPackageURI.toString());
    483                     throw new FileNotFoundException("provider returned no file descriptor");
    484                 } else {
    485                     if (localLOGV) {
    486                         Slog.i(TAG, "Opened file descriptor from download service.");
    487                     }
    488                     inStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
    489                 }
    490             } else {
    491                 Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
    492                 throw new FileNotFoundException("Package URI is not 'file:' or 'content:'");
    493             }
    494 
    495             /*
    496              * If this resource is encrypted, get the decrypted stream version
    497              * of it.
    498              */
    499             ApkContainer container = new ApkContainer(inStream, encryptionParams);
    500 
    501             try {
    502                 /*
    503                  * We copy the source package file to a temp file and then
    504                  * rename it to the destination file in order to eliminate a
    505                  * window where the package directory scanner notices the new
    506                  * package file but it's not completely copied yet.
    507                  */
    508                 copyToFile(container.getInputStream(), outStream);
    509 
    510                 if (!container.isAuthenticated()) {
    511                     throw new DigestException();
    512                 }
    513             } catch (GeneralSecurityException e) {
    514                 throw new DigestException("A problem occured copying the file.");
    515             }
    516         } finally {
    517             IoUtils.closeQuietly(inStream);
    518         }
    519     }
    520 
    521     private static class ApkContainer {
    522         private static final int MAX_AUTHENTICATED_DATA_SIZE = 16384;
    523 
    524         private final InputStream mInStream;
    525 
    526         private MacAuthenticatedInputStream mAuthenticatedStream;
    527 
    528         private byte[] mTag;
    529 
    530         public ApkContainer(InputStream inStream, ContainerEncryptionParams encryptionParams)
    531                 throws IOException {
    532             if (encryptionParams == null) {
    533                 mInStream = inStream;
    534             } else {
    535                 mInStream = getDecryptedStream(inStream, encryptionParams);
    536                 mTag = encryptionParams.getMacTag();
    537             }
    538         }
    539 
    540         public boolean isAuthenticated() {
    541             if (mAuthenticatedStream == null) {
    542                 return true;
    543             }
    544 
    545             return mAuthenticatedStream.isTagEqual(mTag);
    546         }
    547 
    548         private Mac getMacInstance(ContainerEncryptionParams encryptionParams) throws IOException {
    549             final Mac m;
    550             try {
    551                 final String macAlgo = encryptionParams.getMacAlgorithm();
    552 
    553                 if (macAlgo != null) {
    554                     m = Mac.getInstance(macAlgo);
    555                     m.init(encryptionParams.getMacKey(), encryptionParams.getMacSpec());
    556                 } else {
    557                     m = null;
    558                 }
    559 
    560                 return m;
    561             } catch (NoSuchAlgorithmException e) {
    562                 throw new IOException(e);
    563             } catch (InvalidKeyException e) {
    564                 throw new IOException(e);
    565             } catch (InvalidAlgorithmParameterException e) {
    566                 throw new IOException(e);
    567             }
    568         }
    569 
    570         public InputStream getInputStream() {
    571             return mInStream;
    572         }
    573 
    574         private InputStream getDecryptedStream(InputStream inStream,
    575                 ContainerEncryptionParams encryptionParams) throws IOException {
    576             final Cipher c;
    577             try {
    578                 c = Cipher.getInstance(encryptionParams.getEncryptionAlgorithm());
    579                 c.init(Cipher.DECRYPT_MODE, encryptionParams.getEncryptionKey(),
    580                         encryptionParams.getEncryptionSpec());
    581             } catch (NoSuchAlgorithmException e) {
    582                 throw new IOException(e);
    583             } catch (NoSuchPaddingException e) {
    584                 throw new IOException(e);
    585             } catch (InvalidKeyException e) {
    586                 throw new IOException(e);
    587             } catch (InvalidAlgorithmParameterException e) {
    588                 throw new IOException(e);
    589             }
    590 
    591             final long encStart = encryptionParams.getEncryptedDataStart();
    592             final long end = encryptionParams.getDataEnd();
    593             if (end < encStart) {
    594                 throw new IOException("end <= encStart");
    595             }
    596 
    597             final Mac mac = getMacInstance(encryptionParams);
    598             if (mac != null) {
    599                 final long macStart = encryptionParams.getAuthenticatedDataStart();
    600                 if (macStart >= Integer.MAX_VALUE) {
    601                     throw new IOException("macStart >= Integer.MAX_VALUE");
    602                 }
    603 
    604                 final long furtherOffset;
    605                 if (macStart >= 0 && encStart >= 0 && macStart < encStart) {
    606                     /*
    607                      * If there is authenticated data at the beginning, read
    608                      * that into our MAC first.
    609                      */
    610                     final long authenticatedLengthLong = encStart - macStart;
    611                     if (authenticatedLengthLong > MAX_AUTHENTICATED_DATA_SIZE) {
    612                         throw new IOException("authenticated data is too long");
    613                     }
    614                     final int authenticatedLength = (int) authenticatedLengthLong;
    615 
    616                     final byte[] authenticatedData = new byte[(int) authenticatedLength];
    617 
    618                     Streams.readFully(inStream, authenticatedData, (int) macStart,
    619                             authenticatedLength);
    620                     mac.update(authenticatedData, 0, authenticatedLength);
    621 
    622                     furtherOffset = 0;
    623                 } else {
    624                     /*
    625                      * No authenticated data at the beginning. Just skip the
    626                      * required number of bytes to the beginning of the stream.
    627                      */
    628                     if (encStart > 0) {
    629                         furtherOffset = encStart;
    630                     } else {
    631                         furtherOffset = 0;
    632                     }
    633                 }
    634 
    635                 /*
    636                  * If there is data at the end of the stream we want to ignore,
    637                  * wrap this in a LimitedLengthInputStream.
    638                  */
    639                 if (furtherOffset >= 0 && end > furtherOffset) {
    640                     inStream = new LimitedLengthInputStream(inStream, furtherOffset, end - encStart);
    641                 } else if (furtherOffset > 0) {
    642                     inStream.skip(furtherOffset);
    643                 }
    644 
    645                 mAuthenticatedStream = new MacAuthenticatedInputStream(inStream, mac);
    646 
    647                 inStream = mAuthenticatedStream;
    648             } else {
    649                 if (encStart >= 0) {
    650                     if (end > encStart) {
    651                         inStream = new LimitedLengthInputStream(inStream, encStart, end - encStart);
    652                     } else {
    653                         inStream.skip(encStart);
    654                     }
    655                 }
    656             }
    657 
    658             return new CipherInputStream(inStream, c);
    659         }
    660 
    661     }
    662 
    663     private static final int PREFER_INTERNAL = 1;
    664     private static final int PREFER_EXTERNAL = 2;
    665 
    666     private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
    667             long threshold) {
    668         int prefer;
    669         boolean checkBoth = false;
    670 
    671         final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
    672 
    673         check_inner : {
    674             /*
    675              * Explicit install flags should override the manifest settings.
    676              */
    677             if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
    678                 prefer = PREFER_INTERNAL;
    679                 break check_inner;
    680             } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
    681                 prefer = PREFER_EXTERNAL;
    682                 break check_inner;
    683             }
    684 
    685             /* No install flags. Check for manifest option. */
    686             if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
    687                 prefer = PREFER_INTERNAL;
    688                 break check_inner;
    689             } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
    690                 prefer = PREFER_EXTERNAL;
    691                 checkBoth = true;
    692                 break check_inner;
    693             } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
    694                 // We default to preferring internal storage.
    695                 prefer = PREFER_INTERNAL;
    696                 checkBoth = true;
    697                 break check_inner;
    698             }
    699 
    700             // Pick user preference
    701             int installPreference = Settings.Global.getInt(getApplicationContext()
    702                     .getContentResolver(),
    703                     Settings.Global.DEFAULT_INSTALL_LOCATION,
    704                     PackageHelper.APP_INSTALL_AUTO);
    705             if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
    706                 prefer = PREFER_INTERNAL;
    707                 break check_inner;
    708             } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
    709                 prefer = PREFER_EXTERNAL;
    710                 break check_inner;
    711             }
    712 
    713             /*
    714              * Fall back to default policy of internal-only if nothing else is
    715              * specified.
    716              */
    717             prefer = PREFER_INTERNAL;
    718         }
    719 
    720         final boolean emulated = Environment.isExternalStorageEmulated();
    721 
    722         final File apkFile = new File(archiveFilePath);
    723 
    724         boolean fitsOnInternal = false;
    725         if (checkBoth || prefer == PREFER_INTERNAL) {
    726             try {
    727                 fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
    728             } catch (IOException e) {
    729                 return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
    730             }
    731         }
    732 
    733         boolean fitsOnSd = false;
    734         if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
    735             try {
    736                 fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked);
    737             } catch (IOException e) {
    738                 return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
    739             }
    740         }
    741 
    742         if (prefer == PREFER_INTERNAL) {
    743             if (fitsOnInternal) {
    744                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    745             }
    746         } else if (!emulated && prefer == PREFER_EXTERNAL) {
    747             if (fitsOnSd) {
    748                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    749             }
    750         }
    751 
    752         if (checkBoth) {
    753             if (fitsOnInternal) {
    754                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    755             } else if (!emulated && fitsOnSd) {
    756                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    757             }
    758         }
    759 
    760         /*
    761          * If they requested to be on the external media by default, return that
    762          * the media was unavailable. Otherwise, indicate there was insufficient
    763          * storage space available.
    764          */
    765         if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
    766                 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
    767             return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
    768         } else {
    769             return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
    770         }
    771     }
    772 
    773     /**
    774      * Measure a file to see if it fits within the free space threshold.
    775      *
    776      * @param apkFile file to check
    777      * @param threshold byte threshold to compare against
    778      * @return true if file fits under threshold
    779      * @throws FileNotFoundException when APK does not exist
    780      */
    781     private boolean isUnderInternalThreshold(File apkFile, boolean isForwardLocked, long threshold)
    782             throws IOException {
    783         long size = apkFile.length();
    784         if (size == 0 && !apkFile.exists()) {
    785             throw new FileNotFoundException();
    786         }
    787 
    788         if (isForwardLocked) {
    789             size += PackageHelper.extractPublicFiles(apkFile.getAbsolutePath(), null);
    790         }
    791 
    792         final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
    793         final long availInternalSize = (long) internalStats.getAvailableBlocks()
    794                 * (long) internalStats.getBlockSize();
    795 
    796         return (availInternalSize - size) > threshold;
    797     }
    798 
    799 
    800     /**
    801      * Measure a file to see if it fits in the external free space.
    802      *
    803      * @param apkFile file to check
    804      * @return true if file fits
    805      * @throws IOException when file does not exist
    806      */
    807     private boolean isUnderExternalThreshold(File apkFile, boolean isForwardLocked)
    808             throws IOException {
    809         if (Environment.isExternalStorageEmulated()) {
    810             return false;
    811         }
    812 
    813         final int sizeMb = calculateContainerSize(apkFile, isForwardLocked);
    814 
    815         final int availSdMb;
    816         if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
    817             final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
    818             final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
    819             availSdMb = sdStats.getAvailableBlocks() * blocksToMb;
    820         } else {
    821             availSdMb = -1;
    822         }
    823 
    824         return availSdMb > sizeMb;
    825     }
    826 
    827     /**
    828      * Calculate the container size for an APK. Takes into account the
    829      *
    830      * @param apkFile file from which to calculate size
    831      * @return size in megabytes (2^20 bytes)
    832      * @throws IOException when there is a problem reading the file
    833      */
    834     private int calculateContainerSize(File apkFile, boolean forwardLocked) throws IOException {
    835         // Calculate size of container needed to hold base APK.
    836         long sizeBytes = apkFile.length();
    837         if (sizeBytes == 0 && !apkFile.exists()) {
    838             throw new FileNotFoundException();
    839         }
    840 
    841         // Check all the native files that need to be copied and add that to the
    842         // container size.
    843         sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile);
    844 
    845         if (forwardLocked) {
    846             sizeBytes += PackageHelper.extractPublicFiles(apkFile.getPath(), null);
    847         }
    848 
    849         int sizeMb = (int) (sizeBytes >> 20);
    850         if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) {
    851             sizeMb++;
    852         }
    853 
    854         /*
    855          * Add buffer size because we don't have a good way to determine the
    856          * real FAT size. Your FAT size varies with how many directory entries
    857          * you need, how big the whole filesystem is, and other such headaches.
    858          */
    859         sizeMb++;
    860 
    861         return sizeMb;
    862     }
    863 }
    864