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