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