OSDN Git Service

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