| /* |
| * Copyright (C) 2021 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.server.pm; |
| |
| import static android.content.pm.PackageManager.INSTALL_INTERNAL; |
| import static android.content.pm.PackageManager.MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL; |
| import static android.content.pm.PackageManager.MOVE_FAILED_DEVICE_ADMIN; |
| import static android.content.pm.PackageManager.MOVE_FAILED_DOESNT_EXIST; |
| import static android.content.pm.PackageManager.MOVE_FAILED_INTERNAL_ERROR; |
| import static android.content.pm.PackageManager.MOVE_FAILED_LOCKED_USER; |
| import static android.content.pm.PackageManager.MOVE_FAILED_OPERATION_PENDING; |
| import static android.content.pm.PackageManager.MOVE_FAILED_SYSTEM_PACKAGE; |
| |
| import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; |
| import static com.android.server.pm.PackageManagerService.TAG; |
| |
| import android.app.ApplicationExitInfo; |
| import android.content.Intent; |
| import android.content.pm.IPackageInstallObserver2; |
| import android.content.pm.IPackageMoveObserver; |
| import android.content.pm.PackageInstaller; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageStats; |
| import android.content.pm.UserInfo; |
| import android.content.pm.parsing.ApkLiteParseUtils; |
| import android.content.pm.parsing.PackageLite; |
| import android.content.pm.parsing.result.ParseResult; |
| import android.content.pm.parsing.result.ParseTypeImpl; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.storage.StorageManager; |
| import android.os.storage.StorageManagerInternal; |
| import android.os.storage.VolumeInfo; |
| import android.util.MathUtils; |
| import android.util.Slog; |
| import android.util.SparseIntArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.os.SomeArgs; |
| import com.android.internal.util.FrameworkStatsLog; |
| import com.android.server.pm.parsing.pkg.AndroidPackageUtils; |
| import com.android.server.pm.pkg.AndroidPackage; |
| import com.android.server.pm.pkg.PackageStateInternal; |
| import com.android.server.pm.pkg.PackageStateUtils; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Objects; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| public final class MovePackageHelper { |
| final PackageManagerService mPm; |
| |
| // TODO(b/198166813): remove PMS dependency |
| public MovePackageHelper(PackageManagerService pm) { |
| mPm = pm; |
| } |
| |
| public void movePackageInternal(final String packageName, final String volumeUuid, |
| final int moveId, final int callingUid, UserHandle user) |
| throws PackageManagerException { |
| final StorageManager storage = mPm.mInjector.getSystemService(StorageManager.class); |
| final PackageManager pm = mPm.mContext.getPackageManager(); |
| |
| Computer snapshot = mPm.snapshotComputer(); |
| final PackageStateInternal packageState = snapshot.getPackageStateForInstalledAndFiltered( |
| packageName, callingUid, user.getIdentifier()); |
| if (packageState == null || packageState.getPkg() == null) { |
| throw new PackageManagerException(MOVE_FAILED_DOESNT_EXIST, "Missing package"); |
| } |
| final int[] installedUserIds = PackageStateUtils.queryInstalledUsers(packageState, |
| mPm.mUserManager.getUserIds(), true); |
| final UserHandle userForMove; |
| if (installedUserIds.length > 0) { |
| userForMove = UserHandle.of(installedUserIds[0]); |
| } else { |
| throw new PackageManagerException(MOVE_FAILED_DOESNT_EXIST, |
| "Package is not installed for any user"); |
| } |
| for (int userId : installedUserIds) { |
| if (snapshot.shouldFilterApplicationIncludingUninstalled(packageState, callingUid, |
| userId)) { |
| throw new PackageManagerException(MOVE_FAILED_DOESNT_EXIST, "Missing package"); |
| } |
| } |
| final AndroidPackage pkg = packageState.getPkg(); |
| if (packageState.isSystem()) { |
| throw new PackageManagerException(MOVE_FAILED_SYSTEM_PACKAGE, |
| "Cannot move system application"); |
| } |
| |
| final boolean isInternalStorage = VolumeInfo.ID_PRIVATE_INTERNAL.equals(volumeUuid); |
| final boolean allow3rdPartyOnInternal = mPm.mContext.getResources().getBoolean( |
| com.android.internal.R.bool.config_allow3rdPartyAppOnInternal); |
| if (isInternalStorage && !allow3rdPartyOnInternal) { |
| throw new PackageManagerException(MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL, |
| "3rd party apps are not allowed on internal storage"); |
| } |
| |
| final File probe = new File(pkg.getPath()); |
| if (!probe.isDirectory()) { |
| throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, |
| "Move only supported for modern cluster style installs"); |
| } |
| |
| final String currentVolumeUuid = packageState.getVolumeUuid(); |
| if (Objects.equals(currentVolumeUuid, volumeUuid)) { |
| throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, |
| "Package already moved to " + volumeUuid); |
| } |
| if (!pkg.isExternalStorage() |
| && mPm.isPackageDeviceAdminOnAnyUser(snapshot, packageName)) { |
| throw new PackageManagerException(MOVE_FAILED_DEVICE_ADMIN, |
| "Device admin cannot be moved"); |
| } |
| |
| if (snapshot.getFrozenPackages().containsKey(packageName)) { |
| throw new PackageManagerException(MOVE_FAILED_OPERATION_PENDING, |
| "Failed to move already frozen package"); |
| } |
| |
| final boolean isCurrentLocationExternal = pkg.isExternalStorage(); |
| final File codeFile = new File(pkg.getPath()); |
| final InstallSource installSource = packageState.getInstallSource(); |
| final String packageAbiOverride = packageState.getCpuAbiOverride(); |
| final int appId = UserHandle.getAppId(pkg.getUid()); |
| final String seinfo = packageState.getSeInfo(); |
| final String label = String.valueOf(pm.getApplicationLabel( |
| AndroidPackageUtils.generateAppInfoWithoutState(pkg))); |
| final int targetSdkVersion = pkg.getTargetSdkVersion(); |
| final String fromCodePath; |
| if (codeFile.getParentFile().getName().startsWith( |
| PackageManagerService.RANDOM_DIR_PREFIX)) { |
| fromCodePath = codeFile.getParentFile().getAbsolutePath(); |
| } else { |
| fromCodePath = codeFile.getAbsolutePath(); |
| } |
| |
| final PackageFreezer freezer; |
| synchronized (mPm.mLock) { |
| freezer = mPm.freezePackage(packageName, UserHandle.USER_ALL, |
| "movePackageInternal", ApplicationExitInfo.REASON_USER_REQUESTED, |
| null /* request */); |
| } |
| |
| final Bundle extras = new Bundle(); |
| extras.putString(Intent.EXTRA_PACKAGE_NAME, packageName); |
| extras.putString(Intent.EXTRA_TITLE, label); |
| mPm.mMoveCallbacks.notifyCreated(moveId, extras); |
| |
| int installFlags; |
| final boolean moveCompleteApp; |
| final File measurePath; |
| |
| installFlags = INSTALL_INTERNAL; |
| if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid)) { |
| moveCompleteApp = true; |
| measurePath = Environment.getDataAppDirectory(volumeUuid); |
| } else if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) { |
| moveCompleteApp = false; |
| measurePath = storage.getPrimaryPhysicalVolume().getPath(); |
| } else { |
| final VolumeInfo volume = storage.findVolumeByUuid(volumeUuid); |
| if (volume == null || volume.getType() != VolumeInfo.TYPE_PRIVATE |
| || !volume.isMountedWritable()) { |
| freezer.close(); |
| throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, |
| "Move location not mounted private volume"); |
| } |
| |
| moveCompleteApp = true; |
| measurePath = Environment.getDataAppDirectory(volumeUuid); |
| } |
| |
| // If we're moving app data around, we need all the users unlocked |
| if (moveCompleteApp) { |
| for (int userId : installedUserIds) { |
| if (StorageManager.isFileEncrypted() |
| && !StorageManager.isCeStorageUnlocked(userId)) { |
| freezer.close(); |
| throw new PackageManagerException(MOVE_FAILED_LOCKED_USER, |
| "User " + userId + " must be unlocked"); |
| } |
| } |
| } |
| |
| final PackageStats stats = new PackageStats(null, -1); |
| synchronized (mPm.mInstallLock) { |
| for (int userId : installedUserIds) { |
| if (!getPackageSizeInfoLI(packageName, userId, stats)) { |
| freezer.close(); |
| throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, |
| "Failed to measure package size"); |
| } |
| } |
| } |
| |
| if (DEBUG_INSTALL) { |
| Slog.d(TAG, "Measured code size " + stats.codeSize + ", data size " |
| + stats.dataSize); |
| } |
| |
| final long startFreeBytes = measurePath.getUsableSpace(); |
| final long sizeBytes; |
| if (moveCompleteApp) { |
| sizeBytes = stats.codeSize + stats.dataSize; |
| } else { |
| sizeBytes = stats.codeSize; |
| } |
| |
| if (sizeBytes > storage.getStorageBytesUntilLow(measurePath)) { |
| freezer.close(); |
| throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, |
| "Not enough free space to move"); |
| } |
| |
| try { |
| prepareUserStorageForMove(currentVolumeUuid, volumeUuid, installedUserIds); |
| } catch (RuntimeException e) { |
| freezer.close(); |
| throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, |
| "Failed to prepare user storage while moving app"); |
| } |
| |
| mPm.mMoveCallbacks.notifyStatusChanged(moveId, 10); |
| |
| final CountDownLatch installedLatch = new CountDownLatch(1); |
| final IPackageInstallObserver2 installObserver = new IPackageInstallObserver2.Stub() { |
| @Override |
| public void onUserActionRequired(Intent intent) throws RemoteException { |
| freezer.close(); |
| throw new IllegalStateException(); |
| } |
| |
| @Override |
| public void onPackageInstalled(String basePackageName, int returnCode, String msg, |
| Bundle extras) throws RemoteException { |
| if (DEBUG_INSTALL) { |
| Slog.d(TAG, "Install result for move: " |
| + PackageManager.installStatusToString(returnCode, msg)); |
| } |
| |
| installedLatch.countDown(); |
| freezer.close(); |
| |
| final int status = PackageManager.installStatusToPublicStatus(returnCode); |
| switch (status) { |
| case PackageInstaller.STATUS_SUCCESS: |
| mPm.mMoveCallbacks.notifyStatusChanged(moveId, |
| PackageManager.MOVE_SUCCEEDED); |
| logAppMovedStorage(packageName, isCurrentLocationExternal); |
| break; |
| case PackageInstaller.STATUS_FAILURE_STORAGE: |
| mPm.mMoveCallbacks.notifyStatusChanged(moveId, |
| PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE); |
| break; |
| default: |
| mPm.mMoveCallbacks.notifyStatusChanged(moveId, |
| PackageManager.MOVE_FAILED_INTERNAL_ERROR); |
| break; |
| } |
| } |
| }; |
| |
| final MoveInfo move; |
| if (moveCompleteApp) { |
| // Kick off a thread to report progress estimates |
| new Thread(() -> { |
| while (true) { |
| try { |
| if (installedLatch.await(1, TimeUnit.SECONDS)) { |
| break; |
| } |
| } catch (InterruptedException ignored) { |
| } |
| |
| final long deltaFreeBytes = startFreeBytes - measurePath.getUsableSpace(); |
| final int progress = 10 + (int) MathUtils.constrain( |
| ((deltaFreeBytes * 80) / sizeBytes), 0, 80); |
| mPm.mMoveCallbacks.notifyStatusChanged(moveId, progress); |
| } |
| }).start(); |
| |
| move = new MoveInfo(moveId, currentVolumeUuid, volumeUuid, packageName, |
| appId, seinfo, targetSdkVersion, fromCodePath); |
| } else { |
| move = null; |
| } |
| |
| installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; |
| |
| final OriginInfo origin = OriginInfo.fromExistingFile(codeFile); |
| final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); |
| final ParseResult<PackageLite> ret = ApkLiteParseUtils.parsePackageLite(input, |
| new File(origin.mResolvedPath), /* flags */ 0); |
| final PackageLite lite = ret.isSuccess() ? ret.getResult() : null; |
| final InstallingSession installingSession = new InstallingSession(origin, move, |
| installObserver, installFlags, /* developmentInstallFlags= */ 0, installSource, |
| volumeUuid, userForMove, packageAbiOverride, |
| PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, lite, mPm); |
| installingSession.movePackage(); |
| } |
| |
| /** |
| * Logs that an app has been moved from internal to external storage and vice versa. |
| * @param packageName The package that was moved. |
| */ |
| private void logAppMovedStorage(String packageName, boolean isPreviousLocationExternal) { |
| final Computer snapshot = mPm.snapshotComputer(); |
| final AndroidPackage pkg = snapshot.getPackage(packageName); |
| if (pkg == null) { |
| return; |
| } |
| |
| final StorageManager storage = mPm.mInjector.getSystemService(StorageManager.class); |
| VolumeInfo volume = storage.findVolumeByUuid( |
| StorageManager.convert(pkg.getVolumeUuid()).toString()); |
| int packageExternalStorageType = PackageManagerServiceUtils.getPackageExternalStorageType( |
| volume, pkg.isExternalStorage()); |
| |
| if (!isPreviousLocationExternal && pkg.isExternalStorage()) { |
| // Move from internal to external storage. |
| FrameworkStatsLog.write(FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED, |
| packageExternalStorageType, |
| FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED__MOVE_TYPE__TO_EXTERNAL, |
| packageName); |
| } else if (isPreviousLocationExternal && !pkg.isExternalStorage()) { |
| // Move from external to internal storage. |
| FrameworkStatsLog.write(FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED, |
| packageExternalStorageType, |
| FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED__MOVE_TYPE__TO_INTERNAL, |
| packageName); |
| } |
| } |
| |
| @GuardedBy("mPm.mInstallLock") |
| private boolean getPackageSizeInfoLI(String packageName, int userId, PackageStats stats) { |
| final Computer snapshot = mPm.snapshotComputer(); |
| final PackageStateInternal packageStateInternal = |
| snapshot.getPackageStateInternal(packageName); |
| if (packageStateInternal == null) { |
| Slog.w(TAG, "Failed to find settings for " + packageName); |
| return false; |
| } |
| |
| final String[] packageNames = { packageName }; |
| final long[] ceDataInodes = { |
| packageStateInternal.getUserStateOrDefault(userId).getCeDataInode() }; |
| final String[] codePaths = { packageStateInternal.getPathString() }; |
| |
| try { |
| mPm.mInstaller.getAppSize(packageStateInternal.getVolumeUuid(), packageNames, userId, |
| 0, packageStateInternal.getAppId(), ceDataInodes, codePaths, stats); |
| |
| // For now, ignore code size of packages on system partition |
| if (PackageManagerServiceUtils.isSystemApp(packageStateInternal) |
| && !PackageManagerServiceUtils.isUpdatedSystemApp(packageStateInternal)) { |
| stats.codeSize = 0; |
| } |
| |
| // External clients expect these to be tracked separately |
| stats.dataSize -= stats.cacheSize; |
| |
| } catch (Installer.InstallerException e) { |
| Slog.w(TAG, String.valueOf(e)); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private void prepareUserStorageForMove(String fromVolumeUuid, String toVolumeUuid, |
| int[] userIds) { |
| if (DEBUG_INSTALL) { |
| Slog.d(TAG, "Preparing user directories before moving app, from UUID " + fromVolumeUuid |
| + " to UUID " + toVolumeUuid); |
| } |
| final StorageManagerInternal smInternal = |
| mPm.mInjector.getLocalService(StorageManagerInternal.class); |
| final ArrayList<UserInfo> users = new ArrayList<>(); |
| for (int userId : userIds) { |
| final UserInfo user = mPm.mUserManager.getUserInfo(userId); |
| users.add(user); |
| } |
| smInternal.prepareUserStorageForMove(fromVolumeUuid, toVolumeUuid, users); |
| } |
| |
| public static class MoveCallbacks extends Handler { |
| private static final int MSG_CREATED = 1; |
| private static final int MSG_STATUS_CHANGED = 2; |
| |
| private final RemoteCallbackList<IPackageMoveObserver> |
| mCallbacks = new RemoteCallbackList<>(); |
| |
| public final SparseIntArray mLastStatus = new SparseIntArray(); |
| |
| public MoveCallbacks(Looper looper) { |
| super(looper); |
| } |
| |
| public void register(IPackageMoveObserver callback) { |
| mCallbacks.register(callback); |
| } |
| |
| public void unregister(IPackageMoveObserver callback) { |
| mCallbacks.unregister(callback); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| final SomeArgs args = (SomeArgs) msg.obj; |
| final int n = mCallbacks.beginBroadcast(); |
| for (int i = 0; i < n; i++) { |
| final IPackageMoveObserver callback = mCallbacks.getBroadcastItem(i); |
| try { |
| invokeCallback(callback, msg.what, args); |
| } catch (RemoteException ignored) { |
| } |
| } |
| mCallbacks.finishBroadcast(); |
| args.recycle(); |
| } |
| |
| private void invokeCallback(IPackageMoveObserver callback, int what, SomeArgs args) |
| throws RemoteException { |
| switch (what) { |
| case MSG_CREATED: { |
| callback.onCreated(args.argi1, (Bundle) args.arg2); |
| break; |
| } |
| case MSG_STATUS_CHANGED: { |
| callback.onStatusChanged(args.argi1, args.argi2, (long) args.arg3); |
| break; |
| } |
| } |
| } |
| |
| public void notifyCreated(int moveId, Bundle extras) { |
| Slog.v(TAG, "Move " + moveId + " created " + extras.toString()); |
| |
| final SomeArgs args = SomeArgs.obtain(); |
| args.argi1 = moveId; |
| args.arg2 = extras; |
| obtainMessage(MSG_CREATED, args).sendToTarget(); |
| } |
| |
| public void notifyStatusChanged(int moveId, int status) { |
| notifyStatusChanged(moveId, status, -1); |
| } |
| |
| public void notifyStatusChanged(int moveId, int status, long estMillis) { |
| Slog.v(TAG, "Move " + moveId + " status " + status); |
| |
| final SomeArgs args = SomeArgs.obtain(); |
| args.argi1 = moveId; |
| args.argi2 = status; |
| args.arg3 = estMillis; |
| obtainMessage(MSG_STATUS_CHANGED, args).sendToTarget(); |
| |
| synchronized (mLastStatus) { |
| mLastStatus.put(moveId, status); |
| } |
| } |
| } |
| } |