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