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