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