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