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