1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.defcontainer; 18 19import android.app.IntentService; 20import android.content.Intent; 21import android.content.pm.ContainerEncryptionParams; 22import android.content.pm.IPackageManager; 23import android.content.pm.LimitedLengthInputStream; 24import android.content.pm.MacAuthenticatedInputStream; 25import android.content.pm.PackageCleanItem; 26import android.content.pm.PackageInfo; 27import android.content.pm.PackageInfoLite; 28import android.content.pm.PackageManager; 29import android.content.pm.PackageParser; 30import android.content.res.ObbInfo; 31import android.content.res.ObbScanner; 32import android.net.Uri; 33import android.os.Environment; 34import android.os.Environment.UserEnvironment; 35import android.os.FileUtils; 36import android.os.IBinder; 37import android.os.ParcelFileDescriptor; 38import android.os.Process; 39import android.os.RemoteException; 40import android.os.ServiceManager; 41import android.os.StatFs; 42import android.os.SystemClock; 43import android.provider.Settings; 44import android.util.DisplayMetrics; 45import android.util.Log; 46import android.util.Slog; 47 48import com.android.internal.app.IMediaContainerService; 49import com.android.internal.content.NativeLibraryHelper; 50import com.android.internal.content.PackageHelper; 51 52import java.io.BufferedInputStream; 53import java.io.File; 54import java.io.FileInputStream; 55import java.io.FileNotFoundException; 56import java.io.IOException; 57import java.io.InputStream; 58import java.io.OutputStream; 59import java.security.DigestException; 60import java.security.GeneralSecurityException; 61import java.security.InvalidAlgorithmParameterException; 62import java.security.InvalidKeyException; 63import java.security.NoSuchAlgorithmException; 64 65import javax.crypto.Cipher; 66import javax.crypto.CipherInputStream; 67import javax.crypto.Mac; 68import javax.crypto.NoSuchPaddingException; 69 70import libcore.io.ErrnoException; 71import libcore.io.IoUtils; 72import libcore.io.Libcore; 73import libcore.io.Streams; 74import libcore.io.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 */ 83public class DefaultContainerService extends IntentService { 84 private static final String TAG = "DefContainer"; 85 private static final boolean localLOGV = false; 86 87 private static final String LIB_DIR_NAME = "lib"; 88 89 private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() { 90 /** 91 * Creates a new container and copies resource there. 92 * @param paackageURI the uri of resource to be copied. Can be either 93 * a content uri or a file uri 94 * @param cid the id of the secure container that should 95 * be used for creating a secure container into which the resource 96 * will be copied. 97 * @param key Refers to key used for encrypting the secure container 98 * @param resFileName Name of the target resource file(relative to newly 99 * created secure container) 100 * @return Returns the new cache path where the resource has been copied into 101 * 102 */ 103 public String copyResourceToContainer(final Uri packageURI, final String cid, 104 final String key, final String resFileName, final String publicResFileName, 105 boolean isExternal, boolean isForwardLocked) { 106 if (packageURI == null || cid == null) { 107 return null; 108 } 109 110 return copyResourceInner(packageURI, cid, key, resFileName, publicResFileName, 111 isExternal, isForwardLocked); 112 } 113 114 /** 115 * Copy specified resource to output stream 116 * 117 * @param packageURI the uri of resource to be copied. Should be a file 118 * uri 119 * @param encryptionParams parameters describing the encryption used for 120 * this file 121 * @param outStream Remote file descriptor to be used for copying 122 * @return returns status code according to those in 123 * {@link PackageManager} 124 */ 125 public int copyResource(final Uri packageURI, ContainerEncryptionParams encryptionParams, 126 ParcelFileDescriptor outStream) { 127 if (packageURI == null || outStream == null) { 128 return PackageManager.INSTALL_FAILED_INVALID_URI; 129 } 130 131 ParcelFileDescriptor.AutoCloseOutputStream autoOut 132 = new ParcelFileDescriptor.AutoCloseOutputStream(outStream); 133 134 try { 135 copyFile(packageURI, autoOut, encryptionParams); 136 return PackageManager.INSTALL_SUCCEEDED; 137 } catch (FileNotFoundException e) { 138 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: " 139 + e.getMessage()); 140 return PackageManager.INSTALL_FAILED_INVALID_URI; 141 } catch (IOException e) { 142 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: " 143 + e.getMessage()); 144 return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; 145 } catch (DigestException e) { 146 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " Security: " 147 + e.getMessage()); 148 return PackageManager.INSTALL_FAILED_INVALID_APK; 149 } 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} 856