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