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.content.Intent;
     24 import android.content.pm.IPackageManager;
     25 import android.content.pm.PackageInfo;
     26 import android.content.pm.PackageInfoLite;
     27 import android.content.pm.PackageManager;
     28 import android.content.pm.PackageParser;
     29 import android.content.res.ObbInfo;
     30 import android.content.res.ObbScanner;
     31 import android.net.Uri;
     32 import android.os.Environment;
     33 import android.os.IBinder;
     34 import android.os.ParcelFileDescriptor;
     35 import android.os.Process;
     36 import android.os.RemoteException;
     37 import android.os.ServiceManager;
     38 import android.os.StatFs;
     39 import android.app.IntentService;
     40 import android.util.DisplayMetrics;
     41 import android.util.Log;
     42 import android.util.Pair;
     43 
     44 import java.io.File;
     45 import java.io.FileInputStream;
     46 import java.io.FileNotFoundException;
     47 import java.io.FileOutputStream;
     48 import java.io.IOException;
     49 import java.io.InputStream;
     50 import java.util.LinkedList;
     51 import java.util.List;
     52 import java.util.zip.ZipEntry;
     53 import java.util.zip.ZipException;
     54 import java.util.zip.ZipFile;
     55 
     56 import android.os.FileUtils;
     57 import android.provider.Settings;
     58 
     59 /*
     60  * This service copies a downloaded apk to a file passed in as
     61  * a ParcelFileDescriptor or to a newly created container specified
     62  * by parameters. The DownloadManager gives access to this process
     63  * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
     64  * permission to access apks downloaded via the download manager.
     65  */
     66 public class DefaultContainerService extends IntentService {
     67     private static final String TAG = "DefContainer";
     68     private static final boolean localLOGV = true;
     69 
     70     private static final String LIB_DIR_NAME = "lib";
     71 
     72     private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
     73         /*
     74          * Creates a new container and copies resource there.
     75          * @param paackageURI the uri of resource to be copied. Can be either
     76          * a content uri or a file uri
     77          * @param cid the id of the secure container that should
     78          * be used for creating a secure container into which the resource
     79          * will be copied.
     80          * @param key Refers to key used for encrypting the secure container
     81          * @param resFileName Name of the target resource file(relative to newly
     82          * created secure container)
     83          * @return Returns the new cache path where the resource has been copied into
     84          *
     85          */
     86         public String copyResourceToContainer(final Uri packageURI,
     87                 final String cid,
     88                 final String key, final String resFileName) {
     89             if (packageURI == null || cid == null) {
     90                 return null;
     91             }
     92             return copyResourceInner(packageURI, cid, key, resFileName);
     93         }
     94 
     95         /*
     96          * Copy specified resource to output stream
     97          * @param packageURI the uri of resource to be copied. Should be a
     98          * file uri
     99          * @param outStream Remote file descriptor to be used for copying
    100          * @return Returns true if copy succeded or false otherwise.
    101          */
    102         public boolean copyResource(final Uri packageURI,
    103                 ParcelFileDescriptor outStream) {
    104             if (packageURI == null ||  outStream == null) {
    105                 return false;
    106             }
    107             ParcelFileDescriptor.AutoCloseOutputStream
    108             autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
    109             return copyFile(packageURI, autoOut);
    110         }
    111 
    112         /*
    113          * Determine the recommended install location for package
    114          * specified by file uri location.
    115          * @param fileUri the uri of resource to be copied. Should be a
    116          * file uri
    117          * @return Returns PackageInfoLite object containing
    118          * the package info and recommended app location.
    119          */
    120         public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags) {
    121             PackageInfoLite ret = new PackageInfoLite();
    122             if (fileUri == null) {
    123                 Log.i(TAG, "Invalid package uri " + fileUri);
    124                 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
    125                 return ret;
    126             }
    127             String scheme = fileUri.getScheme();
    128             if (scheme != null && !scheme.equals("file")) {
    129                 Log.w(TAG, "Falling back to installing on internal storage only");
    130                 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    131                 return ret;
    132             }
    133             String archiveFilePath = fileUri.getPath();
    134             PackageParser packageParser = new PackageParser(archiveFilePath);
    135             File sourceFile = new File(archiveFilePath);
    136             DisplayMetrics metrics = new DisplayMetrics();
    137             metrics.setToDefaults();
    138             PackageParser.PackageLite pkg = packageParser.parsePackageLite(
    139                     archiveFilePath, 0);
    140             // Nuke the parser reference right away and force a gc
    141             packageParser = null;
    142             Runtime.getRuntime().gc();
    143             if (pkg == null) {
    144                 Log.w(TAG, "Failed to parse package");
    145                 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
    146                 return ret;
    147             }
    148             ret.packageName = pkg.packageName;
    149             ret.installLocation = pkg.installLocation;
    150             ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, archiveFilePath, flags);
    151             return ret;
    152         }
    153 
    154         public boolean checkFreeStorage(boolean external, Uri fileUri) {
    155             return checkFreeStorageInner(external, fileUri);
    156         }
    157 
    158         public ObbInfo getObbInfo(String filename) {
    159             try {
    160                 return ObbScanner.getObbInfo(filename);
    161             } catch (IOException e) {
    162                 Log.d(TAG, "Couldn't get OBB info for " + filename);
    163                 return null;
    164             }
    165         }
    166     };
    167 
    168     public DefaultContainerService() {
    169         super("DefaultContainerService");
    170         setIntentRedelivery(true);
    171     }
    172 
    173     @Override
    174     protected void onHandleIntent(Intent intent) {
    175         if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
    176             IPackageManager pm = IPackageManager.Stub.asInterface(
    177                     ServiceManager.getService("package"));
    178             String pkg = null;
    179             try {
    180                 while ((pkg=pm.nextPackageToClean(pkg)) != null) {
    181                     eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
    182                     eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
    183                 }
    184             } catch (RemoteException e) {
    185             }
    186         }
    187     }
    188 
    189     void eraseFiles(File path) {
    190         if (path.isDirectory()) {
    191             String[] files = path.list();
    192             if (files != null) {
    193                 for (String file : files) {
    194                     eraseFiles(new File(path, file));
    195                 }
    196             }
    197         }
    198         path.delete();
    199     }
    200 
    201     public IBinder onBind(Intent intent) {
    202         return mBinder;
    203     }
    204 
    205     private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) {
    206         // Make sure the sdcard is mounted.
    207         String status = Environment.getExternalStorageState();
    208         if (!status.equals(Environment.MEDIA_MOUNTED)) {
    209             Log.w(TAG, "Make sure sdcard is mounted.");
    210             return null;
    211         }
    212 
    213         // The .apk file
    214         String codePath = packageURI.getPath();
    215         File codeFile = new File(codePath);
    216 
    217         // Calculate size of container needed to hold base APK.
    218         long sizeBytes = codeFile.length();
    219 
    220         // Check all the native files that need to be copied and add that to the container size.
    221         ZipFile zipFile;
    222         List<Pair<ZipEntry, String>> nativeFiles;
    223         try {
    224             zipFile = new ZipFile(codeFile);
    225 
    226             nativeFiles = new LinkedList<Pair<ZipEntry, String>>();
    227 
    228             NativeLibraryHelper.listPackageNativeBinariesLI(zipFile, nativeFiles);
    229 
    230             final int N = nativeFiles.size();
    231             for (int i = 0; i < N; i++) {
    232                 final Pair<ZipEntry, String> entry = nativeFiles.get(i);
    233 
    234                 /*
    235                  * Note that PackageHelper.createSdDir adds a 1MB padding on
    236                  * our claimed size, so we don't have to worry about block
    237                  * alignment here.
    238                  */
    239                 sizeBytes += entry.first.getSize();
    240             }
    241         } catch (ZipException e) {
    242             Log.w(TAG, "Failed to extract data from package file", e);
    243             return null;
    244         } catch (IOException e) {
    245             Log.w(TAG, "Failed to cache package shared libs", e);
    246             return null;
    247         }
    248 
    249         // Create new container
    250         String newCachePath = null;
    251         if ((newCachePath = PackageHelper.createSdDir(sizeBytes, newCid, key, Process.myUid())) == null) {
    252             Log.e(TAG, "Failed to create container " + newCid);
    253             return null;
    254         }
    255         if (localLOGV)
    256             Log.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
    257         File resFile = new File(newCachePath, resFileName);
    258         if (!FileUtils.copyFile(new File(codePath), resFile)) {
    259             Log.e(TAG, "Failed to copy " + codePath + " to " + resFile);
    260             // Clean up container
    261             PackageHelper.destroySdDir(newCid);
    262             return null;
    263         }
    264 
    265         try {
    266             File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
    267             sharedLibraryDir.mkdir();
    268 
    269             final int N = nativeFiles.size();
    270             for (int i = 0; i < N; i++) {
    271                 final Pair<ZipEntry, String> entry = nativeFiles.get(i);
    272 
    273                 InputStream is = zipFile.getInputStream(entry.first);
    274                 try {
    275                     File destFile = new File(sharedLibraryDir, entry.second);
    276                     if (!FileUtils.copyToFile(is, destFile)) {
    277                         throw new IOException("Couldn't copy native binary "
    278                                 + entry.first.getName() + " to " + entry.second);
    279                     }
    280                 } finally {
    281                     is.close();
    282                 }
    283             }
    284         } catch (IOException e) {
    285             Log.e(TAG, "Couldn't copy native file to container", e);
    286             PackageHelper.destroySdDir(newCid);
    287             return null;
    288         }
    289 
    290         if (localLOGV) Log.i(TAG, "Copied " + codePath + " to " + resFile);
    291         if (!PackageHelper.finalizeSdDir(newCid)) {
    292             Log.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
    293             // Clean up container
    294             PackageHelper.destroySdDir(newCid);
    295         }
    296         if (localLOGV) Log.i(TAG, "Finalized container " + newCid);
    297         if (PackageHelper.isContainerMounted(newCid)) {
    298             if (localLOGV) Log.i(TAG, "Unmounting " + newCid +
    299                     " at path " + newCachePath);
    300             // Force a gc to avoid being killed.
    301             Runtime.getRuntime().gc();
    302             PackageHelper.unMountSdDir(newCid);
    303         } else {
    304             if (localLOGV) Log.i(TAG, "Container " + newCid + " not mounted");
    305         }
    306         return newCachePath;
    307     }
    308 
    309     public static boolean copyToFile(InputStream inputStream, FileOutputStream out) {
    310         try {
    311             byte[] buffer = new byte[4096];
    312             int bytesRead;
    313             while ((bytesRead = inputStream.read(buffer)) >= 0) {
    314                 out.write(buffer, 0, bytesRead);
    315             }
    316             return true;
    317         } catch (IOException e) {
    318             Log.i(TAG, "Exception : " + e + " when copying file");
    319             return false;
    320         }
    321     }
    322 
    323     public static boolean copyToFile(File srcFile, FileOutputStream out) {
    324         InputStream inputStream = null;
    325         try {
    326             inputStream = new FileInputStream(srcFile);
    327             return copyToFile(inputStream, out);
    328         } catch (IOException e) {
    329             return false;
    330         } finally {
    331             try { if (inputStream != null) inputStream.close(); } catch (IOException e) {}
    332         }
    333     }
    334 
    335     private  boolean copyFile(Uri pPackageURI, FileOutputStream outStream) {
    336         String scheme = pPackageURI.getScheme();
    337         if (scheme == null || scheme.equals("file")) {
    338             final File srcPackageFile = new File(pPackageURI.getPath());
    339             // We copy the source package file to a temp file and then rename it to the
    340             // destination file in order to eliminate a window where the package directory
    341             // scanner notices the new package file but it's not completely copied yet.
    342             if (!copyToFile(srcPackageFile, outStream)) {
    343                 Log.e(TAG, "Couldn't copy file: " + srcPackageFile);
    344                 return false;
    345             }
    346         } else if (scheme.equals("content")) {
    347             ParcelFileDescriptor fd = null;
    348             try {
    349                 fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
    350             } catch (FileNotFoundException e) {
    351                 Log.e(TAG, "Couldn't open file descriptor from download service. Failed with exception " + e);
    352                 return false;
    353             }
    354             if (fd == null) {
    355                 Log.e(TAG, "Couldn't open file descriptor from download service (null).");
    356                 return false;
    357             } else {
    358                 if (localLOGV) {
    359                     Log.v(TAG, "Opened file descriptor from download service.");
    360                 }
    361                 ParcelFileDescriptor.AutoCloseInputStream
    362                 dlStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
    363                 // We copy the source package file to a temp file and then rename it to the
    364                 // destination file in order to eliminate a window where the package directory
    365                 // scanner notices the new package file but it's not completely copied yet.
    366                 if (!copyToFile(dlStream, outStream)) {
    367                     Log.e(TAG, "Couldn't copy " + pPackageURI + " to temp file.");
    368                     return false;
    369                 }
    370             }
    371         } else {
    372             Log.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
    373             return false;
    374         }
    375         return true;
    376     }
    377 
    378     // Constants related to app heuristics
    379     // No-installation limit for internal flash: 10% or less space available
    380     private static final double LOW_NAND_FLASH_TRESHOLD = 0.1;
    381 
    382     // SD-to-internal app size threshold: currently set to 1 MB
    383     private static final long INSTALL_ON_SD_THRESHOLD = (1024 * 1024);
    384     private static final int ERR_LOC = -1;
    385 
    386     private int recommendAppInstallLocation(int installLocation,
    387             String archiveFilePath, int flags) {
    388         boolean checkInt = false;
    389         boolean checkExt = false;
    390         boolean checkBoth = false;
    391         check_inner : {
    392             // Check flags.
    393             if ((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) {
    394                 // Check for forward locked app
    395                 checkInt = true;
    396                 break check_inner;
    397             } else if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
    398                 // Explicit flag to install internally.
    399                 // Check internal storage and return
    400                 checkInt = true;
    401                 break check_inner;
    402             } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
    403                 // Explicit flag to install externally.
    404                 // Check external storage and return
    405                 checkExt = true;
    406                 break check_inner;
    407             }
    408             // Check for manifest option
    409             if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
    410                 checkInt = true;
    411                 break check_inner;
    412             } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
    413                 checkExt = true;
    414                 checkBoth = true;
    415                 break check_inner;
    416             } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
    417                 checkInt = true;
    418                 checkBoth = true;
    419                 break check_inner;
    420             }
    421             // Pick user preference
    422             int installPreference = Settings.System.getInt(getApplicationContext()
    423                     .getContentResolver(),
    424                     Settings.Secure.DEFAULT_INSTALL_LOCATION,
    425                     PackageHelper.APP_INSTALL_AUTO);
    426             if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
    427                 checkInt = true;
    428                 break check_inner;
    429             } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
    430                 checkExt = true;
    431                 break check_inner;
    432             }
    433             // Fall back to default policy if nothing else is specified.
    434             checkInt = true;
    435         }
    436 
    437         // Package size = code size + cache size + data size
    438         // If code size > 1 MB, install on SD card.
    439         // Else install on internal NAND flash, unless space on NAND is less than 10%
    440         String status = Environment.getExternalStorageState();
    441         long availSDSize = -1;
    442         boolean mediaAvailable = false;
    443         if (status.equals(Environment.MEDIA_MOUNTED)) {
    444             StatFs sdStats = new StatFs(
    445                     Environment.getExternalStorageDirectory().getPath());
    446             availSDSize = (long)sdStats.getAvailableBlocks() *
    447                     (long)sdStats.getBlockSize();
    448             mediaAvailable = true;
    449         }
    450         StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
    451         long totalInternalSize = (long)internalStats.getBlockCount() *
    452                 (long)internalStats.getBlockSize();
    453         long availInternalSize = (long)internalStats.getAvailableBlocks() *
    454                 (long)internalStats.getBlockSize();
    455 
    456         double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
    457 
    458         File apkFile = new File(archiveFilePath);
    459         long pkgLen = apkFile.length();
    460 
    461         // To make final copy
    462         long reqInstallSize = pkgLen;
    463         // For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
    464         long reqInternalSize = 0;
    465         boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
    466         boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
    467         boolean fitsOnSd = false;
    468         if (mediaAvailable && (reqInstallSize < availSDSize)) {
    469             // If we do not have an internal size requirement
    470             // don't do a threshold check.
    471             if (reqInternalSize == 0) {
    472                 fitsOnSd = true;
    473             } else if ((reqInternalSize < availInternalSize) && intThresholdOk) {
    474                 fitsOnSd = true;
    475             }
    476         }
    477         boolean fitsOnInt = intThresholdOk && intAvailOk;
    478         if (checkInt) {
    479             // Check for internal memory availability
    480             if (fitsOnInt) {
    481                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    482             }
    483         } else if (checkExt) {
    484             if (fitsOnSd) {
    485                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    486             }
    487         }
    488         if (checkBoth) {
    489             // Check for internal first
    490             if (fitsOnInt) {
    491                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
    492             }
    493             // Check for external next
    494             if (fitsOnSd) {
    495                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    496             }
    497         }
    498         if ((checkExt || checkBoth) && !mediaAvailable) {
    499             return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
    500         }
    501         return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
    502     }
    503 
    504     private boolean checkFreeStorageInner(boolean external, Uri packageURI) {
    505         File apkFile = new File(packageURI.getPath());
    506         long size = apkFile.length();
    507         if (external) {
    508             String status = Environment.getExternalStorageState();
    509             long availSDSize = -1;
    510             if (status.equals(Environment.MEDIA_MOUNTED)) {
    511                 StatFs sdStats = new StatFs(
    512                         Environment.getExternalStorageDirectory().getPath());
    513                 availSDSize = (long)sdStats.getAvailableBlocks() *
    514                 (long)sdStats.getBlockSize();
    515             }
    516             return availSDSize > size;
    517         }
    518         StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
    519         long totalInternalSize = (long)internalStats.getBlockCount() *
    520         (long)internalStats.getBlockSize();
    521         long availInternalSize = (long)internalStats.getAvailableBlocks() *
    522         (long)internalStats.getBlockSize();
    523 
    524         double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
    525         // To make final copy
    526         long reqInstallSize = size;
    527         // For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
    528         long reqInternalSize = 0;
    529         boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
    530         boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
    531         return intThresholdOk && intAvailOk;
    532     }
    533 }
    534