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