1 /*
2  * Copyright (C) 2020 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.csuite.core;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 
21 import com.android.tradefed.device.DeviceNotAvailableException;
22 import com.android.tradefed.device.ITestDevice;
23 import com.android.tradefed.log.LogUtil.CLog;
24 import com.android.tradefed.targetprep.TargetSetupError;
25 import com.android.tradefed.util.CommandResult;
26 import com.android.tradefed.util.CommandStatus;
27 
28 import com.google.common.annotations.VisibleForTesting;
29 
30 import java.nio.file.Paths;
31 import java.util.Arrays;
32 
33 /**
34  * Uninstalls a system app.
35  *
36  * <p>This utility class may not restore the uninstalled system app after test completes.
37  *
38  * <p>The class may disable dm verity on some devices, and it does not re-enable it after
39  * uninstalling a system app.
40  */
41 public final class SystemPackageUninstaller {
42     @VisibleForTesting static final String OPTION_PACKAGE_NAME = "package-name";
43     static final String SYSPROP_DEV_BOOTCOMPLETE = "dev.bootcomplete";
44     static final String SYSPROP_SYS_BOOT_COMPLETED = "sys.boot_completed";
45     static final long WAIT_FOR_BOOT_COMPLETE_TIMEOUT_MILLIS = 1000 * 60;
46     @VisibleForTesting static final int MAX_NUMBER_OF_UPDATES = 100;
47     @VisibleForTesting static final String PM_CHECK_COMMAND = "pm path android";
48 
uninstallPackage(String packageName, ITestDevice device)49     public static void uninstallPackage(String packageName, ITestDevice device)
50             throws TargetSetupError, DeviceNotAvailableException {
51         checkNotNull(packageName);
52 
53         if (!isPackageManagerRunning(device)) {
54             CLog.w(
55                     "Package manager is not available on the device."
56                             + " Attempting to recover it by restarting the framework.");
57             runAsRoot(
58                     device,
59                     () -> {
60                         stopFramework(device);
61                         startFramework(device);
62                     });
63             if (!isPackageManagerRunning(device)) {
64                 throw new TargetSetupError(
65                         "The package manager failed to start.", device.getDeviceDescriptor());
66             }
67         }
68 
69         if (!isPackageInstalled(packageName, device)) {
70             CLog.i("Package %s is not installed.", packageName);
71             return;
72         }
73 
74         // Attempts to uninstall the package/updates from user partition.
75         // This method should be called before the other methods and requires
76         // the framework to be running.
77         removePackageUpdates(packageName, device);
78 
79         if (!isPackageInstalled(packageName, device)) {
80             CLog.i("Package %s has been removed.", packageName);
81             return;
82         }
83 
84         String packageInstallDirectory = getPackageInstallDirectory(packageName, device);
85         CLog.d("Install directory for package %s is %s", packageName, packageInstallDirectory);
86 
87         if (!isPackagePathSystemApp(packageInstallDirectory)) {
88             CLog.w("%s is not a system app, skipping", packageName);
89             return;
90         }
91 
92         CLog.i("Uninstalling system app %s", packageName);
93 
94         runWithWritableFilesystem(
95                 device,
96                 () ->
97                         runWithFrameworkOff(
98                                 device,
99                                 () -> {
100                                     removePackageInstallDirectory(packageInstallDirectory, device);
101                                     removePackageData(packageName, device);
102                                 }));
103     }
104 
105     private interface PreparerTask {
run()106         void run() throws TargetSetupError, DeviceNotAvailableException;
107     }
108 
runWithFrameworkOff(ITestDevice device, PreparerTask action)109     private static void runWithFrameworkOff(ITestDevice device, PreparerTask action)
110             throws TargetSetupError, DeviceNotAvailableException {
111         stopFramework(device);
112 
113         try {
114             action.run();
115         } finally {
116             startFramework(device);
117         }
118     }
119 
runWithWritableFilesystem(ITestDevice device, PreparerTask action)120     private static void runWithWritableFilesystem(ITestDevice device, PreparerTask action)
121             throws TargetSetupError, DeviceNotAvailableException {
122         runAsRoot(
123                 device,
124                 () -> {
125                     // TODO(yuexima): The remountSystemWritable method may internally disable dm
126                     // verity on some devices. Consider restoring verity which would require a
127                     // reboot.
128                     device.remountSystemWritable();
129 
130                     try {
131                         action.run();
132                     } finally {
133                         remountSystemReadOnly(device);
134                     }
135                 });
136     }
137 
runAsRoot(ITestDevice device, PreparerTask action)138     private static void runAsRoot(ITestDevice device, PreparerTask action)
139             throws TargetSetupError, DeviceNotAvailableException {
140         boolean disableRootAfterUninstall = false;
141 
142         if (!device.isAdbRoot()) {
143             if (!device.enableAdbRoot()) {
144                 throw new TargetSetupError(
145                         "Failed to enable adb root", device.getDeviceDescriptor());
146             }
147 
148             disableRootAfterUninstall = true;
149         }
150 
151         try {
152             action.run();
153         } finally {
154             if (disableRootAfterUninstall && !device.disableAdbRoot()) {
155                 throw new TargetSetupError(
156                         "Failed to disable adb root", device.getDeviceDescriptor());
157             }
158         }
159     }
160 
stopFramework(ITestDevice device)161     private static void stopFramework(ITestDevice device)
162             throws TargetSetupError, DeviceNotAvailableException {
163         // 'stop' is a blocking command.
164         executeShellCommandOrThrow(device, "stop", "Failed to stop framework");
165         // Set the boot complete flags to false. When the framework is started again, both flags
166         // will be set to true by the system upon the completion of restarting. This allows
167         // ITestDevice#waitForBootComplete to wait for framework start, and it only works
168         // when adb is rooted.
169         device.setProperty(SYSPROP_SYS_BOOT_COMPLETED, "0");
170         device.setProperty(SYSPROP_DEV_BOOTCOMPLETE, "0");
171     }
172 
startFramework(ITestDevice device)173     private static void startFramework(ITestDevice device)
174             throws TargetSetupError, DeviceNotAvailableException {
175         // 'start' is a non-blocking command.
176         executeShellCommandOrThrow(device, "start", "Failed to start framework");
177         // This wait only blocks if the boot completed flags are set to 0.
178         device.waitForBootComplete(WAIT_FOR_BOOT_COMPLETE_TIMEOUT_MILLIS);
179     }
180 
executeShellCommandOrThrow( ITestDevice device, String command, String failureMessage)181     private static CommandResult executeShellCommandOrThrow(
182             ITestDevice device, String command, String failureMessage)
183             throws TargetSetupError, DeviceNotAvailableException {
184         CommandResult commandResult = device.executeShellV2Command(command);
185 
186         if (commandResult.getStatus() != CommandStatus.SUCCESS) {
187             throw new TargetSetupError(
188                     String.format("%s; Command result: %s", failureMessage, commandResult),
189                     device.getDeviceDescriptor());
190         }
191 
192         return commandResult;
193     }
194 
executeShellCommandOrLog( ITestDevice device, String command, String failureMessage)195     private static CommandResult executeShellCommandOrLog(
196             ITestDevice device, String command, String failureMessage)
197             throws DeviceNotAvailableException {
198         CommandResult commandResult = device.executeShellV2Command(command);
199         if (commandResult.getStatus() != CommandStatus.SUCCESS) {
200             CLog.e("%s. Command result: %s", failureMessage, commandResult);
201         }
202 
203         return commandResult;
204     }
205 
remountSystemReadOnly(ITestDevice device)206     private static void remountSystemReadOnly(ITestDevice device)
207             throws TargetSetupError, DeviceNotAvailableException {
208         executeShellCommandOrThrow(
209                 device,
210                 "mount -o ro,remount /system",
211                 "Failed to remount system partition as read only");
212     }
213 
isPackagePathSystemApp(String packagePath)214     private static boolean isPackagePathSystemApp(String packagePath) {
215         return packagePath.startsWith("/system/") || packagePath.startsWith("/product/");
216     }
217 
removePackageInstallDirectory( String packageInstallDirectory, ITestDevice device)218     private static void removePackageInstallDirectory(
219             String packageInstallDirectory, ITestDevice device)
220             throws TargetSetupError, DeviceNotAvailableException {
221         CLog.i("Removing package install directory %s", packageInstallDirectory);
222         executeShellCommandOrThrow(
223                 device,
224                 String.format("rm -r %s", packageInstallDirectory),
225                 String.format(
226                         "Failed to remove system app package path %s", packageInstallDirectory));
227     }
228 
removePackageUpdates(String packageName, ITestDevice device)229     private static void removePackageUpdates(String packageName, ITestDevice device)
230             throws TargetSetupError, DeviceNotAvailableException {
231         CLog.i("Removing package updates for %s", packageName);
232 
233         // A system package may have update packages. If so, each `adb uninstall` call
234         // only uninstalls the latest update. To remove all update packages we can
235         // call uninstall repeatedly until the command fails.
236         for (int i = 0; i < MAX_NUMBER_OF_UPDATES; i++) {
237             String errMsg = device.uninstallPackage(packageName);
238             if (errMsg != null) {
239                 CLog.d("Completed removing updates as the uninstall command returned: %s", errMsg);
240                 return;
241             }
242             CLog.i("Removed an update package for %s", packageName);
243         }
244 
245         throw new TargetSetupError(
246                 "Too many updates were uninstalled. Something must be wrong.",
247                 device.getDeviceDescriptor());
248     }
249 
removePackageData(String packageName, ITestDevice device)250     private static void removePackageData(String packageName, ITestDevice device)
251             throws DeviceNotAvailableException {
252         String dataPath = String.format("/data/data/%s", packageName);
253         CLog.i("Removing package data directory for %s", dataPath);
254         executeShellCommandOrLog(
255                 device,
256                 String.format("rm -r %s", dataPath),
257                 String.format(
258                         "Failed to remove system app data %s from %s", packageName, dataPath));
259     }
260 
isPackageManagerRunning(ITestDevice device)261     private static boolean isPackageManagerRunning(ITestDevice device)
262             throws DeviceNotAvailableException {
263         return device.executeShellV2Command(PM_CHECK_COMMAND).getStatus() == CommandStatus.SUCCESS;
264     }
265 
isPackageInstalled(String packageName, ITestDevice device)266     private static boolean isPackageInstalled(String packageName, ITestDevice device)
267             throws TargetSetupError, DeviceNotAvailableException {
268         CommandResult commandResult =
269                 executeShellCommandOrThrow(
270                         device,
271                         String.format("pm list packages %s", packageName),
272                         "Failed to execute pm command");
273 
274         if (commandResult.getStdout() == null) {
275             throw new TargetSetupError(
276                     String.format("Failed to get pm command output: %s", commandResult.getStdout()),
277                     device.getDeviceDescriptor());
278         }
279 
280         return Arrays.asList(commandResult.getStdout().split("\\r?\\n"))
281                 .contains(String.format("package:%s", packageName));
282     }
283 
getPackageInstallDirectory(String packageName, ITestDevice device)284     private static String getPackageInstallDirectory(String packageName, ITestDevice device)
285             throws TargetSetupError, DeviceNotAvailableException {
286         CommandResult commandResult =
287                 executeShellCommandOrThrow(
288                         device,
289                         String.format("pm path %s", packageName),
290                         "Failed to execute pm command");
291 
292         if (commandResult.getStdout() == null
293                 || !commandResult.getStdout().startsWith("package:")) {
294             throw new TargetSetupError(
295                     String.format(
296                             "Failed to get pm path command output %s", commandResult.getStdout()),
297                     device.getDeviceDescriptor());
298         }
299 
300         String packageInstallPath = commandResult.getStdout().substring("package:".length());
301         return Paths.get(packageInstallPath).getParent().toString();
302     }
303 }
304