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.os.storage.VolumeInfo.ID_PRIVATE_INTERNAL; 20 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageInfo; 24 import android.content.pm.PackageInstaller.SessionParams; 25 import android.content.pm.PackageManager; 26 import android.content.pm.PackageManager.NameNotFoundException; 27 import android.content.pm.dex.DexMetadataHelper; 28 import android.content.pm.parsing.PackageLite; 29 import android.os.Environment; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.os.storage.IStorageManager; 34 import android.os.storage.StorageManager; 35 import android.os.storage.StorageVolume; 36 import android.os.storage.VolumeInfo; 37 import android.provider.Settings; 38 import android.util.ArrayMap; 39 import android.util.Log; 40 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import libcore.io.IoUtils; 44 45 import java.io.File; 46 import java.io.FileDescriptor; 47 import java.io.IOException; 48 import java.util.Objects; 49 import java.util.UUID; 50 51 /** 52 * Constants used internally between the PackageManager 53 * and media container service transports. 54 * Some utility methods to invoke StorageManagerService api. 55 */ 56 public class PackageHelper { 57 public static final int RECOMMEND_INSTALL_INTERNAL = 1; 58 public static final int RECOMMEND_INSTALL_EXTERNAL = 2; 59 public static final int RECOMMEND_INSTALL_EPHEMERAL = 3; 60 public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1; 61 public static final int RECOMMEND_FAILED_INVALID_APK = -2; 62 public static final int RECOMMEND_FAILED_INVALID_LOCATION = -3; 63 public static final int RECOMMEND_FAILED_ALREADY_EXISTS = -4; 64 public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5; 65 public static final int RECOMMEND_FAILED_INVALID_URI = -6; 66 67 private static final String TAG = "PackageHelper"; 68 // App installation location settings values 69 public static final int APP_INSTALL_AUTO = 0; 70 public static final int APP_INSTALL_INTERNAL = 1; 71 public static final int APP_INSTALL_EXTERNAL = 2; 72 73 private static TestableInterface sDefaultTestableInterface = null; 74 getStorageManager()75 public static IStorageManager getStorageManager() throws RemoteException { 76 IBinder service = ServiceManager.getService("mount"); 77 if (service != null) { 78 return IStorageManager.Stub.asInterface(service); 79 } else { 80 Log.e(TAG, "Can't get storagemanager service"); 81 throw new RemoteException("Could not contact storagemanager service"); 82 } 83 } 84 85 /** 86 * A group of external dependencies used in 87 * {@link #resolveInstallVolume(Context, String, int, long, TestableInterface)}. 88 * It can be backed by real values from the system or mocked ones for testing purposes. 89 */ 90 public static abstract class TestableInterface { getStorageManager(Context context)91 abstract public StorageManager getStorageManager(Context context); getForceAllowOnExternalSetting(Context context)92 abstract public boolean getForceAllowOnExternalSetting(Context context); getAllow3rdPartyOnInternalConfig(Context context)93 abstract public boolean getAllow3rdPartyOnInternalConfig(Context context); getExistingAppInfo(Context context, String packageName)94 abstract public ApplicationInfo getExistingAppInfo(Context context, String packageName); getDataDirectory()95 abstract public File getDataDirectory(); 96 } 97 getDefaultTestableInterface()98 private synchronized static TestableInterface getDefaultTestableInterface() { 99 if (sDefaultTestableInterface == null) { 100 sDefaultTestableInterface = new TestableInterface() { 101 @Override 102 public StorageManager getStorageManager(Context context) { 103 return context.getSystemService(StorageManager.class); 104 } 105 106 @Override 107 public boolean getForceAllowOnExternalSetting(Context context) { 108 return Settings.Global.getInt(context.getContentResolver(), 109 Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0; 110 } 111 112 @Override 113 public boolean getAllow3rdPartyOnInternalConfig(Context context) { 114 return context.getResources().getBoolean( 115 com.android.internal.R.bool.config_allow3rdPartyAppOnInternal); 116 } 117 118 @Override 119 public ApplicationInfo getExistingAppInfo(Context context, String packageName) { 120 ApplicationInfo existingInfo = null; 121 try { 122 existingInfo = context.getPackageManager().getApplicationInfo(packageName, 123 PackageManager.MATCH_ANY_USER); 124 } catch (NameNotFoundException ignored) { 125 } 126 return existingInfo; 127 } 128 129 @Override 130 public File getDataDirectory() { 131 return Environment.getDataDirectory(); 132 } 133 }; 134 } 135 return sDefaultTestableInterface; 136 } 137 138 @VisibleForTesting 139 @Deprecated resolveInstallVolume(Context context, String packageName, int installLocation, long sizeBytes, TestableInterface testInterface)140 public static String resolveInstallVolume(Context context, String packageName, 141 int installLocation, long sizeBytes, TestableInterface testInterface) 142 throws IOException { 143 final SessionParams params = new SessionParams(SessionParams.MODE_INVALID); 144 params.appPackageName = packageName; 145 params.installLocation = installLocation; 146 params.sizeBytes = sizeBytes; 147 return resolveInstallVolume(context, params, testInterface); 148 } 149 150 /** 151 * Given a requested {@link PackageInfo#installLocation} and calculated 152 * install size, pick the actual volume to install the app. Only considers 153 * internal and private volumes, and prefers to keep an existing package on 154 * its current volume. 155 * 156 * @return the {@link VolumeInfo#fsUuid} to install onto, or {@code null} 157 * for internal storage. 158 */ resolveInstallVolume(Context context, SessionParams params)159 public static String resolveInstallVolume(Context context, SessionParams params) 160 throws IOException { 161 TestableInterface testableInterface = getDefaultTestableInterface(); 162 return resolveInstallVolume(context, params.appPackageName, params.installLocation, 163 params.sizeBytes, testableInterface); 164 } 165 checkFitOnVolume(StorageManager storageManager, String volumePath, SessionParams params)166 private static boolean checkFitOnVolume(StorageManager storageManager, String volumePath, 167 SessionParams params) throws IOException { 168 if (volumePath == null) { 169 return false; 170 } 171 final int installFlags = translateAllocateFlags(params.installFlags); 172 final UUID target = storageManager.getUuidForPath(new File(volumePath)); 173 final long availBytes = storageManager.getAllocatableBytes(target, 174 installFlags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY); 175 if (params.sizeBytes <= availBytes) { 176 return true; 177 } 178 final long cacheClearable = storageManager.getAllocatableBytes(target, 179 installFlags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY); 180 return params.sizeBytes <= availBytes + cacheClearable; 181 } 182 183 @VisibleForTesting resolveInstallVolume(Context context, SessionParams params, TestableInterface testInterface)184 public static String resolveInstallVolume(Context context, SessionParams params, 185 TestableInterface testInterface) throws IOException { 186 final StorageManager storageManager = testInterface.getStorageManager(context); 187 final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context); 188 final boolean allow3rdPartyOnInternal = 189 testInterface.getAllow3rdPartyOnInternalConfig(context); 190 // TODO: handle existing apps installed in ASEC; currently assumes 191 // they'll end up back on internal storage 192 ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context, 193 params.appPackageName); 194 195 final ArrayMap<String, String> volumePaths = new ArrayMap<>(); 196 String internalVolumePath = null; 197 for (VolumeInfo vol : storageManager.getVolumes()) { 198 if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) { 199 final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id); 200 if (isInternalStorage) { 201 internalVolumePath = vol.path; 202 } 203 if (!isInternalStorage || allow3rdPartyOnInternal) { 204 volumePaths.put(vol.fsUuid, vol.path); 205 } 206 } 207 } 208 209 // System apps always forced to internal storage 210 if (existingInfo != null && existingInfo.isSystemApp()) { 211 if (checkFitOnVolume(storageManager, internalVolumePath, params)) { 212 return StorageManager.UUID_PRIVATE_INTERNAL; 213 } else { 214 throw new IOException("Not enough space on existing volume " 215 + existingInfo.volumeUuid + " for system app " + params.appPackageName 216 + " upgrade"); 217 } 218 } 219 220 // If app expresses strong desire for internal storage, honor it 221 if (!forceAllowOnExternal 222 && params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { 223 if (existingInfo != null && !Objects.equals(existingInfo.volumeUuid, 224 StorageManager.UUID_PRIVATE_INTERNAL)) { 225 throw new IOException("Cannot automatically move " + params.appPackageName 226 + " from " + existingInfo.volumeUuid + " to internal storage"); 227 } 228 229 if (!allow3rdPartyOnInternal) { 230 throw new IOException("Not allowed to install non-system apps on internal storage"); 231 } 232 233 if (checkFitOnVolume(storageManager, internalVolumePath, params)) { 234 return StorageManager.UUID_PRIVATE_INTERNAL; 235 } else { 236 throw new IOException("Requested internal only, but not enough space"); 237 } 238 } 239 240 // If app already exists somewhere, we must stay on that volume 241 if (existingInfo != null) { 242 String existingVolumePath = null; 243 if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)) { 244 existingVolumePath = internalVolumePath; 245 } else if (volumePaths.containsKey(existingInfo.volumeUuid)) { 246 existingVolumePath = volumePaths.get(existingInfo.volumeUuid); 247 } 248 249 if (checkFitOnVolume(storageManager, existingVolumePath, params)) { 250 return existingInfo.volumeUuid; 251 } else { 252 throw new IOException("Not enough space on existing volume " 253 + existingInfo.volumeUuid + " for " + params.appPackageName + " upgrade"); 254 } 255 } 256 257 // We're left with new installations with either preferring external or auto, so just pick 258 // volume with most space 259 if (volumePaths.size() == 1) { 260 if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) { 261 return volumePaths.keyAt(0); 262 } 263 } else { 264 String bestCandidate = null; 265 long bestCandidateAvailBytes = Long.MIN_VALUE; 266 for (String vol : volumePaths.keySet()) { 267 final String volumePath = volumePaths.get(vol); 268 final UUID target = storageManager.getUuidForPath(new File(volumePath)); 269 270 // We need to take into account freeable cached space, because we're choosing the 271 // best candidate amongst a list, not just checking if we fit at all. 272 final long availBytes = storageManager.getAllocatableBytes(target, 273 translateAllocateFlags(params.installFlags)); 274 275 if (availBytes >= bestCandidateAvailBytes) { 276 bestCandidate = vol; 277 bestCandidateAvailBytes = availBytes; 278 } 279 } 280 281 if (bestCandidateAvailBytes >= params.sizeBytes) { 282 return bestCandidate; 283 } 284 285 } 286 287 throw new IOException("No special requests, but no room on allowed volumes. " 288 + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal); 289 } 290 fitsOnInternal(Context context, SessionParams params)291 public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException { 292 final StorageManager storage = context.getSystemService(StorageManager.class); 293 final UUID target = storage.getUuidForPath(Environment.getDataDirectory()); 294 final int flags = translateAllocateFlags(params.installFlags); 295 296 final long allocateableBytes = storage.getAllocatableBytes(target, 297 flags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY); 298 299 // If we fit on internal storage without including freeable cache space, don't bother 300 // checking to determine how much space is taken up by the cache. 301 if (params.sizeBytes <= allocateableBytes) { 302 return true; 303 } 304 305 final long cacheClearable = storage.getAllocatableBytes(target, 306 flags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY); 307 308 return params.sizeBytes <= allocateableBytes + cacheClearable; 309 } 310 fitsOnExternal(Context context, SessionParams params)311 public static boolean fitsOnExternal(Context context, SessionParams params) { 312 final StorageManager storage = context.getSystemService(StorageManager.class); 313 final StorageVolume primary = storage.getPrimaryVolume(); 314 return (params.sizeBytes > 0) && !primary.isEmulated() 315 && Environment.MEDIA_MOUNTED.equals(primary.getState()) 316 && params.sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile()); 317 } 318 319 @Deprecated resolveInstallLocation(Context context, String packageName, int installLocation, long sizeBytes, int installFlags)320 public static int resolveInstallLocation(Context context, String packageName, 321 int installLocation, long sizeBytes, int installFlags) { 322 final SessionParams params = new SessionParams(SessionParams.MODE_INVALID); 323 params.appPackageName = packageName; 324 params.installLocation = installLocation; 325 params.sizeBytes = sizeBytes; 326 params.installFlags = installFlags; 327 try { 328 return resolveInstallLocation(context, params); 329 } catch (IOException e) { 330 throw new IllegalStateException(e); 331 } 332 } 333 334 /** 335 * Given a requested {@link PackageInfo#installLocation} and calculated 336 * install size, pick the actual location to install the app. 337 */ resolveInstallLocation(Context context, SessionParams params)338 public static int resolveInstallLocation(Context context, SessionParams params) 339 throws IOException { 340 ApplicationInfo existingInfo = null; 341 try { 342 existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName, 343 PackageManager.MATCH_ANY_USER); 344 } catch (NameNotFoundException ignored) { 345 } 346 347 final int prefer; 348 final boolean checkBoth; 349 boolean ephemeral = false; 350 if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) { 351 prefer = RECOMMEND_INSTALL_INTERNAL; 352 ephemeral = true; 353 checkBoth = false; 354 } else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { 355 prefer = RECOMMEND_INSTALL_INTERNAL; 356 checkBoth = false; 357 } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { 358 prefer = RECOMMEND_INSTALL_INTERNAL; 359 checkBoth = false; 360 } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) { 361 prefer = RECOMMEND_INSTALL_EXTERNAL; 362 checkBoth = true; 363 } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) { 364 // When app is already installed, prefer same medium 365 if (existingInfo != null) { 366 // TODO: distinguish if this is external ASEC 367 if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { 368 prefer = RECOMMEND_INSTALL_EXTERNAL; 369 } else { 370 prefer = RECOMMEND_INSTALL_INTERNAL; 371 } 372 } else { 373 prefer = RECOMMEND_INSTALL_INTERNAL; 374 } 375 checkBoth = true; 376 } else { 377 prefer = RECOMMEND_INSTALL_INTERNAL; 378 checkBoth = false; 379 } 380 381 boolean fitsOnInternal = false; 382 if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) { 383 fitsOnInternal = fitsOnInternal(context, params); 384 } 385 386 boolean fitsOnExternal = false; 387 if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) { 388 fitsOnExternal = fitsOnExternal(context, params); 389 } 390 391 if (prefer == RECOMMEND_INSTALL_INTERNAL) { 392 // The ephemeral case will either fit and return EPHEMERAL, or will not fit 393 // and will fall through to return INSUFFICIENT_STORAGE 394 if (fitsOnInternal) { 395 return (ephemeral) 396 ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL 397 : PackageHelper.RECOMMEND_INSTALL_INTERNAL; 398 } 399 } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) { 400 if (fitsOnExternal) { 401 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; 402 } 403 } 404 405 if (checkBoth) { 406 if (fitsOnInternal) { 407 return PackageHelper.RECOMMEND_INSTALL_INTERNAL; 408 } else if (fitsOnExternal) { 409 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; 410 } 411 } 412 413 return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; 414 } 415 416 @Deprecated calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride)417 public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, 418 String abiOverride) throws IOException { 419 return calculateInstalledSize(pkg, abiOverride); 420 } 421 calculateInstalledSize(PackageLite pkg, String abiOverride)422 public static long calculateInstalledSize(PackageLite pkg, String abiOverride) 423 throws IOException { 424 return calculateInstalledSize(pkg, abiOverride, null); 425 } 426 calculateInstalledSize(PackageLite pkg, String abiOverride, FileDescriptor fd)427 public static long calculateInstalledSize(PackageLite pkg, String abiOverride, 428 FileDescriptor fd) throws IOException { 429 NativeLibraryHelper.Handle handle = null; 430 try { 431 handle = fd != null ? NativeLibraryHelper.Handle.createFd(pkg, fd) 432 : NativeLibraryHelper.Handle.create(pkg); 433 return calculateInstalledSize(pkg, handle, abiOverride); 434 } finally { 435 IoUtils.closeQuietly(handle); 436 } 437 } 438 439 @Deprecated calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, NativeLibraryHelper.Handle handle, String abiOverride)440 public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, 441 NativeLibraryHelper.Handle handle, String abiOverride) throws IOException { 442 return calculateInstalledSize(pkg, handle, abiOverride); 443 } 444 calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, String abiOverride)445 public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, 446 String abiOverride) throws IOException { 447 long sizeBytes = 0; 448 449 // Include raw APKs, and possibly unpacked resources 450 for (String codePath : pkg.getAllApkPaths()) { 451 final File codeFile = new File(codePath); 452 sizeBytes += codeFile.length(); 453 } 454 455 // Include raw dex metadata files 456 sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg); 457 458 // Include all relevant native code 459 sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride); 460 461 return sizeBytes; 462 } 463 replaceEnd(String str, String before, String after)464 public static String replaceEnd(String str, String before, String after) { 465 if (!str.endsWith(before)) { 466 throw new IllegalArgumentException( 467 "Expected " + str + " to end with " + before); 468 } 469 return str.substring(0, str.length() - before.length()) + after; 470 } 471 translateAllocateFlags(int installFlags)472 public static int translateAllocateFlags(int installFlags) { 473 if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) { 474 return StorageManager.FLAG_ALLOCATE_AGGRESSIVE; 475 } else { 476 return 0; 477 } 478 } 479 } 480