/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.content;

import static android.os.storage.VolumeInfo.ID_PRIVATE_INTERNAL;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageParser.PackageLite;
import android.content.pm.dex.DexMetadataHelper;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import libcore.io.IoUtils;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;

Constants used internally between the PackageManager and media container service transports. Some utility methods to invoke StorageManagerService api.
/** * Constants used internally between the PackageManager * and media container service transports. * Some utility methods to invoke StorageManagerService api. */
public class PackageHelper { public static final int RECOMMEND_INSTALL_INTERNAL = 1; public static final int RECOMMEND_INSTALL_EXTERNAL = 2; public static final int RECOMMEND_INSTALL_EPHEMERAL = 3; public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1; public static final int RECOMMEND_FAILED_INVALID_APK = -2; public static final int RECOMMEND_FAILED_INVALID_LOCATION = -3; public static final int RECOMMEND_FAILED_ALREADY_EXISTS = -4; public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5; public static final int RECOMMEND_FAILED_INVALID_URI = -6; public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7; private static final String TAG = "PackageHelper"; // App installation location settings values public static final int APP_INSTALL_AUTO = 0; public static final int APP_INSTALL_INTERNAL = 1; public static final int APP_INSTALL_EXTERNAL = 2; private static TestableInterface sDefaultTestableInterface = null; public static IStorageManager getStorageManager() throws RemoteException { IBinder service = ServiceManager.getService("mount"); if (service != null) { return IStorageManager.Stub.asInterface(service); } else { Log.e(TAG, "Can't get storagemanager service"); throw new RemoteException("Could not contact storagemanager service"); } }
A group of external dependencies used in resolveInstallVolume(Context, String, int, long). It can be backed by real values from the system or mocked ones for testing purposes.
/** * A group of external dependencies used in * {@link #resolveInstallVolume(Context, String, int, long)}. It can be backed by real values * from the system or mocked ones for testing purposes. */
public static abstract class TestableInterface { abstract public StorageManager getStorageManager(Context context); abstract public boolean getForceAllowOnExternalSetting(Context context); abstract public boolean getAllow3rdPartyOnInternalConfig(Context context); abstract public ApplicationInfo getExistingAppInfo(Context context, String packageName); abstract public File getDataDirectory(); } private synchronized static TestableInterface getDefaultTestableInterface() { if (sDefaultTestableInterface == null) { sDefaultTestableInterface = new TestableInterface() { @Override public StorageManager getStorageManager(Context context) { return context.getSystemService(StorageManager.class); } @Override public boolean getForceAllowOnExternalSetting(Context context) { return Settings.Global.getInt(context.getContentResolver(), Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0; } @Override public boolean getAllow3rdPartyOnInternalConfig(Context context) { return context.getResources().getBoolean( com.android.internal.R.bool.config_allow3rdPartyAppOnInternal); } @Override public ApplicationInfo getExistingAppInfo(Context context, String packageName) { ApplicationInfo existingInfo = null; try { existingInfo = context.getPackageManager().getApplicationInfo(packageName, PackageManager.MATCH_ANY_USER); } catch (NameNotFoundException ignored) { } return existingInfo; } @Override public File getDataDirectory() { return Environment.getDataDirectory(); } }; } return sDefaultTestableInterface; } @VisibleForTesting @Deprecated public static String resolveInstallVolume(Context context, String packageName, int installLocation, long sizeBytes, TestableInterface testInterface) throws IOException { final SessionParams params = new SessionParams(SessionParams.MODE_INVALID); params.appPackageName = packageName; params.installLocation = installLocation; params.sizeBytes = sizeBytes; return resolveInstallVolume(context, params, testInterface); }
Given a requested PackageInfo.installLocation and calculated install size, pick the actual volume to install the app. Only considers internal and private volumes, and prefers to keep an existing package on its current volume.
Returns:the VolumeInfo.fsUuid to install onto, or null for internal storage.
/** * Given a requested {@link PackageInfo#installLocation} and calculated * install size, pick the actual volume to install the app. Only considers * internal and private volumes, and prefers to keep an existing package on * its current volume. * * @return the {@link VolumeInfo#fsUuid} to install onto, or {@code null} * for internal storage. */
public static String resolveInstallVolume(Context context, SessionParams params) throws IOException { TestableInterface testableInterface = getDefaultTestableInterface(); return resolveInstallVolume(context, params.appPackageName, params.installLocation, params.sizeBytes, testableInterface); } @VisibleForTesting public static String resolveInstallVolume(Context context, SessionParams params, TestableInterface testInterface) throws IOException { final StorageManager storageManager = testInterface.getStorageManager(context); final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context); final boolean allow3rdPartyOnInternal = testInterface.getAllow3rdPartyOnInternalConfig(context); // TODO: handle existing apps installed in ASEC; currently assumes // they'll end up back on internal storage ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context, params.appPackageName); // Figure out best candidate volume, and also if we fit on internal final ArraySet<String> allCandidates = new ArraySet<>(); boolean fitsOnInternal = false; VolumeInfo bestCandidate = null; long bestCandidateAvailBytes = Long.MIN_VALUE; for (VolumeInfo vol : storageManager.getVolumes()) { if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) { final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id); final UUID target = storageManager.getUuidForPath(new File(vol.path)); final long availBytes = storageManager.getAllocatableBytes(target, translateAllocateFlags(params.installFlags)); if (isInternalStorage) { fitsOnInternal = (params.sizeBytes <= availBytes); } if (!isInternalStorage || allow3rdPartyOnInternal) { if (availBytes >= params.sizeBytes) { allCandidates.add(vol.fsUuid); } if (availBytes >= bestCandidateAvailBytes) { bestCandidate = vol; bestCandidateAvailBytes = availBytes; } } } } // System apps always forced to internal storage if (existingInfo != null && existingInfo.isSystemApp()) { if (fitsOnInternal) { return StorageManager.UUID_PRIVATE_INTERNAL; } else { throw new IOException("Not enough space on existing volume " + existingInfo.volumeUuid + " for system app " + params.appPackageName + " upgrade"); } } // If app expresses strong desire for internal storage, honor it if (!forceAllowOnExternal && params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { if (existingInfo != null && !Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)) { throw new IOException("Cannot automatically move " + params.appPackageName + " from " + existingInfo.volumeUuid + " to internal storage"); } if (!allow3rdPartyOnInternal) { throw new IOException("Not allowed to install non-system apps on internal storage"); } if (fitsOnInternal) { return StorageManager.UUID_PRIVATE_INTERNAL; } else { throw new IOException("Requested internal only, but not enough space"); } } // If app already exists somewhere, we must stay on that volume if (existingInfo != null) { if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL) && fitsOnInternal) { return StorageManager.UUID_PRIVATE_INTERNAL; } else if (allCandidates.contains(existingInfo.volumeUuid)) { return existingInfo.volumeUuid; } else { throw new IOException("Not enough space on existing volume " + existingInfo.volumeUuid + " for " + params.appPackageName + " upgrade"); } } // We're left with new installations with either preferring external or auto, so just pick // volume with most space if (bestCandidate != null) { return bestCandidate.fsUuid; } else { throw new IOException("No special requests, but no room on allowed volumes. " + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal); } } public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException { final StorageManager storage = context.getSystemService(StorageManager.class); final UUID target = storage.getUuidForPath(Environment.getDataDirectory()); return (params.sizeBytes <= storage.getAllocatableBytes(target, translateAllocateFlags(params.installFlags))); } public static boolean fitsOnExternal(Context context, SessionParams params) { final StorageManager storage = context.getSystemService(StorageManager.class); final StorageVolume primary = storage.getPrimaryVolume(); return (params.sizeBytes > 0) && !primary.isEmulated() && Environment.MEDIA_MOUNTED.equals(primary.getState()) && params.sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile()); } @Deprecated public static int resolveInstallLocation(Context context, String packageName, int installLocation, long sizeBytes, int installFlags) { final SessionParams params = new SessionParams(SessionParams.MODE_INVALID); params.appPackageName = packageName; params.installLocation = installLocation; params.sizeBytes = sizeBytes; params.installFlags = installFlags; try { return resolveInstallLocation(context, params); } catch (IOException e) { throw new IllegalStateException(e); } }
Given a requested PackageInfo.installLocation and calculated install size, pick the actual location to install the app.
/** * Given a requested {@link PackageInfo#installLocation} and calculated * install size, pick the actual location to install the app. */
public static int resolveInstallLocation(Context context, SessionParams params) throws IOException { ApplicationInfo existingInfo = null; try { existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName, PackageManager.MATCH_ANY_USER); } catch (NameNotFoundException ignored) { } final int prefer; final boolean checkBoth; boolean ephemeral = false; if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) { prefer = RECOMMEND_INSTALL_INTERNAL; ephemeral = true; checkBoth = false; } else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { prefer = RECOMMEND_INSTALL_INTERNAL; checkBoth = false; } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { prefer = RECOMMEND_INSTALL_EXTERNAL; checkBoth = false; } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { prefer = RECOMMEND_INSTALL_INTERNAL; checkBoth = false; } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) { prefer = RECOMMEND_INSTALL_EXTERNAL; checkBoth = true; } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) { // When app is already installed, prefer same medium if (existingInfo != null) { // TODO: distinguish if this is external ASEC if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { prefer = RECOMMEND_INSTALL_EXTERNAL; } else { prefer = RECOMMEND_INSTALL_INTERNAL; } } else { prefer = RECOMMEND_INSTALL_INTERNAL; } checkBoth = true; } else { prefer = RECOMMEND_INSTALL_INTERNAL; checkBoth = false; } boolean fitsOnInternal = false; if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) { fitsOnInternal = fitsOnInternal(context, params); } boolean fitsOnExternal = false; if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) { fitsOnExternal = fitsOnExternal(context, params); } if (prefer == RECOMMEND_INSTALL_INTERNAL) { // The ephemeral case will either fit and return EPHEMERAL, or will not fit // and will fall through to return INSUFFICIENT_STORAGE if (fitsOnInternal) { return (ephemeral) ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL : PackageHelper.RECOMMEND_INSTALL_INTERNAL; } } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) { if (fitsOnExternal) { return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; } } if (checkBoth) { if (fitsOnInternal) { return PackageHelper.RECOMMEND_INSTALL_INTERNAL; } else if (fitsOnExternal) { return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; } } return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; } @Deprecated public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride) throws IOException { return calculateInstalledSize(pkg, abiOverride); } public static long calculateInstalledSize(PackageLite pkg, String abiOverride) throws IOException { return calculateInstalledSize(pkg, abiOverride, null); } public static long calculateInstalledSize(PackageLite pkg, String abiOverride, FileDescriptor fd) throws IOException { NativeLibraryHelper.Handle handle = null; try { handle = fd != null ? NativeLibraryHelper.Handle.createFd(pkg, fd) : NativeLibraryHelper.Handle.create(pkg); return calculateInstalledSize(pkg, handle, abiOverride); } finally { IoUtils.closeQuietly(handle); } } @Deprecated public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, NativeLibraryHelper.Handle handle, String abiOverride) throws IOException { return calculateInstalledSize(pkg, handle, abiOverride); } public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, String abiOverride) throws IOException { long sizeBytes = 0; // Include raw APKs, and possibly unpacked resources for (String codePath : pkg.getAllCodePaths()) { final File codeFile = new File(codePath); sizeBytes += codeFile.length(); } // Include raw dex metadata files sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg); // Include all relevant native code sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride); return sizeBytes; } public static String replaceEnd(String str, String before, String after) { if (!str.endsWith(before)) { throw new IllegalArgumentException( "Expected " + str + " to end with " + before); } return str.substring(0, str.length() - before.length()) + after; } public static int translateAllocateFlags(int installFlags) { if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) { return StorageManager.FLAG_ALLOCATE_AGGRESSIVE; } else { return 0; } } }