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 com.android.internal.app.IMediaContainerService;
     20 import com.android.internal.content.NativeLibraryHelper;
     21 import com.android.internal.content.PackageHelper;
     22 
     23 import android.app.IntentService;
     24 import android.content.Intent;
     25 import android.content.pm.IPackageManager;
     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.FileUtils;
     35 import android.os.IBinder;
     36 import android.os.ParcelFileDescriptor;
     37 import android.os.Process;
     38 import android.os.RemoteException;
     39 import android.os.ServiceManager;
     40 import android.os.StatFs;
     41 import android.provider.Settings;
     42 import android.util.DisplayMetrics;
     43 import android.util.Slog;
     44 
     45 import java.io.BufferedInputStream;
     46 import java.io.File;
     47 import java.io.FileInputStream;
     48 import java.io.FileNotFoundException;
     49 import java.io.IOException;
     50 import java.io.InputStream;
     51 import java.io.OutputStream;
     52 
     53 /*
     54  * This service copies a downloaded apk to a file passed in as
     55  * a ParcelFileDescriptor or to a newly created container specified
     56  * by parameters. The DownloadManager gives access to this process
     57  * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
     58  * permission to access apks downloaded via the download manager.
     59  */
     60 public class DefaultContainerService extends IntentService {
     61     private static final String TAG = "DefContainer";
     62     private static final boolean localLOGV = true;
     63 
     64     private static final String LIB_DIR_NAME = "lib";
     65 
     66     private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
     67         /*
     68          * Creates a new container and copies resource there.
     69          * @param paackageURI the uri of resource to be copied. Can be either
     70          * a content uri or a file uri
     71          * @param cid the id of the secure container that should
     72          * be used for creating a secure container into which the resource
     73          * will be copied.
     74          * @param key Refers to key used for encrypting the secure container
     75          * @param resFileName Name of the target resource file(relative to newly
     76          * created secure container)
     77          * @return Returns the new cache path where the resource has been copied into
     78          *
     79          */
     80         public String copyResourceToContainer(final Uri packageURI,
     81                 final String cid,
     82                 final String key, final String resFileName) {
     83             if (packageURI == null || cid == null) {
     84                 return null;
     85             }
     86             return copyResourceInner(packageURI, cid, key, resFileName);
     87         }
     88 
     89         /*
     90          * Copy specified resource to output stream
     91          * @param packageURI the uri of resource to be copied. Should be a file
     92          * uri
     93          * @param outStream Remote file descriptor to be used for copying
     94          * @return returns status code according to those in {@link
     95          * PackageManager}
     96          */
     97         public int copyResource(final Uri packageURI, ParcelFileDescriptor outStream) {
     98             if (packageURI == null || outStream == null) {
     99                 return PackageManager.INSTALL_FAILED_INVALID_URI;
    100             }
    101 
    102             ParcelFileDescriptor.AutoCloseOutputStream autoOut
    103                     = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
    104 
    105             try {
    106                 copyFile(packageURI, autoOut);
    107                 return PackageManager.INSTALL_SUCCEEDED;
    108             } catch (FileNotFoundException e) {
    109                 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: "
    110                         + e.getMessage());
    111                 return PackageManager.INSTALL_FAILED_INVALID_URI;
    112             } catch (IOException e) {
    113                 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: "
    114                         + e.getMessage());
    115                 return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
    116             }
    117         }
    118 
    119         /*
    120          * Determine the recommended install location for package
    121          * specified by file uri location.
    122          * @param fileUri the uri of resource to be copied. Should be a
    123          * file uri
    124          * @return Returns PackageInfoLite object containing
    125          * the package info and recommended app location.
    126          */
    127         public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags, long threshold) {
    128             PackageInfoLite ret = new PackageInfoLite();
    129             if (fileUri == null) {
    130                 Slog.i(TAG, "Invalid package uri " + fileUri);
    131                 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
    132                 return ret;
    133             }
    134             String scheme = fileUri.getScheme();
    135             if (scheme != null && !scheme.equals("file")) {
    136                 Slog.w(TAG, "Falling back to installing on internal storage only");
    137                 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    138                 return ret;
    139             }
    140             String archiveFilePath = fileUri.getPath();
    141             DisplayMetrics metrics = new DisplayMetrics();
    142             metrics.setToDefaults();
    143 
    144             PackageParser.PackageLite pkg = PackageParser.parsePackageLite(archiveFilePath, 0);
    145             if (pkg == null) {
    146                 Slog.w(TAG, "Failed to parse package");
    147 
    148                 final File apkFile = new File(archiveFilePath);
    149                 if (!apkFile.exists()) {
    150                     ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
    151                 } else {
    152                     ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
    153                 }
    154 
    155                 return ret;
    156             }
    157             ret.packageName = pkg.packageName;
    158             ret.installLocation = pkg.installLocation;
    159             ret.verifiers = pkg.verifiers;
    160 
    161             ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation,
    162                     archiveFilePath, flags, threshold);
    163 
    164             return ret;
    165         }
    166 
    167         @Override
    168         public boolean checkInternalFreeStorage(Uri packageUri, long threshold)
    169                 throws RemoteException {
    170             final File apkFile = new File(packageUri.getPath());
    171             try {
    172                 return isUnderInternalThreshold(apkFile, threshold);
    173             } catch (FileNotFoundException e) {
    174                 return true;
    175             }
    176         }
    177 
    178         @Override
    179         public boolean checkExternalFreeStorage(Uri packageUri) throws RemoteException {
    180             final File apkFile = new File(packageUri.getPath());
    181             try {
    182                 return isUnderExternalThreshold(apkFile);
    183             } catch (FileNotFoundException e) {
    184                 return true;
    185             }
    186         }
    187 
    188         public ObbInfo getObbInfo(String filename) {
    189             try {
    190                 return ObbScanner.getObbInfo(filename);
    191             } catch (IOException e) {
    192                 Slog.d(TAG, "Couldn't get OBB info for " + filename);
    193                 return null;
    194             }
    195         }
    196 
    197         @Override
    198         public long calculateDirectorySize(String path) throws RemoteException {
    199             final File directory = new File(path);
    200             if (directory.exists() && directory.isDirectory()) {
    201                 return MeasurementUtils.measureDirectory(path);
    202             } else {
    203                 return 0L;
    204             }
    205         }
    206     };
    207 
    208     public DefaultContainerService() {
    209         super("DefaultContainerService");
    210         setIntentRedelivery(true);
    211     }
    212 
    213     @Override
    214     protected void onHandleIntent(Intent intent) {
    215         if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
    216             IPackageManager pm = IPackageManager.Stub.asInterface(
    217                     ServiceManager.getService("package"));
    218             String pkg = null;
    219             try {
    220                 while ((pkg=pm.nextPackageToClean(pkg)) != null) {
    221                     eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
    222                     eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
    223                     eraseFiles(Environment.getExternalStorageAppObbDirectory(pkg));
    224                 }
    225             } catch (RemoteException e) {
    226             }
    227         }
    228     }
    229 
    230     void eraseFiles(File path) {
    231         if (path.isDirectory()) {
    232             String[] files = path.list();
    233             if (files != null) {
    234                 for (String file : files) {
    235                     eraseFiles(new File(path, file));
    236                 }
    237             }
    238         }
    239         path.delete();
    240     }
    241 
    242     public IBinder onBind(Intent intent) {
    243         return mBinder;
    244     }
    245 
    246     private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) {
    247         // Make sure the sdcard is mounted.
    248         String status = Environment.getExternalStorageState();
    249         if (!status.equals(Environment.MEDIA_MOUNTED)) {
    250             Slog.w(TAG, "Make sure sdcard is mounted.");
    251             return null;
    252         }
    253 
    254         // The .apk file
    255         String codePath = packageURI.getPath();
    256         File codeFile = new File(codePath);
    257 
    258         // Calculate size of container needed to hold base APK.
    259         int sizeMb;
    260         try {
    261             sizeMb = calculateContainerSize(codeFile);
    262         } catch (FileNotFoundException e) {
    263             Slog.w(TAG, "File does not exist when trying to copy " + codeFile.getPath());
    264             return null;
    265         }
    266 
    267         // Create new container
    268         final String newCachePath;
    269         if ((newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid())) == null) {
    270             Slog.e(TAG, "Failed to create container " + newCid);
    271             return null;
    272         }
    273 
    274         if (localLOGV) {
    275             Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
    276         }
    277 
    278         final File resFile = new File(newCachePath, resFileName);
    279         if (FileUtils.copyFile(new File(codePath), resFile)) {
    280             if (localLOGV) {
    281                 Slog.i(TAG, "Copied " + codePath + " to " + resFile);
    282             }
    283         } else {
    284             Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile);
    285             // Clean up container
    286             PackageHelper.destroySdDir(newCid);
    287             return null;
    288         }
    289 
    290         final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
    291         if (sharedLibraryDir.mkdir()) {
    292             int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir);
    293             if (ret != PackageManager.INSTALL_SUCCEEDED) {
    294                 Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath());
    295                 PackageHelper.destroySdDir(newCid);
    296                 return null;
    297             }
    298         } else {
    299             Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath());
    300             PackageHelper.destroySdDir(newCid);
    301             return null;
    302         }
    303 
    304         if (!PackageHelper.finalizeSdDir(newCid)) {
    305             Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
    306             // Clean up container
    307             PackageHelper.destroySdDir(newCid);
    308             return null;
    309         }
    310 
    311         if (localLOGV) {
    312             Slog.i(TAG, "Finalized container " + newCid);
    313         }
    314 
    315         if (PackageHelper.isContainerMounted(newCid)) {
    316             if (localLOGV) {
    317                 Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath);
    318             }
    319 
    320             // Force a gc to avoid being killed.
    321             Runtime.getRuntime().gc();
    322             PackageHelper.unMountSdDir(newCid);
    323         } else {
    324             if (localLOGV) {
    325                 Slog.i(TAG, "Container " + newCid + " not mounted");
    326             }
    327         }
    328 
    329         return newCachePath;
    330     }
    331 
    332     private static void copyToFile(InputStream inputStream, OutputStream out) throws IOException {
    333         byte[] buffer = new byte[16384];
    334         int bytesRead;
    335         while ((bytesRead = inputStream.read(buffer)) >= 0) {
    336             out.write(buffer, 0, bytesRead);
    337         }
    338     }
    339 
    340     private static void copyToFile(File srcFile, OutputStream out)
    341             throws FileNotFoundException, IOException {
    342         InputStream inputStream = new BufferedInputStream(new FileInputStream(srcFile));
    343         try {
    344             copyToFile(inputStream, out);
    345         } finally {
    346             try { inputStream.close(); } catch (IOException e) {}
    347         }
    348     }
    349 
    350     private void copyFile(Uri pPackageURI, OutputStream outStream) throws FileNotFoundException,
    351             IOException {
    352         String scheme = pPackageURI.getScheme();
    353         if (scheme == null || scheme.equals("file")) {
    354             final File srcPackageFile = new File(pPackageURI.getPath());
    355             // We copy the source package file to a temp file and then rename it to the
    356             // destination file in order to eliminate a window where the package directory
    357             // scanner notices the new package file but it's not completely copied yet.
    358             copyToFile(srcPackageFile, outStream);
    359         } else if (scheme.equals("content")) {
    360             ParcelFileDescriptor fd = null;
    361             try {
    362                 fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
    363             } catch (FileNotFoundException e) {
    364                 Slog.e(TAG, "Couldn't open file descriptor from download service. "
    365                         + "Failed with exception " + e);
    366                 throw e;
    367             }
    368 
    369             if (fd == null) {
    370                 Slog.e(TAG, "Provider returned no file descriptor for " + pPackageURI.toString());
    371                 throw new FileNotFoundException("provider returned no file descriptor");
    372             } else {
    373                 if (localLOGV) {
    374                     Slog.i(TAG, "Opened file descriptor from download service.");
    375                 }
    376                 ParcelFileDescriptor.AutoCloseInputStream dlStream
    377                         = new ParcelFileDescriptor.AutoCloseInputStream(fd);
    378 
    379                 // We copy the source package file to a temp file and then rename it to the
    380                 // destination file in order to eliminate a window where the package directory
    381                 // scanner notices the new package file but it's not completely
    382                 // copied
    383                 copyToFile(dlStream, outStream);
    384             }
    385         } else {
    386             Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
    387             throw new FileNotFoundException("Package URI is not 'file:' or 'content:'");
    388         }
    389     }
    390 
    391     private static final int PREFER_INTERNAL = 1;
    392     private static final int PREFER_EXTERNAL = 2;
    393 
    394     private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
    395             long threshold) {
    396         int prefer;
    397         boolean checkBoth = false;
    398 
    399         check_inner : {
    400             /*
    401              * Explicit install flags should override the manifest settings.
    402              */
    403             if ((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) {
    404                 /*
    405                  * Forward-locked applications cannot be installed on SD card,
    406                  * so only allow checking internal storage.
    407                  */
    408                 prefer = PREFER_INTERNAL;
    409                 break check_inner;
    410             } else if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
    411                 prefer = PREFER_INTERNAL;
    412                 break check_inner;
    413             } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
    414                 prefer = PREFER_EXTERNAL;
    415                 break check_inner;
    416             }
    417 
    418             /* No install flags. Check for manifest option. */
    419             if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
    420                 prefer = PREFER_INTERNAL;
    421                 break check_inner;
    422             } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
    423                 prefer = PREFER_EXTERNAL;
    424                 checkBoth = true;
    425                 break check_inner;
    426             } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
    427                 // We default to preferring internal storage.
    428                 prefer = PREFER_INTERNAL;
    429                 checkBoth = true;
    430                 break check_inner;
    431             }
    432 
    433             // Pick user preference
    434             int installPreference = Settings.System.getInt(getApplicationContext()
    435                     .getContentResolver(),
    436                     Settings.Secure.DEFAULT_INSTALL_LOCATION,
    437                     PackageHelper.APP_INSTALL_AUTO);
    438             if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
    439                 prefer = PREFER_INTERNAL;
    440                 break check_inner;
    441             } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
    442                 prefer = PREFER_EXTERNAL;
    443                 break check_inner;
    444             }
    445 
    446             /*
    447              * Fall back to default policy of internal-only if nothing else is
    448              * specified.
    449              */
    450             prefer = PREFER_INTERNAL;
    451         }
    452 
    453         final boolean emulated = Environment.isExternalStorageEmulated();
    454 
    455         final File apkFile = new File(archiveFilePath);
    456 
    457         boolean fitsOnInternal = false;
    458         if (checkBoth || prefer == PREFER_INTERNAL) {
    459             try {
    460                 fitsOnInternal = isUnderInternalThreshold(apkFile, threshold);
    461             } catch (FileNotFoundException e) {
    462                 return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
    463             }
    464         }
    465 
    466         boolean fitsOnSd = false;
    467         if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
    468             try {
    469                 fitsOnSd = isUnderExternalThreshold(apkFile);
    470             } catch (FileNotFoundException e) {
    471                 return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
    472             }
    473         }
    474 
    475         if (prefer == PREFER_INTERNAL) {
    476             if (fitsOnInternal) {
    477                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    478             }
    479         } else if (!emulated && prefer == PREFER_EXTERNAL) {
    480             if (fitsOnSd) {
    481                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    482             }
    483         }
    484 
    485         if (checkBoth) {
    486             if (fitsOnInternal) {
    487                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    488             } else if (!emulated && fitsOnSd) {
    489                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    490             }
    491         }
    492 
    493         /*
    494          * If they requested to be on the external media by default, return that
    495          * the media was unavailable. Otherwise, indicate there was insufficient
    496          * storage space available.
    497          */
    498         if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
    499                 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
    500             return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
    501         } else {
    502             return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
    503         }
    504     }
    505 
    506     /**
    507      * Measure a file to see if it fits within the free space threshold.
    508      *
    509      * @param apkFile file to check
    510      * @param threshold byte threshold to compare against
    511      * @return true if file fits under threshold
    512      * @throws FileNotFoundException when APK does not exist
    513      */
    514     private boolean isUnderInternalThreshold(File apkFile, long threshold)
    515             throws FileNotFoundException {
    516         final long size = apkFile.length();
    517         if (size == 0 && !apkFile.exists()) {
    518             throw new FileNotFoundException();
    519         }
    520 
    521         final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
    522         final long availInternalSize = (long) internalStats.getAvailableBlocks()
    523                 * (long) internalStats.getBlockSize();
    524 
    525         return (availInternalSize - size) > threshold;
    526     }
    527 
    528 
    529     /**
    530      * Measure a file to see if it fits in the external free space.
    531      *
    532      * @param apkFile file to check
    533      * @return true if file fits
    534      * @throws IOException when file does not exist
    535      */
    536     private boolean isUnderExternalThreshold(File apkFile) throws FileNotFoundException {
    537         if (Environment.isExternalStorageEmulated()) {
    538             return false;
    539         }
    540 
    541         final int sizeMb = calculateContainerSize(apkFile);
    542 
    543         final int availSdMb;
    544         if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
    545             final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
    546             final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
    547             availSdMb = sdStats.getAvailableBlocks() * blocksToMb;
    548         } else {
    549             availSdMb = -1;
    550         }
    551 
    552         return availSdMb > sizeMb;
    553     }
    554 
    555     /**
    556      * Calculate the container size for an APK. Takes into account the
    557      *
    558      * @param apkFile file from which to calculate size
    559      * @return size in megabytes (2^20 bytes)
    560      * @throws FileNotFoundException when file does not exist
    561      */
    562     private int calculateContainerSize(File apkFile) throws FileNotFoundException {
    563         // Calculate size of container needed to hold base APK.
    564         long sizeBytes = apkFile.length();
    565         if (sizeBytes == 0 && !apkFile.exists()) {
    566             throw new FileNotFoundException();
    567         }
    568 
    569         // Check all the native files that need to be copied and add that to the
    570         // container size.
    571         sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile);
    572 
    573         int sizeMb = (int) (sizeBytes >> 20);
    574         if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) {
    575             sizeMb++;
    576         }
    577 
    578         /*
    579          * Add buffer size because we don't have a good way to determine the
    580          * real FAT size. Your FAT size varies with how many directory entries
    581          * you need, how big the whole filesystem is, and other such headaches.
    582          */
    583         sizeMb++;
    584 
    585         return sizeMb;
    586     }
    587 }
    588