1 /*
2  * Copyright (C) 2021 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.server.pm;
18 
19 import static android.content.pm.PackageManager.INSTALL_INTERNAL;
20 import static android.content.pm.PackageManager.MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL;
21 import static android.content.pm.PackageManager.MOVE_FAILED_DEVICE_ADMIN;
22 import static android.content.pm.PackageManager.MOVE_FAILED_DOESNT_EXIST;
23 import static android.content.pm.PackageManager.MOVE_FAILED_INTERNAL_ERROR;
24 import static android.content.pm.PackageManager.MOVE_FAILED_LOCKED_USER;
25 import static android.content.pm.PackageManager.MOVE_FAILED_OPERATION_PENDING;
26 import static android.content.pm.PackageManager.MOVE_FAILED_SYSTEM_PACKAGE;
27 
28 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
29 import static com.android.server.pm.PackageManagerService.TAG;
30 
31 import android.app.ApplicationExitInfo;
32 import android.content.Intent;
33 import android.content.pm.IPackageInstallObserver2;
34 import android.content.pm.IPackageMoveObserver;
35 import android.content.pm.PackageInstaller;
36 import android.content.pm.PackageManager;
37 import android.content.pm.PackageStats;
38 import android.content.pm.parsing.ApkLiteParseUtils;
39 import android.content.pm.parsing.PackageLite;
40 import android.content.pm.parsing.result.ParseResult;
41 import android.content.pm.parsing.result.ParseTypeImpl;
42 import android.os.Bundle;
43 import android.os.Environment;
44 import android.os.Handler;
45 import android.os.Looper;
46 import android.os.Message;
47 import android.os.RemoteCallbackList;
48 import android.os.RemoteException;
49 import android.os.UserHandle;
50 import android.os.storage.StorageManager;
51 import android.os.storage.VolumeInfo;
52 import android.util.MathUtils;
53 import android.util.Slog;
54 import android.util.SparseIntArray;
55 
56 import com.android.internal.annotations.GuardedBy;
57 import com.android.internal.os.SomeArgs;
58 import com.android.internal.util.FrameworkStatsLog;
59 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
60 import com.android.server.pm.pkg.AndroidPackage;
61 import com.android.server.pm.pkg.PackageStateInternal;
62 import com.android.server.pm.pkg.PackageStateUtils;
63 
64 import java.io.File;
65 import java.util.Objects;
66 import java.util.concurrent.CountDownLatch;
67 import java.util.concurrent.TimeUnit;
68 
69 public final class MovePackageHelper {
70     final PackageManagerService mPm;
71 
72     // TODO(b/198166813): remove PMS dependency
MovePackageHelper(PackageManagerService pm)73     public MovePackageHelper(PackageManagerService pm) {
74         mPm = pm;
75     }
76 
movePackageInternal(final String packageName, final String volumeUuid, final int moveId, final int callingUid, UserHandle user)77     public void movePackageInternal(final String packageName, final String volumeUuid,
78             final int moveId, final int callingUid, UserHandle user)
79             throws PackageManagerException {
80         final StorageManager storage = mPm.mInjector.getSystemService(StorageManager.class);
81         final PackageManager pm = mPm.mContext.getPackageManager();
82 
83         Computer snapshot = mPm.snapshotComputer();
84         final PackageStateInternal packageState = snapshot.getPackageStateForInstalledAndFiltered(
85                 packageName, callingUid, user.getIdentifier());
86         if (packageState == null || packageState.getPkg() == null) {
87             throw new PackageManagerException(MOVE_FAILED_DOESNT_EXIST, "Missing package");
88         }
89         final AndroidPackage pkg = packageState.getPkg();
90         if (packageState.isSystem()) {
91             throw new PackageManagerException(MOVE_FAILED_SYSTEM_PACKAGE,
92                     "Cannot move system application");
93         }
94 
95         final boolean isInternalStorage = VolumeInfo.ID_PRIVATE_INTERNAL.equals(volumeUuid);
96         final boolean allow3rdPartyOnInternal = mPm.mContext.getResources().getBoolean(
97                 com.android.internal.R.bool.config_allow3rdPartyAppOnInternal);
98         if (isInternalStorage && !allow3rdPartyOnInternal) {
99             throw new PackageManagerException(MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL,
100                     "3rd party apps are not allowed on internal storage");
101         }
102 
103 
104         final String currentVolumeUuid = packageState.getVolumeUuid();
105 
106         final File probe = new File(pkg.getPath());
107         final File probeOat = new File(probe, "oat");
108         if (!probe.isDirectory() || !probeOat.isDirectory()) {
109             throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
110                     "Move only supported for modern cluster style installs");
111         }
112 
113         if (Objects.equals(currentVolumeUuid, volumeUuid)) {
114             throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
115                     "Package already moved to " + volumeUuid);
116         }
117         if (!pkg.isExternalStorage()
118                 && mPm.isPackageDeviceAdminOnAnyUser(snapshot, packageName)) {
119             throw new PackageManagerException(MOVE_FAILED_DEVICE_ADMIN,
120                     "Device admin cannot be moved");
121         }
122 
123         if (snapshot.getFrozenPackages().containsKey(packageName)) {
124             throw new PackageManagerException(MOVE_FAILED_OPERATION_PENDING,
125                     "Failed to move already frozen package");
126         }
127 
128         final boolean isCurrentLocationExternal = pkg.isExternalStorage();
129         final File codeFile = new File(pkg.getPath());
130         final InstallSource installSource = packageState.getInstallSource();
131         final String packageAbiOverride = packageState.getCpuAbiOverride();
132         final int appId = UserHandle.getAppId(pkg.getUid());
133         final String seinfo = packageState.getSeInfo();
134         final String label = String.valueOf(pm.getApplicationLabel(
135                 AndroidPackageUtils.generateAppInfoWithoutState(pkg)));
136         final int targetSdkVersion = pkg.getTargetSdkVersion();
137         final int[] installedUserIds = PackageStateUtils.queryInstalledUsers(packageState,
138                 mPm.mUserManager.getUserIds(), true);
139         final String fromCodePath;
140         if (codeFile.getParentFile().getName().startsWith(
141                 PackageManagerService.RANDOM_DIR_PREFIX)) {
142             fromCodePath = codeFile.getParentFile().getAbsolutePath();
143         } else {
144             fromCodePath = codeFile.getAbsolutePath();
145         }
146 
147         final PackageFreezer freezer;
148         synchronized (mPm.mLock) {
149             freezer = mPm.freezePackage(packageName, UserHandle.USER_ALL,
150                     "movePackageInternal", ApplicationExitInfo.REASON_USER_REQUESTED);
151         }
152 
153         final Bundle extras = new Bundle();
154         extras.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
155         extras.putString(Intent.EXTRA_TITLE, label);
156         mPm.mMoveCallbacks.notifyCreated(moveId, extras);
157 
158         int installFlags;
159         final boolean moveCompleteApp;
160         final File measurePath;
161 
162         installFlags = INSTALL_INTERNAL;
163         if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid)) {
164             moveCompleteApp = true;
165             measurePath = Environment.getDataAppDirectory(volumeUuid);
166         } else if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) {
167             moveCompleteApp = false;
168             measurePath = storage.getPrimaryPhysicalVolume().getPath();
169         } else {
170             final VolumeInfo volume = storage.findVolumeByUuid(volumeUuid);
171             if (volume == null || volume.getType() != VolumeInfo.TYPE_PRIVATE
172                     || !volume.isMountedWritable()) {
173                 freezer.close();
174                 throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
175                         "Move location not mounted private volume");
176             }
177 
178             moveCompleteApp = true;
179             measurePath = Environment.getDataAppDirectory(volumeUuid);
180         }
181 
182         // If we're moving app data around, we need all the users unlocked
183         if (moveCompleteApp) {
184             for (int userId : installedUserIds) {
185                 if (StorageManager.isFileEncrypted() && !StorageManager.isUserKeyUnlocked(userId)) {
186                     freezer.close();
187                     throw new PackageManagerException(MOVE_FAILED_LOCKED_USER,
188                             "User " + userId + " must be unlocked");
189                 }
190             }
191         }
192 
193         final PackageStats stats = new PackageStats(null, -1);
194         synchronized (mPm.mInstallLock) {
195             for (int userId : installedUserIds) {
196                 if (!getPackageSizeInfoLI(packageName, userId, stats)) {
197                     freezer.close();
198                     throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
199                             "Failed to measure package size");
200                 }
201             }
202         }
203 
204         if (DEBUG_INSTALL) {
205             Slog.d(TAG, "Measured code size " + stats.codeSize + ", data size "
206                     + stats.dataSize);
207         }
208 
209         final long startFreeBytes = measurePath.getUsableSpace();
210         final long sizeBytes;
211         if (moveCompleteApp) {
212             sizeBytes = stats.codeSize + stats.dataSize;
213         } else {
214             sizeBytes = stats.codeSize;
215         }
216 
217         if (sizeBytes > storage.getStorageBytesUntilLow(measurePath)) {
218             freezer.close();
219             throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
220                     "Not enough free space to move");
221         }
222 
223         mPm.mMoveCallbacks.notifyStatusChanged(moveId, 10);
224 
225         final CountDownLatch installedLatch = new CountDownLatch(1);
226         final IPackageInstallObserver2 installObserver = new IPackageInstallObserver2.Stub() {
227             @Override
228             public void onUserActionRequired(Intent intent) throws RemoteException {
229                 freezer.close();
230                 throw new IllegalStateException();
231             }
232 
233             @Override
234             public void onPackageInstalled(String basePackageName, int returnCode, String msg,
235                     Bundle extras) throws RemoteException {
236                 if (DEBUG_INSTALL) {
237                     Slog.d(TAG, "Install result for move: "
238                             + PackageManager.installStatusToString(returnCode, msg));
239                 }
240 
241                 installedLatch.countDown();
242                 freezer.close();
243 
244                 final int status = PackageManager.installStatusToPublicStatus(returnCode);
245                 switch (status) {
246                     case PackageInstaller.STATUS_SUCCESS:
247                         mPm.mMoveCallbacks.notifyStatusChanged(moveId,
248                                 PackageManager.MOVE_SUCCEEDED);
249                         logAppMovedStorage(packageName, isCurrentLocationExternal);
250                         break;
251                     case PackageInstaller.STATUS_FAILURE_STORAGE:
252                         mPm.mMoveCallbacks.notifyStatusChanged(moveId,
253                                 PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE);
254                         break;
255                     default:
256                         mPm.mMoveCallbacks.notifyStatusChanged(moveId,
257                                 PackageManager.MOVE_FAILED_INTERNAL_ERROR);
258                         break;
259                 }
260             }
261         };
262 
263         final MoveInfo move;
264         if (moveCompleteApp) {
265             // Kick off a thread to report progress estimates
266             new Thread(() -> {
267                 while (true) {
268                     try {
269                         if (installedLatch.await(1, TimeUnit.SECONDS)) {
270                             break;
271                         }
272                     } catch (InterruptedException ignored) {
273                     }
274 
275                     final long deltaFreeBytes = startFreeBytes - measurePath.getUsableSpace();
276                     final int progress = 10 + (int) MathUtils.constrain(
277                             ((deltaFreeBytes * 80) / sizeBytes), 0, 80);
278                     mPm.mMoveCallbacks.notifyStatusChanged(moveId, progress);
279                 }
280             }).start();
281 
282             move = new MoveInfo(moveId, currentVolumeUuid, volumeUuid, packageName,
283                     appId, seinfo, targetSdkVersion, fromCodePath);
284         } else {
285             move = null;
286         }
287 
288         installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
289 
290         final OriginInfo origin = OriginInfo.fromExistingFile(codeFile);
291         final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
292         final ParseResult<PackageLite> ret = ApkLiteParseUtils.parsePackageLite(input,
293                 new File(origin.mResolvedPath), /* flags */ 0);
294         final PackageLite lite = ret.isSuccess() ? ret.getResult() : null;
295         final InstallingSession installingSession = new InstallingSession(origin, move,
296                 installObserver, installFlags, installSource, volumeUuid, user, packageAbiOverride,
297                 PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, lite, mPm);
298         installingSession.movePackage();
299     }
300 
301     /**
302      * Logs that an app has been moved from internal to external storage and vice versa.
303      * @param packageName The package that was moved.
304      */
logAppMovedStorage(String packageName, boolean isPreviousLocationExternal)305     private void logAppMovedStorage(String packageName, boolean isPreviousLocationExternal) {
306         final Computer snapshot = mPm.snapshotComputer();
307         final AndroidPackage pkg = snapshot.getPackage(packageName);
308         if (pkg == null) {
309             return;
310         }
311 
312         final StorageManager storage = mPm.mInjector.getSystemService(StorageManager.class);
313         VolumeInfo volume = storage.findVolumeByUuid(
314                 StorageManager.convert(pkg.getVolumeUuid()).toString());
315         int packageExternalStorageType = PackageManagerServiceUtils.getPackageExternalStorageType(
316                 volume, pkg.isExternalStorage());
317 
318         if (!isPreviousLocationExternal && pkg.isExternalStorage()) {
319             // Move from internal to external storage.
320             FrameworkStatsLog.write(FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED,
321                     packageExternalStorageType,
322                     FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED__MOVE_TYPE__TO_EXTERNAL,
323                     packageName);
324         } else if (isPreviousLocationExternal && !pkg.isExternalStorage()) {
325             // Move from external to internal storage.
326             FrameworkStatsLog.write(FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED,
327                     packageExternalStorageType,
328                     FrameworkStatsLog.APP_MOVED_STORAGE_REPORTED__MOVE_TYPE__TO_INTERNAL,
329                     packageName);
330         }
331     }
332 
333     @GuardedBy("mPm.mInstallLock")
getPackageSizeInfoLI(String packageName, int userId, PackageStats stats)334     private boolean getPackageSizeInfoLI(String packageName, int userId, PackageStats stats) {
335         final Computer snapshot = mPm.snapshotComputer();
336         final PackageStateInternal packageStateInternal =
337                 snapshot.getPackageStateInternal(packageName);
338         if (packageStateInternal == null) {
339             Slog.w(TAG, "Failed to find settings for " + packageName);
340             return false;
341         }
342 
343         final String[] packageNames = { packageName };
344         final long[] ceDataInodes = {
345                 packageStateInternal.getUserStateOrDefault(userId).getCeDataInode() };
346         final String[] codePaths = { packageStateInternal.getPathString() };
347 
348         try {
349             mPm.mInstaller.getAppSize(packageStateInternal.getVolumeUuid(), packageNames, userId,
350                     0, packageStateInternal.getAppId(), ceDataInodes, codePaths, stats);
351 
352             // For now, ignore code size of packages on system partition
353             if (PackageManagerServiceUtils.isSystemApp(packageStateInternal)
354                     && !PackageManagerServiceUtils.isUpdatedSystemApp(packageStateInternal)) {
355                 stats.codeSize = 0;
356             }
357 
358             // External clients expect these to be tracked separately
359             stats.dataSize -= stats.cacheSize;
360 
361         } catch (Installer.InstallerException e) {
362             Slog.w(TAG, String.valueOf(e));
363             return false;
364         }
365 
366         return true;
367     }
368 
369     public static class MoveCallbacks extends Handler {
370         private static final int MSG_CREATED = 1;
371         private static final int MSG_STATUS_CHANGED = 2;
372 
373         private final RemoteCallbackList<IPackageMoveObserver>
374                 mCallbacks = new RemoteCallbackList<>();
375 
376         public final SparseIntArray mLastStatus = new SparseIntArray();
377 
MoveCallbacks(Looper looper)378         public MoveCallbacks(Looper looper) {
379             super(looper);
380         }
381 
register(IPackageMoveObserver callback)382         public void register(IPackageMoveObserver callback) {
383             mCallbacks.register(callback);
384         }
385 
unregister(IPackageMoveObserver callback)386         public void unregister(IPackageMoveObserver callback) {
387             mCallbacks.unregister(callback);
388         }
389 
390         @Override
handleMessage(Message msg)391         public void handleMessage(Message msg) {
392             final SomeArgs args = (SomeArgs) msg.obj;
393             final int n = mCallbacks.beginBroadcast();
394             for (int i = 0; i < n; i++) {
395                 final IPackageMoveObserver callback = mCallbacks.getBroadcastItem(i);
396                 try {
397                     invokeCallback(callback, msg.what, args);
398                 } catch (RemoteException ignored) {
399                 }
400             }
401             mCallbacks.finishBroadcast();
402             args.recycle();
403         }
404 
invokeCallback(IPackageMoveObserver callback, int what, SomeArgs args)405         private void invokeCallback(IPackageMoveObserver callback, int what, SomeArgs args)
406                 throws RemoteException {
407             switch (what) {
408                 case MSG_CREATED: {
409                     callback.onCreated(args.argi1, (Bundle) args.arg2);
410                     break;
411                 }
412                 case MSG_STATUS_CHANGED: {
413                     callback.onStatusChanged(args.argi1, args.argi2, (long) args.arg3);
414                     break;
415                 }
416             }
417         }
418 
notifyCreated(int moveId, Bundle extras)419         public void notifyCreated(int moveId, Bundle extras) {
420             Slog.v(TAG, "Move " + moveId + " created " + extras.toString());
421 
422             final SomeArgs args = SomeArgs.obtain();
423             args.argi1 = moveId;
424             args.arg2 = extras;
425             obtainMessage(MSG_CREATED, args).sendToTarget();
426         }
427 
notifyStatusChanged(int moveId, int status)428         public void notifyStatusChanged(int moveId, int status) {
429             notifyStatusChanged(moveId, status, -1);
430         }
431 
notifyStatusChanged(int moveId, int status, long estMillis)432         public void notifyStatusChanged(int moveId, int status, long estMillis) {
433             Slog.v(TAG, "Move " + moveId + " status " + status);
434 
435             final SomeArgs args = SomeArgs.obtain();
436             args.argi1 = moveId;
437             args.argi2 = status;
438             args.arg3 = estMillis;
439             obtainMessage(MSG_STATUS_CHANGED, args).sendToTarget();
440 
441             synchronized (mLastStatus) {
442                 mLastStatus.put(moveId, status);
443             }
444         }
445     }
446 }
447