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