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