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