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 com.android.internal.app.IMediaContainerService; 20 import com.android.internal.content.NativeLibraryHelper; 21 import com.android.internal.content.PackageHelper; 22 23 import android.app.IntentService; 24 import android.content.Intent; 25 import android.content.pm.IPackageManager; 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.FileUtils; 35 import android.os.IBinder; 36 import android.os.ParcelFileDescriptor; 37 import android.os.Process; 38 import android.os.RemoteException; 39 import android.os.ServiceManager; 40 import android.os.StatFs; 41 import android.provider.Settings; 42 import android.util.DisplayMetrics; 43 import android.util.Slog; 44 45 import java.io.BufferedInputStream; 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.FileNotFoundException; 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.io.OutputStream; 52 53 /* 54 * This service copies a downloaded apk to a file passed in as 55 * a ParcelFileDescriptor or to a newly created container specified 56 * by parameters. The DownloadManager gives access to this process 57 * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER 58 * permission to access apks downloaded via the download manager. 59 */ 60 public class DefaultContainerService extends IntentService { 61 private static final String TAG = "DefContainer"; 62 private static final boolean localLOGV = true; 63 64 private static final String LIB_DIR_NAME = "lib"; 65 66 private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() { 67 /* 68 * Creates a new container and copies resource there. 69 * @param paackageURI the uri of resource to be copied. Can be either 70 * a content uri or a file uri 71 * @param cid the id of the secure container that should 72 * be used for creating a secure container into which the resource 73 * will be copied. 74 * @param key Refers to key used for encrypting the secure container 75 * @param resFileName Name of the target resource file(relative to newly 76 * created secure container) 77 * @return Returns the new cache path where the resource has been copied into 78 * 79 */ 80 public String copyResourceToContainer(final Uri packageURI, 81 final String cid, 82 final String key, final String resFileName) { 83 if (packageURI == null || cid == null) { 84 return null; 85 } 86 return copyResourceInner(packageURI, cid, key, resFileName); 87 } 88 89 /* 90 * Copy specified resource to output stream 91 * @param packageURI the uri of resource to be copied. Should be a file 92 * uri 93 * @param outStream Remote file descriptor to be used for copying 94 * @return returns status code according to those in {@link 95 * PackageManager} 96 */ 97 public int copyResource(final Uri packageURI, ParcelFileDescriptor outStream) { 98 if (packageURI == null || outStream == null) { 99 return PackageManager.INSTALL_FAILED_INVALID_URI; 100 } 101 102 ParcelFileDescriptor.AutoCloseOutputStream autoOut 103 = new ParcelFileDescriptor.AutoCloseOutputStream(outStream); 104 105 try { 106 copyFile(packageURI, autoOut); 107 return PackageManager.INSTALL_SUCCEEDED; 108 } catch (FileNotFoundException e) { 109 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: " 110 + e.getMessage()); 111 return PackageManager.INSTALL_FAILED_INVALID_URI; 112 } catch (IOException e) { 113 Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: " 114 + e.getMessage()); 115 return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; 116 } 117 } 118 119 /* 120 * Determine the recommended install location for package 121 * specified by file uri location. 122 * @param fileUri the uri of resource to be copied. Should be a 123 * file uri 124 * @return Returns PackageInfoLite object containing 125 * the package info and recommended app location. 126 */ 127 public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags, long threshold) { 128 PackageInfoLite ret = new PackageInfoLite(); 129 if (fileUri == null) { 130 Slog.i(TAG, "Invalid package uri " + fileUri); 131 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK; 132 return ret; 133 } 134 String scheme = fileUri.getScheme(); 135 if (scheme != null && !scheme.equals("file")) { 136 Slog.w(TAG, "Falling back to installing on internal storage only"); 137 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL; 138 return ret; 139 } 140 String archiveFilePath = fileUri.getPath(); 141 DisplayMetrics metrics = new DisplayMetrics(); 142 metrics.setToDefaults(); 143 144 PackageParser.PackageLite pkg = PackageParser.parsePackageLite(archiveFilePath, 0); 145 if (pkg == null) { 146 Slog.w(TAG, "Failed to parse package"); 147 148 final File apkFile = new File(archiveFilePath); 149 if (!apkFile.exists()) { 150 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI; 151 } else { 152 ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK; 153 } 154 155 return ret; 156 } 157 ret.packageName = pkg.packageName; 158 ret.installLocation = pkg.installLocation; 159 ret.verifiers = pkg.verifiers; 160 161 ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, 162 archiveFilePath, flags, threshold); 163 164 return ret; 165 } 166 167 @Override 168 public boolean checkInternalFreeStorage(Uri packageUri, long threshold) 169 throws RemoteException { 170 final File apkFile = new File(packageUri.getPath()); 171 try { 172 return isUnderInternalThreshold(apkFile, threshold); 173 } catch (FileNotFoundException e) { 174 return true; 175 } 176 } 177 178 @Override 179 public boolean checkExternalFreeStorage(Uri packageUri) throws RemoteException { 180 final File apkFile = new File(packageUri.getPath()); 181 try { 182 return isUnderExternalThreshold(apkFile); 183 } catch (FileNotFoundException e) { 184 return true; 185 } 186 } 187 188 public ObbInfo getObbInfo(String filename) { 189 try { 190 return ObbScanner.getObbInfo(filename); 191 } catch (IOException e) { 192 Slog.d(TAG, "Couldn't get OBB info for " + filename); 193 return null; 194 } 195 } 196 197 @Override 198 public long calculateDirectorySize(String path) throws RemoteException { 199 final File directory = new File(path); 200 if (directory.exists() && directory.isDirectory()) { 201 return MeasurementUtils.measureDirectory(path); 202 } else { 203 return 0L; 204 } 205 } 206 }; 207 208 public DefaultContainerService() { 209 super("DefaultContainerService"); 210 setIntentRedelivery(true); 211 } 212 213 @Override 214 protected void onHandleIntent(Intent intent) { 215 if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) { 216 IPackageManager pm = IPackageManager.Stub.asInterface( 217 ServiceManager.getService("package")); 218 String pkg = null; 219 try { 220 while ((pkg=pm.nextPackageToClean(pkg)) != null) { 221 eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg)); 222 eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg)); 223 eraseFiles(Environment.getExternalStorageAppObbDirectory(pkg)); 224 } 225 } catch (RemoteException e) { 226 } 227 } 228 } 229 230 void eraseFiles(File path) { 231 if (path.isDirectory()) { 232 String[] files = path.list(); 233 if (files != null) { 234 for (String file : files) { 235 eraseFiles(new File(path, file)); 236 } 237 } 238 } 239 path.delete(); 240 } 241 242 public IBinder onBind(Intent intent) { 243 return mBinder; 244 } 245 246 private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) { 247 // Make sure the sdcard is mounted. 248 String status = Environment.getExternalStorageState(); 249 if (!status.equals(Environment.MEDIA_MOUNTED)) { 250 Slog.w(TAG, "Make sure sdcard is mounted."); 251 return null; 252 } 253 254 // The .apk file 255 String codePath = packageURI.getPath(); 256 File codeFile = new File(codePath); 257 258 // Calculate size of container needed to hold base APK. 259 int sizeMb; 260 try { 261 sizeMb = calculateContainerSize(codeFile); 262 } catch (FileNotFoundException e) { 263 Slog.w(TAG, "File does not exist when trying to copy " + codeFile.getPath()); 264 return null; 265 } 266 267 // Create new container 268 final String newCachePath; 269 if ((newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid())) == null) { 270 Slog.e(TAG, "Failed to create container " + newCid); 271 return null; 272 } 273 274 if (localLOGV) { 275 Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath); 276 } 277 278 final File resFile = new File(newCachePath, resFileName); 279 if (FileUtils.copyFile(new File(codePath), resFile)) { 280 if (localLOGV) { 281 Slog.i(TAG, "Copied " + codePath + " to " + resFile); 282 } 283 } else { 284 Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile); 285 // Clean up container 286 PackageHelper.destroySdDir(newCid); 287 return null; 288 } 289 290 final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME); 291 if (sharedLibraryDir.mkdir()) { 292 int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir); 293 if (ret != PackageManager.INSTALL_SUCCEEDED) { 294 Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath()); 295 PackageHelper.destroySdDir(newCid); 296 return null; 297 } 298 } else { 299 Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath()); 300 PackageHelper.destroySdDir(newCid); 301 return null; 302 } 303 304 if (!PackageHelper.finalizeSdDir(newCid)) { 305 Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath); 306 // Clean up container 307 PackageHelper.destroySdDir(newCid); 308 return null; 309 } 310 311 if (localLOGV) { 312 Slog.i(TAG, "Finalized container " + newCid); 313 } 314 315 if (PackageHelper.isContainerMounted(newCid)) { 316 if (localLOGV) { 317 Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath); 318 } 319 320 // Force a gc to avoid being killed. 321 Runtime.getRuntime().gc(); 322 PackageHelper.unMountSdDir(newCid); 323 } else { 324 if (localLOGV) { 325 Slog.i(TAG, "Container " + newCid + " not mounted"); 326 } 327 } 328 329 return newCachePath; 330 } 331 332 private static void copyToFile(InputStream inputStream, OutputStream out) throws IOException { 333 byte[] buffer = new byte[16384]; 334 int bytesRead; 335 while ((bytesRead = inputStream.read(buffer)) >= 0) { 336 out.write(buffer, 0, bytesRead); 337 } 338 } 339 340 private static void copyToFile(File srcFile, OutputStream out) 341 throws FileNotFoundException, IOException { 342 InputStream inputStream = new BufferedInputStream(new FileInputStream(srcFile)); 343 try { 344 copyToFile(inputStream, out); 345 } finally { 346 try { inputStream.close(); } catch (IOException e) {} 347 } 348 } 349 350 private void copyFile(Uri pPackageURI, OutputStream outStream) throws FileNotFoundException, 351 IOException { 352 String scheme = pPackageURI.getScheme(); 353 if (scheme == null || scheme.equals("file")) { 354 final File srcPackageFile = new File(pPackageURI.getPath()); 355 // We copy the source package file to a temp file and then rename it to the 356 // destination file in order to eliminate a window where the package directory 357 // scanner notices the new package file but it's not completely copied yet. 358 copyToFile(srcPackageFile, outStream); 359 } else if (scheme.equals("content")) { 360 ParcelFileDescriptor fd = null; 361 try { 362 fd = getContentResolver().openFileDescriptor(pPackageURI, "r"); 363 } catch (FileNotFoundException e) { 364 Slog.e(TAG, "Couldn't open file descriptor from download service. " 365 + "Failed with exception " + e); 366 throw e; 367 } 368 369 if (fd == null) { 370 Slog.e(TAG, "Provider returned no file descriptor for " + pPackageURI.toString()); 371 throw new FileNotFoundException("provider returned no file descriptor"); 372 } else { 373 if (localLOGV) { 374 Slog.i(TAG, "Opened file descriptor from download service."); 375 } 376 ParcelFileDescriptor.AutoCloseInputStream dlStream 377 = new ParcelFileDescriptor.AutoCloseInputStream(fd); 378 379 // We copy the source package file to a temp file and then rename it to the 380 // destination file in order to eliminate a window where the package directory 381 // scanner notices the new package file but it's not completely 382 // copied 383 copyToFile(dlStream, outStream); 384 } 385 } else { 386 Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI); 387 throw new FileNotFoundException("Package URI is not 'file:' or 'content:'"); 388 } 389 } 390 391 private static final int PREFER_INTERNAL = 1; 392 private static final int PREFER_EXTERNAL = 2; 393 394 private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags, 395 long threshold) { 396 int prefer; 397 boolean checkBoth = false; 398 399 check_inner : { 400 /* 401 * Explicit install flags should override the manifest settings. 402 */ 403 if ((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) { 404 /* 405 * Forward-locked applications cannot be installed on SD card, 406 * so only allow checking internal storage. 407 */ 408 prefer = PREFER_INTERNAL; 409 break check_inner; 410 } else if ((flags & PackageManager.INSTALL_INTERNAL) != 0) { 411 prefer = PREFER_INTERNAL; 412 break check_inner; 413 } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) { 414 prefer = PREFER_EXTERNAL; 415 break check_inner; 416 } 417 418 /* No install flags. Check for manifest option. */ 419 if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { 420 prefer = PREFER_INTERNAL; 421 break check_inner; 422 } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) { 423 prefer = PREFER_EXTERNAL; 424 checkBoth = true; 425 break check_inner; 426 } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) { 427 // We default to preferring internal storage. 428 prefer = PREFER_INTERNAL; 429 checkBoth = true; 430 break check_inner; 431 } 432 433 // Pick user preference 434 int installPreference = Settings.System.getInt(getApplicationContext() 435 .getContentResolver(), 436 Settings.Secure.DEFAULT_INSTALL_LOCATION, 437 PackageHelper.APP_INSTALL_AUTO); 438 if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) { 439 prefer = PREFER_INTERNAL; 440 break check_inner; 441 } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) { 442 prefer = PREFER_EXTERNAL; 443 break check_inner; 444 } 445 446 /* 447 * Fall back to default policy of internal-only if nothing else is 448 * specified. 449 */ 450 prefer = PREFER_INTERNAL; 451 } 452 453 final boolean emulated = Environment.isExternalStorageEmulated(); 454 455 final File apkFile = new File(archiveFilePath); 456 457 boolean fitsOnInternal = false; 458 if (checkBoth || prefer == PREFER_INTERNAL) { 459 try { 460 fitsOnInternal = isUnderInternalThreshold(apkFile, threshold); 461 } catch (FileNotFoundException e) { 462 return PackageHelper.RECOMMEND_FAILED_INVALID_URI; 463 } 464 } 465 466 boolean fitsOnSd = false; 467 if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) { 468 try { 469 fitsOnSd = isUnderExternalThreshold(apkFile); 470 } catch (FileNotFoundException e) { 471 return PackageHelper.RECOMMEND_FAILED_INVALID_URI; 472 } 473 } 474 475 if (prefer == PREFER_INTERNAL) { 476 if (fitsOnInternal) { 477 return PackageHelper.RECOMMEND_INSTALL_INTERNAL; 478 } 479 } else if (!emulated && prefer == PREFER_EXTERNAL) { 480 if (fitsOnSd) { 481 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; 482 } 483 } 484 485 if (checkBoth) { 486 if (fitsOnInternal) { 487 return PackageHelper.RECOMMEND_INSTALL_INTERNAL; 488 } else if (!emulated && fitsOnSd) { 489 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; 490 } 491 } 492 493 /* 494 * If they requested to be on the external media by default, return that 495 * the media was unavailable. Otherwise, indicate there was insufficient 496 * storage space available. 497 */ 498 if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL) 499 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { 500 return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE; 501 } else { 502 return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; 503 } 504 } 505 506 /** 507 * Measure a file to see if it fits within the free space threshold. 508 * 509 * @param apkFile file to check 510 * @param threshold byte threshold to compare against 511 * @return true if file fits under threshold 512 * @throws FileNotFoundException when APK does not exist 513 */ 514 private boolean isUnderInternalThreshold(File apkFile, long threshold) 515 throws FileNotFoundException { 516 final long size = apkFile.length(); 517 if (size == 0 && !apkFile.exists()) { 518 throw new FileNotFoundException(); 519 } 520 521 final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath()); 522 final long availInternalSize = (long) internalStats.getAvailableBlocks() 523 * (long) internalStats.getBlockSize(); 524 525 return (availInternalSize - size) > threshold; 526 } 527 528 529 /** 530 * Measure a file to see if it fits in the external free space. 531 * 532 * @param apkFile file to check 533 * @return true if file fits 534 * @throws IOException when file does not exist 535 */ 536 private boolean isUnderExternalThreshold(File apkFile) throws FileNotFoundException { 537 if (Environment.isExternalStorageEmulated()) { 538 return false; 539 } 540 541 final int sizeMb = calculateContainerSize(apkFile); 542 543 final int availSdMb; 544 if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { 545 final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath()); 546 final int blocksToMb = (1 << 20) / sdStats.getBlockSize(); 547 availSdMb = sdStats.getAvailableBlocks() * blocksToMb; 548 } else { 549 availSdMb = -1; 550 } 551 552 return availSdMb > sizeMb; 553 } 554 555 /** 556 * Calculate the container size for an APK. Takes into account the 557 * 558 * @param apkFile file from which to calculate size 559 * @return size in megabytes (2^20 bytes) 560 * @throws FileNotFoundException when file does not exist 561 */ 562 private int calculateContainerSize(File apkFile) throws FileNotFoundException { 563 // Calculate size of container needed to hold base APK. 564 long sizeBytes = apkFile.length(); 565 if (sizeBytes == 0 && !apkFile.exists()) { 566 throw new FileNotFoundException(); 567 } 568 569 // Check all the native files that need to be copied and add that to the 570 // container size. 571 sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile); 572 573 int sizeMb = (int) (sizeBytes >> 20); 574 if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) { 575 sizeMb++; 576 } 577 578 /* 579 * Add buffer size because we don't have a good way to determine the 580 * real FAT size. Your FAT size varies with how many directory entries 581 * you need, how big the whole filesystem is, and other such headaches. 582 */ 583 sizeMb++; 584 585 return sizeMb; 586 } 587 } 588