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