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