1 /* 2 * Copyright (C) 2009 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.internal.content; 18 19 import static android.net.TrafficStats.MB_IN_BYTES; 20 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageInfo; 24 import android.content.pm.PackageManager; 25 import android.content.pm.PackageManager.NameNotFoundException; 26 import android.content.pm.PackageParser.PackageLite; 27 import android.os.Environment; 28 import android.os.FileUtils; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.os.ServiceManager; 32 import android.os.storage.IMountService; 33 import android.os.storage.StorageManager; 34 import android.os.storage.StorageResultCode; 35 import android.os.storage.StorageVolume; 36 import android.os.storage.VolumeInfo; 37 import android.provider.Settings; 38 import android.util.ArraySet; 39 import android.util.Log; 40 41 import libcore.io.IoUtils; 42 43 import java.io.File; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.util.Collections; 48 import java.util.Objects; 49 import java.util.zip.ZipEntry; 50 import java.util.zip.ZipFile; 51 import java.util.zip.ZipOutputStream; 52 53 /** 54 * Constants used internally between the PackageManager 55 * and media container service transports. 56 * Some utility methods to invoke MountService api. 57 */ 58 public class PackageHelper { 59 public static final int RECOMMEND_INSTALL_INTERNAL = 1; 60 public static final int RECOMMEND_INSTALL_EXTERNAL = 2; 61 public static final int RECOMMEND_INSTALL_EPHEMERAL = 3; 62 public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1; 63 public static final int RECOMMEND_FAILED_INVALID_APK = -2; 64 public static final int RECOMMEND_FAILED_INVALID_LOCATION = -3; 65 public static final int RECOMMEND_FAILED_ALREADY_EXISTS = -4; 66 public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5; 67 public static final int RECOMMEND_FAILED_INVALID_URI = -6; 68 public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7; 69 70 private static final boolean localLOGV = false; 71 private static final String TAG = "PackageHelper"; 72 // App installation location settings values 73 public static final int APP_INSTALL_AUTO = 0; 74 public static final int APP_INSTALL_INTERNAL = 1; 75 public static final int APP_INSTALL_EXTERNAL = 2; 76 getMountService()77 public static IMountService getMountService() throws RemoteException { 78 IBinder service = ServiceManager.getService("mount"); 79 if (service != null) { 80 return IMountService.Stub.asInterface(service); 81 } else { 82 Log.e(TAG, "Can't get mount service"); 83 throw new RemoteException("Could not contact mount service"); 84 } 85 } 86 createSdDir(long sizeBytes, String cid, String sdEncKey, int uid, boolean isExternal)87 public static String createSdDir(long sizeBytes, String cid, String sdEncKey, int uid, 88 boolean isExternal) { 89 // Round up to nearest MB, plus another MB for filesystem overhead 90 final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1; 91 try { 92 IMountService mountService = getMountService(); 93 94 if (localLOGV) 95 Log.i(TAG, "Size of container " + sizeMb + " MB"); 96 97 int rc = mountService.createSecureContainer(cid, sizeMb, "ext4", sdEncKey, uid, 98 isExternal); 99 if (rc != StorageResultCode.OperationSucceeded) { 100 Log.e(TAG, "Failed to create secure container " + cid); 101 return null; 102 } 103 String cachePath = mountService.getSecureContainerPath(cid); 104 if (localLOGV) Log.i(TAG, "Created secure container " + cid + 105 " at " + cachePath); 106 return cachePath; 107 } catch (RemoteException e) { 108 Log.e(TAG, "MountService running?"); 109 } 110 return null; 111 } 112 resizeSdDir(long sizeBytes, String cid, String sdEncKey)113 public static boolean resizeSdDir(long sizeBytes, String cid, String sdEncKey) { 114 // Round up to nearest MB, plus another MB for filesystem overhead 115 final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1; 116 try { 117 IMountService mountService = getMountService(); 118 int rc = mountService.resizeSecureContainer(cid, sizeMb, sdEncKey); 119 if (rc == StorageResultCode.OperationSucceeded) { 120 return true; 121 } 122 } catch (RemoteException e) { 123 Log.e(TAG, "MountService running?"); 124 } 125 Log.e(TAG, "Failed to create secure container " + cid); 126 return false; 127 } 128 mountSdDir(String cid, String key, int ownerUid)129 public static String mountSdDir(String cid, String key, int ownerUid) { 130 return mountSdDir(cid, key, ownerUid, true); 131 } 132 mountSdDir(String cid, String key, int ownerUid, boolean readOnly)133 public static String mountSdDir(String cid, String key, int ownerUid, boolean readOnly) { 134 try { 135 int rc = getMountService().mountSecureContainer(cid, key, ownerUid, readOnly); 136 if (rc != StorageResultCode.OperationSucceeded) { 137 Log.i(TAG, "Failed to mount container " + cid + " rc : " + rc); 138 return null; 139 } 140 return getMountService().getSecureContainerPath(cid); 141 } catch (RemoteException e) { 142 Log.e(TAG, "MountService running?"); 143 } 144 return null; 145 } 146 unMountSdDir(String cid)147 public static boolean unMountSdDir(String cid) { 148 try { 149 int rc = getMountService().unmountSecureContainer(cid, true); 150 if (rc != StorageResultCode.OperationSucceeded) { 151 Log.e(TAG, "Failed to unmount " + cid + " with rc " + rc); 152 return false; 153 } 154 return true; 155 } catch (RemoteException e) { 156 Log.e(TAG, "MountService running?"); 157 } 158 return false; 159 } 160 renameSdDir(String oldId, String newId)161 public static boolean renameSdDir(String oldId, String newId) { 162 try { 163 int rc = getMountService().renameSecureContainer(oldId, newId); 164 if (rc != StorageResultCode.OperationSucceeded) { 165 Log.e(TAG, "Failed to rename " + oldId + " to " + 166 newId + "with rc " + rc); 167 return false; 168 } 169 return true; 170 } catch (RemoteException e) { 171 Log.i(TAG, "Failed ot rename " + oldId + " to " + newId + 172 " with exception : " + e); 173 } 174 return false; 175 } 176 getSdDir(String cid)177 public static String getSdDir(String cid) { 178 try { 179 return getMountService().getSecureContainerPath(cid); 180 } catch (RemoteException e) { 181 Log.e(TAG, "Failed to get container path for " + cid + 182 " with exception " + e); 183 } 184 return null; 185 } 186 getSdFilesystem(String cid)187 public static String getSdFilesystem(String cid) { 188 try { 189 return getMountService().getSecureContainerFilesystemPath(cid); 190 } catch (RemoteException e) { 191 Log.e(TAG, "Failed to get container path for " + cid + 192 " with exception " + e); 193 } 194 return null; 195 } 196 finalizeSdDir(String cid)197 public static boolean finalizeSdDir(String cid) { 198 try { 199 int rc = getMountService().finalizeSecureContainer(cid); 200 if (rc != StorageResultCode.OperationSucceeded) { 201 Log.i(TAG, "Failed to finalize container " + cid); 202 return false; 203 } 204 return true; 205 } catch (RemoteException e) { 206 Log.e(TAG, "Failed to finalize container " + cid + 207 " with exception " + e); 208 } 209 return false; 210 } 211 destroySdDir(String cid)212 public static boolean destroySdDir(String cid) { 213 try { 214 if (localLOGV) Log.i(TAG, "Forcibly destroying container " + cid); 215 int rc = getMountService().destroySecureContainer(cid, true); 216 if (rc != StorageResultCode.OperationSucceeded) { 217 Log.i(TAG, "Failed to destroy container " + cid); 218 return false; 219 } 220 return true; 221 } catch (RemoteException e) { 222 Log.e(TAG, "Failed to destroy container " + cid + 223 " with exception " + e); 224 } 225 return false; 226 } 227 getSecureContainerList()228 public static String[] getSecureContainerList() { 229 try { 230 return getMountService().getSecureContainerList(); 231 } catch (RemoteException e) { 232 Log.e(TAG, "Failed to get secure container list with exception" + 233 e); 234 } 235 return null; 236 } 237 isContainerMounted(String cid)238 public static boolean isContainerMounted(String cid) { 239 try { 240 return getMountService().isSecureContainerMounted(cid); 241 } catch (RemoteException e) { 242 Log.e(TAG, "Failed to find out if container " + cid + " mounted"); 243 } 244 return false; 245 } 246 247 /** 248 * Extract public files for the single given APK. 249 */ extractPublicFiles(File apkFile, File publicZipFile)250 public static long extractPublicFiles(File apkFile, File publicZipFile) 251 throws IOException { 252 final FileOutputStream fstr; 253 final ZipOutputStream publicZipOutStream; 254 255 if (publicZipFile == null) { 256 fstr = null; 257 publicZipOutStream = null; 258 } else { 259 fstr = new FileOutputStream(publicZipFile); 260 publicZipOutStream = new ZipOutputStream(fstr); 261 Log.d(TAG, "Extracting " + apkFile + " to " + publicZipFile); 262 } 263 264 long size = 0L; 265 266 try { 267 final ZipFile privateZip = new ZipFile(apkFile.getAbsolutePath()); 268 try { 269 // Copy manifest, resources.arsc and res directory to public zip 270 for (final ZipEntry zipEntry : Collections.list(privateZip.entries())) { 271 final String zipEntryName = zipEntry.getName(); 272 if ("AndroidManifest.xml".equals(zipEntryName) 273 || "resources.arsc".equals(zipEntryName) 274 || zipEntryName.startsWith("res/")) { 275 size += zipEntry.getSize(); 276 if (publicZipFile != null) { 277 copyZipEntry(zipEntry, privateZip, publicZipOutStream); 278 } 279 } 280 } 281 } finally { 282 try { privateZip.close(); } catch (IOException e) {} 283 } 284 285 if (publicZipFile != null) { 286 publicZipOutStream.finish(); 287 publicZipOutStream.flush(); 288 FileUtils.sync(fstr); 289 publicZipOutStream.close(); 290 FileUtils.setPermissions(publicZipFile.getAbsolutePath(), FileUtils.S_IRUSR 291 | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IROTH, -1, -1); 292 } 293 } finally { 294 IoUtils.closeQuietly(publicZipOutStream); 295 } 296 297 return size; 298 } 299 copyZipEntry(ZipEntry zipEntry, ZipFile inZipFile, ZipOutputStream outZipStream)300 private static void copyZipEntry(ZipEntry zipEntry, ZipFile inZipFile, 301 ZipOutputStream outZipStream) throws IOException { 302 byte[] buffer = new byte[4096]; 303 int num; 304 305 ZipEntry newEntry; 306 if (zipEntry.getMethod() == ZipEntry.STORED) { 307 // Preserve the STORED method of the input entry. 308 newEntry = new ZipEntry(zipEntry); 309 } else { 310 // Create a new entry so that the compressed len is recomputed. 311 newEntry = new ZipEntry(zipEntry.getName()); 312 } 313 outZipStream.putNextEntry(newEntry); 314 315 final InputStream data = inZipFile.getInputStream(zipEntry); 316 try { 317 while ((num = data.read(buffer)) > 0) { 318 outZipStream.write(buffer, 0, num); 319 } 320 outZipStream.flush(); 321 } finally { 322 IoUtils.closeQuietly(data); 323 } 324 } 325 fixSdPermissions(String cid, int gid, String filename)326 public static boolean fixSdPermissions(String cid, int gid, String filename) { 327 try { 328 int rc = getMountService().fixPermissionsSecureContainer(cid, gid, filename); 329 if (rc != StorageResultCode.OperationSucceeded) { 330 Log.i(TAG, "Failed to fixperms container " + cid); 331 return false; 332 } 333 return true; 334 } catch (RemoteException e) { 335 Log.e(TAG, "Failed to fixperms container " + cid + " with exception " + e); 336 } 337 return false; 338 } 339 340 /** 341 * Given a requested {@link PackageInfo#installLocation} and calculated 342 * install size, pick the actual volume to install the app. Only considers 343 * internal and private volumes, and prefers to keep an existing package on 344 * its current volume. 345 * 346 * @return the {@link VolumeInfo#fsUuid} to install onto, or {@code null} 347 * for internal storage. 348 */ resolveInstallVolume(Context context, String packageName, int installLocation, long sizeBytes)349 public static String resolveInstallVolume(Context context, String packageName, 350 int installLocation, long sizeBytes) throws IOException { 351 final boolean forceAllowOnExternal = Settings.Global.getInt( 352 context.getContentResolver(), Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0; 353 // TODO: handle existing apps installed in ASEC; currently assumes 354 // they'll end up back on internal storage 355 ApplicationInfo existingInfo = null; 356 try { 357 existingInfo = context.getPackageManager().getApplicationInfo(packageName, 358 PackageManager.GET_UNINSTALLED_PACKAGES); 359 } catch (NameNotFoundException ignored) { 360 } 361 362 final StorageManager storageManager = context.getSystemService(StorageManager.class); 363 final boolean fitsOnInternal = fitsOnInternal(context, sizeBytes); 364 365 final ArraySet<String> allCandidates = new ArraySet<>(); 366 VolumeInfo bestCandidate = null; 367 long bestCandidateAvailBytes = Long.MIN_VALUE; 368 for (VolumeInfo vol : storageManager.getVolumes()) { 369 if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) { 370 final long availBytes = storageManager.getStorageBytesUntilLow(new File(vol.path)); 371 if (availBytes >= sizeBytes) { 372 allCandidates.add(vol.fsUuid); 373 } 374 if (availBytes >= bestCandidateAvailBytes) { 375 bestCandidate = vol; 376 bestCandidateAvailBytes = availBytes; 377 } 378 } 379 } 380 381 // System apps always forced to internal storage 382 if (existingInfo != null && existingInfo.isSystemApp()) { 383 installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; 384 } 385 386 // If app expresses strong desire for internal storage, honor it 387 if (!forceAllowOnExternal 388 && installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { 389 if (existingInfo != null && !Objects.equals(existingInfo.volumeUuid, 390 StorageManager.UUID_PRIVATE_INTERNAL)) { 391 throw new IOException("Cannot automatically move " + packageName + " from " 392 + existingInfo.volumeUuid + " to internal storage"); 393 } 394 if (fitsOnInternal) { 395 return StorageManager.UUID_PRIVATE_INTERNAL; 396 } else { 397 throw new IOException("Requested internal only, but not enough space"); 398 } 399 } 400 401 // If app already exists somewhere, we must stay on that volume 402 if (existingInfo != null) { 403 if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL) 404 && fitsOnInternal) { 405 return StorageManager.UUID_PRIVATE_INTERNAL; 406 } else if (allCandidates.contains(existingInfo.volumeUuid)) { 407 return existingInfo.volumeUuid; 408 } else { 409 throw new IOException("Not enough space on existing volume " 410 + existingInfo.volumeUuid + " for " + packageName + " upgrade"); 411 } 412 } 413 414 // We're left with either preferring external or auto, so just pick 415 // volume with most space 416 if (bestCandidate != null) { 417 return bestCandidate.fsUuid; 418 } else if (fitsOnInternal) { 419 return StorageManager.UUID_PRIVATE_INTERNAL; 420 } else { 421 throw new IOException("No special requests, but no room anywhere"); 422 } 423 } 424 fitsOnInternal(Context context, long sizeBytes)425 public static boolean fitsOnInternal(Context context, long sizeBytes) { 426 final StorageManager storage = context.getSystemService(StorageManager.class); 427 final File target = Environment.getDataDirectory(); 428 return (sizeBytes <= storage.getStorageBytesUntilLow(target)); 429 } 430 fitsOnExternal(Context context, long sizeBytes)431 public static boolean fitsOnExternal(Context context, long sizeBytes) { 432 final StorageManager storage = context.getSystemService(StorageManager.class); 433 final StorageVolume primary = storage.getPrimaryVolume(); 434 return (sizeBytes > 0) && !primary.isEmulated() 435 && Environment.MEDIA_MOUNTED.equals(primary.getState()) 436 && sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile()); 437 } 438 439 /** 440 * Given a requested {@link PackageInfo#installLocation} and calculated 441 * install size, pick the actual location to install the app. 442 */ resolveInstallLocation(Context context, String packageName, int installLocation, long sizeBytes, int installFlags)443 public static int resolveInstallLocation(Context context, String packageName, 444 int installLocation, long sizeBytes, int installFlags) { 445 ApplicationInfo existingInfo = null; 446 try { 447 existingInfo = context.getPackageManager().getApplicationInfo(packageName, 448 PackageManager.GET_UNINSTALLED_PACKAGES); 449 } catch (NameNotFoundException ignored) { 450 } 451 452 final int prefer; 453 final boolean checkBoth; 454 boolean ephemeral = false; 455 if ((installFlags & PackageManager.INSTALL_EPHEMERAL) != 0) { 456 prefer = RECOMMEND_INSTALL_INTERNAL; 457 ephemeral = true; 458 checkBoth = false; 459 } else if ((installFlags & PackageManager.INSTALL_INTERNAL) != 0) { 460 prefer = RECOMMEND_INSTALL_INTERNAL; 461 checkBoth = false; 462 } else if ((installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { 463 prefer = RECOMMEND_INSTALL_EXTERNAL; 464 checkBoth = false; 465 } else if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { 466 prefer = RECOMMEND_INSTALL_INTERNAL; 467 checkBoth = false; 468 } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) { 469 prefer = RECOMMEND_INSTALL_EXTERNAL; 470 checkBoth = true; 471 } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) { 472 // When app is already installed, prefer same medium 473 if (existingInfo != null) { 474 // TODO: distinguish if this is external ASEC 475 if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { 476 prefer = RECOMMEND_INSTALL_EXTERNAL; 477 } else { 478 prefer = RECOMMEND_INSTALL_INTERNAL; 479 } 480 } else { 481 prefer = RECOMMEND_INSTALL_INTERNAL; 482 } 483 checkBoth = true; 484 } else { 485 prefer = RECOMMEND_INSTALL_INTERNAL; 486 checkBoth = false; 487 } 488 489 boolean fitsOnInternal = false; 490 if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) { 491 fitsOnInternal = fitsOnInternal(context, sizeBytes); 492 } 493 494 boolean fitsOnExternal = false; 495 if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) { 496 fitsOnExternal = fitsOnExternal(context, sizeBytes); 497 } 498 499 if (prefer == RECOMMEND_INSTALL_INTERNAL) { 500 // The ephemeral case will either fit and return EPHEMERAL, or will not fit 501 // and will fall through to return INSUFFICIENT_STORAGE 502 if (fitsOnInternal) { 503 return (ephemeral) 504 ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL 505 : PackageHelper.RECOMMEND_INSTALL_INTERNAL; 506 } 507 } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) { 508 if (fitsOnExternal) { 509 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; 510 } 511 } 512 513 if (checkBoth) { 514 if (fitsOnInternal) { 515 return PackageHelper.RECOMMEND_INSTALL_INTERNAL; 516 } else if (fitsOnExternal) { 517 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; 518 } 519 } 520 521 return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; 522 } 523 calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride)524 public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, 525 String abiOverride) throws IOException { 526 NativeLibraryHelper.Handle handle = null; 527 try { 528 handle = NativeLibraryHelper.Handle.create(pkg); 529 return calculateInstalledSize(pkg, handle, isForwardLocked, abiOverride); 530 } finally { 531 IoUtils.closeQuietly(handle); 532 } 533 } 534 calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, boolean isForwardLocked, String abiOverride)535 public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, 536 boolean isForwardLocked, String abiOverride) throws IOException { 537 long sizeBytes = 0; 538 539 // Include raw APKs, and possibly unpacked resources 540 for (String codePath : pkg.getAllCodePaths()) { 541 final File codeFile = new File(codePath); 542 sizeBytes += codeFile.length(); 543 544 if (isForwardLocked) { 545 sizeBytes += PackageHelper.extractPublicFiles(codeFile, null); 546 } 547 } 548 549 // Include all relevant native code 550 sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride); 551 552 return sizeBytes; 553 } 554 replaceEnd(String str, String before, String after)555 public static String replaceEnd(String str, String before, String after) { 556 if (!str.endsWith(before)) { 557 throw new IllegalArgumentException( 558 "Expected " + str + " to end with " + before); 559 } 560 return str.substring(0, str.length() - before.length()) + after; 561 } 562 } 563