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