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