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