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