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