DefaultContainerService.java revision 5b993ce7bc29e43a3215a50ce6ce5d6550d4e5e2
1package com.android.defcontainer;
2
3import com.android.internal.app.IMediaContainerService;
4import com.android.internal.content.PackageHelper;
5import android.content.Intent;
6import android.content.pm.IPackageManager;
7import android.content.pm.PackageInfo;
8import android.content.pm.PackageManager;
9import android.content.pm.PackageParser;
10import android.content.pm.PackageParser.Package;
11import android.net.Uri;
12import android.os.Debug;
13import android.os.Environment;
14import android.os.IBinder;
15import android.os.storage.IMountService;
16import android.os.storage.StorageResultCode;
17import android.os.ParcelFileDescriptor;
18import android.os.Process;
19import android.os.RemoteException;
20import android.os.ServiceManager;
21import android.os.StatFs;
22import android.app.IntentService;
23import android.app.Service;
24import android.util.DisplayMetrics;
25import android.util.Log;
26
27import java.io.File;
28import java.io.FileInputStream;
29import java.io.FileNotFoundException;
30import java.io.FileOutputStream;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.OutputStream;
34
35import android.os.FileUtils;
36import android.provider.Settings;
37
38/*
39 * This service copies a downloaded apk to a file passed in as
40 * a ParcelFileDescriptor or to a newly created container specified
41 * by parameters. The DownloadManager gives access to this process
42 * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
43 * permission to access apks downloaded via the download manager.
44 */
45public class DefaultContainerService extends IntentService {
46    private static final String TAG = "DefContainer";
47    private static final boolean localLOGV = false;
48
49    private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
50        /*
51         * Creates a new container and copies resource there.
52         * @param paackageURI the uri of resource to be copied. Can be either
53         * a content uri or a file uri
54         * @param containerId the id of the secure container that should
55         * be used for creating a secure container into which the resource
56         * will be copied.
57         * @param key Refers to key used for encrypting the secure container
58         * @param resFileName Name of the target resource file(relative to newly
59         * created secure container)
60         * @return Returns the new cache path where the resource has been copied into
61         *
62         */
63        public String copyResourceToContainer(final Uri packageURI,
64                final String containerId,
65                final String key, final String resFileName) {
66            if (packageURI == null || containerId == null) {
67                return null;
68            }
69            return copyResourceInner(packageURI, containerId, key, resFileName);
70        }
71
72        /*
73         * Copy specified resource to output stream
74         * @param packageURI the uri of resource to be copied. Should be a
75         * file uri
76         * @param outStream Remote file descriptor to be used for copying
77         * @return Returns true if copy succeded or false otherwise.
78         */
79        public boolean copyResource(final Uri packageURI,
80                ParcelFileDescriptor outStream) {
81            if (packageURI == null ||  outStream == null) {
82                return false;
83            }
84            ParcelFileDescriptor.AutoCloseOutputStream
85            autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
86            return copyFile(packageURI, autoOut);
87        }
88
89        /*
90         * Determine the recommended install location for package
91         * specified by file uri location.
92         * @param fileUri the uri of resource to be copied. Should be a
93         * file uri
94         * @return Returns
95         *  PackageHelper.RECOMMEND_INSTALL_INTERNAL to install on internal storage
96         *  PackageHelper.RECOMMEND_INSTALL_EXTERNAL to install on external media
97         *  PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE for storage errors
98         *  PackageHelper.RECOMMEND_FAILED_INVALID_APK for parse errors.
99         */
100        public int getRecommendedInstallLocation(final Uri fileUri) {
101            if (!fileUri.getScheme().equals("file")) {
102                Log.w(TAG, "Falling back to installing on internal storage only");
103                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
104            }
105            final String archiveFilePath = fileUri.getPath();
106            PackageParser packageParser = new PackageParser(archiveFilePath);
107            File sourceFile = new File(archiveFilePath);
108            DisplayMetrics metrics = new DisplayMetrics();
109            metrics.setToDefaults();
110            PackageParser.Package pkg = packageParser.parsePackage(sourceFile, archiveFilePath, metrics, 0);
111            if (pkg == null) {
112                Log.w(TAG, "Failed to parse package");
113                return PackageHelper.RECOMMEND_FAILED_INVALID_APK;
114            }
115            int loc = recommendAppInstallLocation(pkg);
116            if (loc == PackageManager.INSTALL_EXTERNAL) {
117                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
118            } else if (loc == ERR_LOC) {
119                Log.i(TAG, "Failed to install insufficient storage");
120                return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
121            } else {
122                // Implies install on internal storage.
123                return 0;
124            }
125        }
126    };
127
128    public DefaultContainerService() {
129        super("DefaultContainerService");
130        setIntentRedelivery(true);
131    }
132
133    @Override
134    protected void onHandleIntent(Intent intent) {
135        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
136            IPackageManager pm = IPackageManager.Stub.asInterface(
137                    ServiceManager.getService("package"));
138            String pkg = null;
139            try {
140                while ((pkg=pm.nextPackageToClean(pkg)) != null) {
141                    eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
142                    eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
143                }
144            } catch (RemoteException e) {
145            }
146        }
147    }
148
149    void eraseFiles(File path) {
150        if (path.isDirectory()) {
151            String[] files = path.list();
152            if (files != null) {
153                for (String file : files) {
154                    eraseFiles(new File(path, file));
155                }
156            }
157        }
158        path.delete();
159    }
160
161    public IBinder onBind(Intent intent) {
162        return mBinder;
163    }
164
165    private IMountService getMountService() {
166        return IMountService.Stub.asInterface(ServiceManager.getService("mount"));
167    }
168
169    private String copyResourceInner(Uri packageURI, String newCacheId, String key, String resFileName) {
170        // Create new container at newCachePath
171        String codePath = packageURI.getPath();
172        String newCachePath = null;
173        final int CREATE_FAILED = 1;
174        final int COPY_FAILED = 2;
175        final int FINALIZE_FAILED = 3;
176        final int PASS = 4;
177        int errCode = CREATE_FAILED;
178        // Create new container
179        if ((newCachePath = createSdDir(packageURI, newCacheId, key)) != null) {
180            if (localLOGV) Log.i(TAG, "Created container for " + newCacheId
181                    + " at path : " + newCachePath);
182            File resFile = new File(newCachePath, resFileName);
183            errCode = COPY_FAILED;
184            // Copy file from codePath
185            if (FileUtils.copyFile(new File(codePath), resFile)) {
186                if (localLOGV) Log.i(TAG, "Copied " + codePath + " to " + resFile);
187                errCode = FINALIZE_FAILED;
188                if (finalizeSdDir(newCacheId)) {
189                    errCode = PASS;
190                }
191            }
192        }
193        // Print error based on errCode
194        String errMsg = "";
195        switch (errCode) {
196            case CREATE_FAILED:
197                errMsg = "CREATE_FAILED";
198                break;
199            case COPY_FAILED:
200                errMsg = "COPY_FAILED";
201                if (localLOGV) Log.i(TAG, "Destroying " + newCacheId +
202                        " at path " + newCachePath + " after " + errMsg);
203                destroySdDir(newCacheId);
204                break;
205            case FINALIZE_FAILED:
206                errMsg = "FINALIZE_FAILED";
207                if (localLOGV) Log.i(TAG, "Destroying " + newCacheId +
208                        " at path " + newCachePath + " after " + errMsg);
209                destroySdDir(newCacheId);
210                break;
211            default:
212                errMsg = "PASS";
213            if (localLOGV) Log.i(TAG, "Unmounting " + newCacheId +
214                    " at path " + newCachePath + " after " + errMsg);
215                unMountSdDir(newCacheId);
216                break;
217        }
218        if (errCode != PASS) {
219            return null;
220        }
221        return newCachePath;
222    }
223
224    private String createSdDir(final Uri packageURI,
225            String containerId, String sdEncKey) {
226        File tmpPackageFile = new File(packageURI.getPath());
227        // Create mount point via MountService
228        IMountService mountService = getMountService();
229        long len = tmpPackageFile.length();
230        int mbLen = (int) (len/(1024*1024));
231        if ((len - (mbLen * 1024 * 1024)) > 0) {
232            mbLen++;
233        }
234        if (localLOGV) Log.i(TAG, "mbLen=" + mbLen);
235        String cachePath = null;
236        int ownerUid = Process.myUid();
237        try {
238            int rc = mountService.createSecureContainer(
239                    containerId, mbLen, "vfat", sdEncKey, ownerUid);
240
241            if (rc != StorageResultCode.OperationSucceeded) {
242                Log.e(TAG, String.format("Container creation failed (%d)", rc));
243
244                // XXX: This destroy should not be necessary
245                rc = mountService.destroySecureContainer(containerId);
246                if (rc != StorageResultCode.OperationSucceeded) {
247                    Log.e(TAG, String.format("Container creation-cleanup failed (%d)", rc));
248                    return null;
249                }
250
251                // XXX: Does this ever actually succeed?
252                rc = mountService.createSecureContainer(
253                        containerId, mbLen, "vfat", sdEncKey, ownerUid);
254                if (rc != StorageResultCode.OperationSucceeded) {
255                    Log.e(TAG, String.format("Container creation retry failed (%d)", rc));
256                }
257            }
258
259            cachePath = mountService.getSecureContainerPath(containerId);
260            if (localLOGV) Log.i(TAG, "Trying to create secure container for  "
261                    + containerId + ", cachePath =" + cachePath);
262            return cachePath;
263        } catch(RemoteException e) {
264            Log.e(TAG, "MountService not running?");
265            return null;
266        }
267    }
268
269    private boolean destroySdDir(String containerId) {
270        try {
271            // We need to destroy right away
272            getMountService().destroySecureContainer(containerId);
273            return true;
274        } catch (IllegalStateException e) {
275            Log.i(TAG, "Failed to destroy container : " + containerId);
276        } catch(RemoteException e) {
277            Log.e(TAG, "MountService not running?");
278        }
279        return false;
280    }
281
282    private boolean finalizeSdDir(String containerId){
283        try {
284            getMountService().finalizeSecureContainer(containerId);
285            return true;
286        } catch (IllegalStateException e) {
287            Log.i(TAG, "Failed to finalize container for pkg : " + containerId);
288        } catch(RemoteException e) {
289            Log.e(TAG, "MountService not running?");
290        }
291        return false;
292    }
293
294    private boolean unMountSdDir(String containerId) {
295        try {
296            getMountService().unmountSecureContainer(containerId);
297            return true;
298        } catch (IllegalStateException e) {
299            Log.e(TAG, "Failed to unmount id:  " + containerId + " with exception " + e);
300        } catch(RemoteException e) {
301            Log.e(TAG, "MountService not running?");
302        }
303        return false;
304    }
305
306    private String mountSdDir(String containerId, String key) {
307        try {
308            int rc = getMountService().mountSecureContainer(containerId, key, Process.myUid());
309            if (rc == StorageResultCode.OperationSucceeded) {
310                return getMountService().getSecureContainerPath(containerId);
311            } else {
312                Log.e(TAG, String.format("Failed to mount id %s with rc %d ", containerId, rc));
313            }
314        } catch(RemoteException e) {
315            Log.e(TAG, "MountService not running?");
316        }
317        return null;
318    }
319
320    public static boolean copyToFile(InputStream inputStream, FileOutputStream out) {
321        try {
322            byte[] buffer = new byte[4096];
323            int bytesRead;
324            while ((bytesRead = inputStream.read(buffer)) >= 0) {
325                out.write(buffer, 0, bytesRead);
326            }
327            return true;
328        } catch (IOException e) {
329            Log.i(TAG, "Exception : " + e + " when copying file");
330            return false;
331        }
332    }
333
334    public static boolean copyToFile(File srcFile, FileOutputStream out) {
335        InputStream inputStream = null;
336        try {
337            inputStream = new FileInputStream(srcFile);
338            return copyToFile(inputStream, out);
339        } catch (IOException e) {
340            return false;
341        } finally {
342            try { if (inputStream != null) inputStream.close(); } catch (IOException e) {}
343        }
344    }
345
346    private  boolean copyFile(Uri pPackageURI, FileOutputStream outStream) {
347        if (pPackageURI.getScheme().equals("file")) {
348            final File srcPackageFile = new File(pPackageURI.getPath());
349            // We copy the source package file to a temp file and then rename it to the
350            // destination file in order to eliminate a window where the package directory
351            // scanner notices the new package file but it's not completely copied yet.
352            if (!copyToFile(srcPackageFile, outStream)) {
353                Log.e(TAG, "Couldn't copy file: " + srcPackageFile);
354                return false;
355            }
356        } else if (pPackageURI.getScheme().equals("content")) {
357            ParcelFileDescriptor fd = null;
358            try {
359                fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
360            } catch (FileNotFoundException e) {
361                Log.e(TAG, "Couldn't open file descriptor from download service. Failed with exception " + e);
362                return false;
363            }
364            if (fd == null) {
365                Log.e(TAG, "Couldn't open file descriptor from download service (null).");
366                return false;
367            } else {
368                if (localLOGV) {
369                    Log.v(TAG, "Opened file descriptor from download service.");
370                }
371                ParcelFileDescriptor.AutoCloseInputStream
372                dlStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
373                // We copy the source package file to a temp file and then rename it to the
374                // destination file in order to eliminate a window where the package directory
375                // scanner notices the new package file but it's not completely copied yet.
376                if (!copyToFile(dlStream, outStream)) {
377                    Log.e(TAG, "Couldn't copy " + pPackageURI + " to temp file.");
378                    return false;
379                }
380            }
381        } else {
382            Log.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
383            return false;
384        }
385        return true;
386    }
387
388    // Constants related to app heuristics
389    // No-installation limit for internal flash: 10% or less space available
390    private static final double LOW_NAND_FLASH_TRESHOLD = 0.1;
391
392    // SD-to-internal app size threshold: currently set to 1 MB
393    private static final long INSTALL_ON_SD_THRESHOLD = (1024 * 1024);
394    private static final int ERR_LOC = -1;
395
396    public int recommendAppInstallLocation(Package pkg) {
397        // Initial implementation:
398        // Package size = code size + cache size + data size
399        // If code size > 1 MB, install on SD card.
400        // Else install on internal NAND flash, unless space on NAND is less than 10%
401
402        if (pkg == null) {
403            return ERR_LOC;
404        }
405
406        StatFs internalFlashStats = new StatFs(Environment.getDataDirectory().getPath());
407        StatFs sdcardStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
408
409        long totalInternalFlashSize = (long)internalFlashStats.getBlockCount() *
410                (long)internalFlashStats.getBlockSize();
411        long availInternalFlashSize = (long)internalFlashStats.getAvailableBlocks() *
412                (long)internalFlashStats.getBlockSize();
413        long availSDSize = (long)sdcardStats.getAvailableBlocks() *
414                (long)sdcardStats.getBlockSize();
415
416        double pctNandFree = (double)availInternalFlashSize / (double)totalInternalFlashSize;
417
418        final String archiveFilePath = pkg.mScanPath;
419        File apkFile = new File(archiveFilePath);
420        long pkgLen = apkFile.length();
421
422        boolean auto = true;
423        // To make final copy
424        long reqInstallSize = pkgLen;
425        // For dex files
426        long reqInternalSize = 1 * pkgLen;
427        boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
428        boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalFlashSize);
429        boolean fitsOnSd = (reqInstallSize < availSDSize) && intThresholdOk &&
430                (reqInternalSize < availInternalFlashSize);
431        boolean fitsOnInt = intThresholdOk && intAvailOk;
432
433        // Consider application flags preferences as well...
434        boolean installOnlyOnSd = (pkg.installLocation ==
435                PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL);
436        boolean installOnlyInternal = (pkg.installLocation ==
437                PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY);
438        if (installOnlyInternal) {
439            // If set explicitly in manifest,
440            // let that override everything else
441            auto = false;
442        } else if (installOnlyOnSd){
443            // Check if this can be accommodated on the sdcard
444            if (fitsOnSd) {
445                auto = false;
446            }
447        } else {
448            // Check if user option is enabled
449            boolean setInstallLoc = Settings.System.getInt(getApplicationContext()
450                    .getContentResolver(),
451                    Settings.System.SET_INSTALL_LOCATION, 0) != 0;
452            if (setInstallLoc) {
453                // Pick user preference
454                int installPreference = Settings.System.getInt(getApplicationContext()
455                        .getContentResolver(),
456                        Settings.System.DEFAULT_INSTALL_LOCATION,
457                        PackageInfo.INSTALL_LOCATION_AUTO);
458                if (installPreference == 1) {
459                    installOnlyInternal = true;
460                    auto = false;
461                } else if (installPreference == 2) {
462                    installOnlyOnSd = true;
463                    auto = false;
464                }
465            }
466        }
467        if (!auto) {
468            if (installOnlyOnSd) {
469                return fitsOnSd ? PackageManager.INSTALL_EXTERNAL : ERR_LOC;
470            } else if (installOnlyInternal){
471                // Check on internal flash
472                return fitsOnInt ? 0 : ERR_LOC;
473            }
474        }
475        // Try to install internally
476        if (fitsOnInt) {
477            return 0;
478        }
479        // Try the sdcard now.
480        if (fitsOnSd) {
481            return PackageManager.INSTALL_EXTERNAL;
482        }
483        // Return error code
484        return ERR_LOC;
485    }
486
487}
488