DefaultContainerService.java revision 183ce028f10442dd6ada59de8fa531d690134663
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            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
227
228            final File directory = new File(path);
229            if (directory.exists() && directory.isDirectory()) {
230                return MeasurementUtils.measureDirectory(path);
231            } else {
232                return 0L;
233            }
234        }
235
236        @Override
237        public long[] getFileSystemStats(String path) {
238            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
239
240            try {
241                final StructStatFs stat = Libcore.os.statfs(path);
242                final long totalSize = stat.f_blocks * stat.f_bsize;
243                final long availSize = stat.f_bavail * stat.f_bsize;
244                return new long[] { totalSize, availSize };
245            } catch (ErrnoException e) {
246                throw new IllegalStateException(e);
247            }
248        }
249
250        @Override
251        public void clearDirectory(String path) throws RemoteException {
252            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
253
254            final File directory = new File(path);
255            if (directory.exists() && directory.isDirectory()) {
256                eraseFiles(directory);
257            }
258        }
259    };
260
261    public DefaultContainerService() {
262        super("DefaultContainerService");
263        setIntentRedelivery(true);
264    }
265
266    @Override
267    protected void onHandleIntent(Intent intent) {
268        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
269            IPackageManager pm = IPackageManager.Stub.asInterface(
270                    ServiceManager.getService("package"));
271            String pkg = null;
272            try {
273                while ((pkg=pm.nextPackageToClean(pkg)) != null) {
274                    eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
275                    eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
276                    eraseFiles(Environment.getExternalStorageAppObbDirectory(pkg));
277                }
278            } catch (RemoteException e) {
279            }
280        }
281    }
282
283    void eraseFiles(File path) {
284        if (path.isDirectory()) {
285            String[] files = path.list();
286            if (files != null) {
287                for (String file : files) {
288                    eraseFiles(new File(path, file));
289                }
290            }
291        }
292        path.delete();
293    }
294
295    public IBinder onBind(Intent intent) {
296        return mBinder;
297    }
298
299    private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName,
300            String publicResFileName, boolean isExternal, boolean isForwardLocked) {
301
302        if (isExternal) {
303            // Make sure the sdcard is mounted.
304            String status = Environment.getExternalStorageState();
305            if (!status.equals(Environment.MEDIA_MOUNTED)) {
306                Slog.w(TAG, "Make sure sdcard is mounted.");
307                return null;
308            }
309        }
310
311        // The .apk file
312        String codePath = packageURI.getPath();
313        File codeFile = new File(codePath);
314
315        // Calculate size of container needed to hold base APK.
316        final int sizeMb;
317        try {
318            sizeMb = calculateContainerSize(codeFile, isForwardLocked);
319        } catch (IOException e) {
320            Slog.w(TAG, "Problem when trying to copy " + codeFile.getPath());
321            return null;
322        }
323
324        // Create new container
325        final String newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid(),
326                isExternal);
327        if (newCachePath == null) {
328            Slog.e(TAG, "Failed to create container " + newCid);
329            return null;
330        }
331
332        if (localLOGV) {
333            Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
334        }
335
336        final File resFile = new File(newCachePath, resFileName);
337        if (FileUtils.copyFile(new File(codePath), resFile)) {
338            if (localLOGV) {
339                Slog.i(TAG, "Copied " + codePath + " to " + resFile);
340            }
341        } else {
342            Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile);
343            // Clean up container
344            PackageHelper.destroySdDir(newCid);
345            return null;
346        }
347
348        try {
349            Libcore.os.chmod(resFile.getAbsolutePath(), 0640);
350        } catch (ErrnoException e) {
351            Slog.e(TAG, "Could not chown APK: " + e.getMessage());
352            PackageHelper.destroySdDir(newCid);
353            return null;
354        }
355
356        if (isForwardLocked) {
357            File publicZipFile = new File(newCachePath, publicResFileName);
358            try {
359                PackageHelper.extractPublicFiles(resFile.getAbsolutePath(), publicZipFile);
360                if (localLOGV) {
361                    Slog.i(TAG, "Copied resources to " + publicZipFile);
362                }
363            } catch (IOException e) {
364                Slog.e(TAG, "Could not chown public APK " + publicZipFile.getAbsolutePath() + ": "
365                        + e.getMessage());
366                PackageHelper.destroySdDir(newCid);
367                return null;
368            }
369
370            try {
371                Libcore.os.chmod(publicZipFile.getAbsolutePath(), 0644);
372            } catch (ErrnoException e) {
373                Slog.e(TAG, "Could not chown public resource file: " + e.getMessage());
374                PackageHelper.destroySdDir(newCid);
375                return null;
376            }
377        }
378
379        final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
380        if (sharedLibraryDir.mkdir()) {
381            int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir);
382            if (ret != PackageManager.INSTALL_SUCCEEDED) {
383                Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath());
384                PackageHelper.destroySdDir(newCid);
385                return null;
386            }
387        } else {
388            Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath());
389            PackageHelper.destroySdDir(newCid);
390            return null;
391        }
392
393        if (!PackageHelper.finalizeSdDir(newCid)) {
394            Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
395            // Clean up container
396            PackageHelper.destroySdDir(newCid);
397            return null;
398        }
399
400        if (localLOGV) {
401            Slog.i(TAG, "Finalized container " + newCid);
402        }
403
404        if (PackageHelper.isContainerMounted(newCid)) {
405            if (localLOGV) {
406                Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath);
407            }
408
409            // Force a gc to avoid being killed.
410            Runtime.getRuntime().gc();
411            PackageHelper.unMountSdDir(newCid);
412        } else {
413            if (localLOGV) {
414                Slog.i(TAG, "Container " + newCid + " not mounted");
415            }
416        }
417
418        return newCachePath;
419    }
420
421    private static void copyToFile(InputStream inputStream, OutputStream out) throws IOException {
422        byte[] buffer = new byte[16384];
423        int bytesRead;
424        while ((bytesRead = inputStream.read(buffer)) >= 0) {
425            out.write(buffer, 0, bytesRead);
426        }
427    }
428
429    private void copyFile(Uri pPackageURI, OutputStream outStream,
430            ContainerEncryptionParams encryptionParams) throws FileNotFoundException, IOException,
431            DigestException {
432        String scheme = pPackageURI.getScheme();
433        InputStream inStream = null;
434        try {
435            if (scheme == null || scheme.equals("file")) {
436                final InputStream is = new FileInputStream(new File(pPackageURI.getPath()));
437                inStream = new BufferedInputStream(is);
438            } else if (scheme.equals("content")) {
439                final ParcelFileDescriptor fd;
440                try {
441                    fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
442                } catch (FileNotFoundException e) {
443                    Slog.e(TAG, "Couldn't open file descriptor from download service. "
444                            + "Failed with exception " + e);
445                    throw e;
446                }
447
448                if (fd == null) {
449                    Slog.e(TAG, "Provider returned no file descriptor for " +
450                            pPackageURI.toString());
451                    throw new FileNotFoundException("provider returned no file descriptor");
452                } else {
453                    if (localLOGV) {
454                        Slog.i(TAG, "Opened file descriptor from download service.");
455                    }
456                    inStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
457                }
458            } else {
459                Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
460                throw new FileNotFoundException("Package URI is not 'file:' or 'content:'");
461            }
462
463            /*
464             * If this resource is encrypted, get the decrypted stream version
465             * of it.
466             */
467            ApkContainer container = new ApkContainer(inStream, encryptionParams);
468
469            try {
470                /*
471                 * We copy the source package file to a temp file and then
472                 * rename it to the destination file in order to eliminate a
473                 * window where the package directory scanner notices the new
474                 * package file but it's not completely copied yet.
475                 */
476                copyToFile(container.getInputStream(), outStream);
477
478                if (!container.isAuthenticated()) {
479                    throw new DigestException();
480                }
481            } catch (GeneralSecurityException e) {
482                throw new DigestException("A problem occured copying the file.");
483            }
484        } finally {
485            IoUtils.closeQuietly(inStream);
486        }
487    }
488
489    private static class ApkContainer {
490        private static final int MAX_AUTHENTICATED_DATA_SIZE = 16384;
491
492        private final InputStream mInStream;
493
494        private MacAuthenticatedInputStream mAuthenticatedStream;
495
496        private byte[] mTag;
497
498        public ApkContainer(InputStream inStream, ContainerEncryptionParams encryptionParams)
499                throws IOException {
500            if (encryptionParams == null) {
501                mInStream = inStream;
502            } else {
503                mInStream = getDecryptedStream(inStream, encryptionParams);
504                mTag = encryptionParams.getMacTag();
505            }
506        }
507
508        public boolean isAuthenticated() {
509            if (mAuthenticatedStream == null) {
510                return true;
511            }
512
513            return mAuthenticatedStream.isTagEqual(mTag);
514        }
515
516        private Mac getMacInstance(ContainerEncryptionParams encryptionParams) throws IOException {
517            final Mac m;
518            try {
519                final String macAlgo = encryptionParams.getMacAlgorithm();
520
521                if (macAlgo != null) {
522                    m = Mac.getInstance(macAlgo);
523                    m.init(encryptionParams.getMacKey(), encryptionParams.getMacSpec());
524                } else {
525                    m = null;
526                }
527
528                return m;
529            } catch (NoSuchAlgorithmException e) {
530                throw new IOException(e);
531            } catch (InvalidKeyException e) {
532                throw new IOException(e);
533            } catch (InvalidAlgorithmParameterException e) {
534                throw new IOException(e);
535            }
536        }
537
538        public InputStream getInputStream() {
539            return mInStream;
540        }
541
542        private InputStream getDecryptedStream(InputStream inStream,
543                ContainerEncryptionParams encryptionParams) throws IOException {
544            final Cipher c;
545            try {
546                c = Cipher.getInstance(encryptionParams.getEncryptionAlgorithm());
547                c.init(Cipher.DECRYPT_MODE, encryptionParams.getEncryptionKey(),
548                        encryptionParams.getEncryptionSpec());
549            } catch (NoSuchAlgorithmException e) {
550                throw new IOException(e);
551            } catch (NoSuchPaddingException e) {
552                throw new IOException(e);
553            } catch (InvalidKeyException e) {
554                throw new IOException(e);
555            } catch (InvalidAlgorithmParameterException e) {
556                throw new IOException(e);
557            }
558
559            final long encStart = encryptionParams.getEncryptedDataStart();
560            final long end = encryptionParams.getDataEnd();
561            if (end < encStart) {
562                throw new IOException("end <= encStart");
563            }
564
565            final Mac mac = getMacInstance(encryptionParams);
566            if (mac != null) {
567                final long macStart = encryptionParams.getAuthenticatedDataStart();
568                if (macStart >= Integer.MAX_VALUE) {
569                    throw new IOException("macStart >= Integer.MAX_VALUE");
570                }
571
572                final long furtherOffset;
573                if (macStart >= 0 && encStart >= 0 && macStart < encStart) {
574                    /*
575                     * If there is authenticated data at the beginning, read
576                     * that into our MAC first.
577                     */
578                    final long authenticatedLengthLong = encStart - macStart;
579                    if (authenticatedLengthLong > MAX_AUTHENTICATED_DATA_SIZE) {
580                        throw new IOException("authenticated data is too long");
581                    }
582                    final int authenticatedLength = (int) authenticatedLengthLong;
583
584                    final byte[] authenticatedData = new byte[(int) authenticatedLength];
585
586                    Streams.readFully(inStream, authenticatedData, (int) macStart,
587                            authenticatedLength);
588                    mac.update(authenticatedData, 0, authenticatedLength);
589
590                    furtherOffset = 0;
591                } else {
592                    /*
593                     * No authenticated data at the beginning. Just skip the
594                     * required number of bytes to the beginning of the stream.
595                     */
596                    if (encStart > 0) {
597                        furtherOffset = encStart;
598                    } else {
599                        furtherOffset = 0;
600                    }
601                }
602
603                /*
604                 * If there is data at the end of the stream we want to ignore,
605                 * wrap this in a LimitedLengthInputStream.
606                 */
607                if (furtherOffset >= 0 && end > furtherOffset) {
608                    inStream = new LimitedLengthInputStream(inStream, furtherOffset, end - encStart);
609                } else if (furtherOffset > 0) {
610                    inStream.skip(furtherOffset);
611                }
612
613                mAuthenticatedStream = new MacAuthenticatedInputStream(inStream, mac);
614
615                inStream = mAuthenticatedStream;
616            } else {
617                if (encStart >= 0) {
618                    if (end > encStart) {
619                        inStream = new LimitedLengthInputStream(inStream, encStart, end - encStart);
620                    } else {
621                        inStream.skip(encStart);
622                    }
623                }
624            }
625
626            return new CipherInputStream(inStream, c);
627        }
628
629    }
630
631    private static final int PREFER_INTERNAL = 1;
632    private static final int PREFER_EXTERNAL = 2;
633
634    private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
635            long threshold) {
636        int prefer;
637        boolean checkBoth = false;
638
639        final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
640
641        check_inner : {
642            /*
643             * Explicit install flags should override the manifest settings.
644             */
645            if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
646                prefer = PREFER_INTERNAL;
647                break check_inner;
648            } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
649                prefer = PREFER_EXTERNAL;
650                break check_inner;
651            }
652
653            /* No install flags. Check for manifest option. */
654            if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
655                prefer = PREFER_INTERNAL;
656                break check_inner;
657            } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
658                prefer = PREFER_EXTERNAL;
659                checkBoth = true;
660                break check_inner;
661            } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
662                // We default to preferring internal storage.
663                prefer = PREFER_INTERNAL;
664                checkBoth = true;
665                break check_inner;
666            }
667
668            // Pick user preference
669            int installPreference = Settings.System.getInt(getApplicationContext()
670                    .getContentResolver(),
671                    Settings.Secure.DEFAULT_INSTALL_LOCATION,
672                    PackageHelper.APP_INSTALL_AUTO);
673            if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
674                prefer = PREFER_INTERNAL;
675                break check_inner;
676            } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
677                prefer = PREFER_EXTERNAL;
678                break check_inner;
679            }
680
681            /*
682             * Fall back to default policy of internal-only if nothing else is
683             * specified.
684             */
685            prefer = PREFER_INTERNAL;
686        }
687
688        final boolean emulated = Environment.isExternalStorageEmulated();
689
690        final File apkFile = new File(archiveFilePath);
691
692        boolean fitsOnInternal = false;
693        if (checkBoth || prefer == PREFER_INTERNAL) {
694            try {
695                fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
696            } catch (IOException e) {
697                return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
698            }
699        }
700
701        boolean fitsOnSd = false;
702        if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
703            try {
704                fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked);
705            } catch (IOException e) {
706                return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
707            }
708        }
709
710        if (prefer == PREFER_INTERNAL) {
711            if (fitsOnInternal) {
712                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
713            }
714        } else if (!emulated && prefer == PREFER_EXTERNAL) {
715            if (fitsOnSd) {
716                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
717            }
718        }
719
720        if (checkBoth) {
721            if (fitsOnInternal) {
722                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
723            } else if (!emulated && fitsOnSd) {
724                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
725            }
726        }
727
728        /*
729         * If they requested to be on the external media by default, return that
730         * the media was unavailable. Otherwise, indicate there was insufficient
731         * storage space available.
732         */
733        if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
734                && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
735            return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
736        } else {
737            return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
738        }
739    }
740
741    /**
742     * Measure a file to see if it fits within the free space threshold.
743     *
744     * @param apkFile file to check
745     * @param threshold byte threshold to compare against
746     * @return true if file fits under threshold
747     * @throws FileNotFoundException when APK does not exist
748     */
749    private boolean isUnderInternalThreshold(File apkFile, boolean isForwardLocked, long threshold)
750            throws IOException {
751        long size = apkFile.length();
752        if (size == 0 && !apkFile.exists()) {
753            throw new FileNotFoundException();
754        }
755
756        if (isForwardLocked) {
757            size += PackageHelper.extractPublicFiles(apkFile.getAbsolutePath(), null);
758        }
759
760        final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
761        final long availInternalSize = (long) internalStats.getAvailableBlocks()
762                * (long) internalStats.getBlockSize();
763
764        return (availInternalSize - size) > threshold;
765    }
766
767
768    /**
769     * Measure a file to see if it fits in the external free space.
770     *
771     * @param apkFile file to check
772     * @return true if file fits
773     * @throws IOException when file does not exist
774     */
775    private boolean isUnderExternalThreshold(File apkFile, boolean isForwardLocked)
776            throws IOException {
777        if (Environment.isExternalStorageEmulated()) {
778            return false;
779        }
780
781        final int sizeMb = calculateContainerSize(apkFile, isForwardLocked);
782
783        final int availSdMb;
784        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
785            final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
786            final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
787            availSdMb = sdStats.getAvailableBlocks() * blocksToMb;
788        } else {
789            availSdMb = -1;
790        }
791
792        return availSdMb > sizeMb;
793    }
794
795    /**
796     * Calculate the container size for an APK. Takes into account the
797     *
798     * @param apkFile file from which to calculate size
799     * @return size in megabytes (2^20 bytes)
800     * @throws IOException when there is a problem reading the file
801     */
802    private int calculateContainerSize(File apkFile, boolean forwardLocked) throws IOException {
803        // Calculate size of container needed to hold base APK.
804        long sizeBytes = apkFile.length();
805        if (sizeBytes == 0 && !apkFile.exists()) {
806            throw new FileNotFoundException();
807        }
808
809        // Check all the native files that need to be copied and add that to the
810        // container size.
811        sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile);
812
813        if (forwardLocked) {
814            sizeBytes += PackageHelper.extractPublicFiles(apkFile.getPath(), null);
815        }
816
817        int sizeMb = (int) (sizeBytes >> 20);
818        if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) {
819            sizeMb++;
820        }
821
822        /*
823         * Add buffer size because we don't have a good way to determine the
824         * real FAT size. Your FAT size varies with how many directory entries
825         * you need, how big the whole filesystem is, and other such headaches.
826         */
827        sizeMb++;
828
829        return sizeMb;
830    }
831}
832