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