1 /* 2 * Copyright (C) 2024 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.adservices.common; 18 19 import com.android.adservices.shared.testing.AndroidSdk; 20 import com.android.adservices.shared.testing.Logger; 21 import com.android.adservices.shared.testing.Logger.RealLogger; 22 import com.android.adservices.shared.testing.Nullable; 23 import com.android.adservices.shared.testing.shell.CommandResult; 24 import com.android.internal.annotations.VisibleForTesting; 25 26 import com.google.common.base.Supplier; 27 import com.google.errorprone.annotations.FormatMethod; 28 import com.google.errorprone.annotations.FormatString; 29 30 /** Helper to run the AdServices service side shell command and return output and error. */ 31 public abstract class AbstractAdServicesShellCommandHelper { 32 protected static final String TAG = "AdServicesShellCommand"; 33 34 @VisibleForTesting 35 static final String SHELL_ACTIVITY_NAME = "com.android.adservices.shell.ShellCommandActivity"; 36 37 private static final String SHELL_ACTIVITY_INTENT = 38 "android.adservices.BACK_COMPACT_SHELL_COMMAND"; 39 40 private static final String INVALID_COMMAND_OUTPUT = "Unknown command:"; 41 42 private static final String SHELL_COMMAND_ACTIVITY_DUMP_STR = "-- ShellCommandActivity dump --"; 43 private static final String COMMAND_OUT = "CommandOut:"; 44 private static final String COMMAND_ERR = "CommandErr:"; 45 private static final String COMMAND_STATUS = "CommandStatus:"; 46 private static final String STATUS_FINISHED = "FINISHED"; 47 48 private static final String CMD_ARGS = "cmd-args"; 49 private static final String GET_RESULT_ARG = "get-result"; 50 51 @VisibleForTesting 52 static final String ADSERVICES_MANAGER_SERVICE_CHECK = "service check adservices_manager"; 53 54 private static final long WAIT_SAMPLE_INTERVAL_MILLIS = 1000; 55 private static final long TIMEOUT_ACTIVITY_FINISH_MILLIS = 3000; 56 57 // Max number of times we run the get-result shell command if the command is still running. 58 private static final long GET_RESULT_COMMAND_RETRY = 4; 59 60 private final Logger mLog; 61 private final AbstractDeviceSupportHelper mAdServicesHelper; 62 AbstractAdServicesShellCommandHelper( AbstractDeviceSupportHelper abstractAdServicesHelper, RealLogger logger)63 protected AbstractAdServicesShellCommandHelper( 64 AbstractDeviceSupportHelper abstractAdServicesHelper, RealLogger logger) { 65 mAdServicesHelper = abstractAdServicesHelper; 66 mLog = new Logger(logger, TAG); 67 } 68 69 /** 70 * Executes AdServices shell command and returns the standard output. 71 * 72 * <p>Before running the shell command, ensure {@code adservices_shell_command_enabled} flag is 73 * enabled. 74 * 75 * <p>For device T+, adservices_manager binds to the shell command service, runs the shell 76 * command and returns standard output. 77 * 78 * <p>For device R,S, we enable and start shell command activity and then call dumpsys to run 79 * the shell command. The dumpsys output produces both error and output as part of single 80 * string. 81 */ 82 @FormatMethod runCommand(@ormatString String cmdFmt, @Nullable Object... cmdArgs)83 public String runCommand(@FormatString String cmdFmt, @Nullable Object... cmdArgs) { 84 int level = getDeviceApiLevel(); 85 if (level >= AndroidSdk.TM) { 86 // For Android T, Check if the service adservices_manager is published or not. If 87 // it's not published, use sdk_sandbox to run the shell command. 88 String cmd = 89 (level == AndroidSdk.TM && !isAdServicesManagerServicePublished()) 90 ? String.format( 91 "cmd sdk_sandbox adservices-cmd %s", 92 String.format(cmdFmt, cmdArgs)) 93 : String.format( 94 "cmd adservices_manager %s", String.format(cmdFmt, cmdArgs)); 95 String res = runShellCommand(cmd); 96 mLog.d("Output for command %s: %s", cmd, res); 97 return res; 98 } 99 CommandResult commandResult = runShellCommandRS(String.format(cmdFmt, cmdArgs)); 100 return commandResult.getOut(); 101 } 102 103 /** 104 * Executes AdServices shell command and returns the standard output and standard error wrapped 105 * in a {@link CommandResult}. 106 * 107 * <p>Before running the shell command, ensure {@code adservices_shell_command_enabled} flag is 108 * enabled. 109 * 110 * <p>For device T+, adservices_manager binds to the shell command service, runs the shell 111 * command and returns both standard output and standard error as part of {@link CommandResult}. 112 * 113 * <p>For device R,S, we enable and start the shell command activity and then call dumpsys to 114 * run the shell command. The dumpsys output produces both error and output as part of single 115 * string. We will populate the {@link CommandResult} {@code out} field with this string. The 116 * caller would need to infer from the {@code out} field whether it's actually standard output 117 * or error. 118 */ 119 @FormatMethod runCommandRwe(@ormatString String cmdFmt, @Nullable Object... cmdArgs)120 public CommandResult runCommandRwe(@FormatString String cmdFmt, @Nullable Object... cmdArgs) { 121 int level = getDeviceApiLevel(); 122 if (level >= AndroidSdk.TM) { 123 // For Android T, Check if the service adservices_manager is published or not. If 124 // it's not published, use sdk_sandbox to run the shell command. 125 // For Android T, Check if the service adservices_manager is published or not. If 126 // it's not published, use sdk_sandbox to run the shell command. 127 String cmd = 128 (level == AndroidSdk.TM && !isAdServicesManagerServicePublished()) 129 ? String.format( 130 "cmd sdk_sandbox adservices-cmd %s", 131 String.format(cmdFmt, cmdArgs)) 132 : String.format( 133 "cmd adservices_manager %s", String.format(cmdFmt, cmdArgs)); 134 CommandResult res = runShellCommandRwe(cmd); 135 mLog.d("Output for command %s: %s", cmd, res); 136 return res; 137 } 138 139 return runShellCommandRS(String.format(cmdFmt, cmdArgs)); 140 } 141 142 /** Executes a shell command and returns the standard output. */ 143 // TODO(b/324491698): Provide an abstraction for runShellCommand. runShellCommand(String cmd)144 protected abstract String runShellCommand(String cmd); 145 146 /** 147 * Executes a shell command and returns the standard output and standard error wrapped in a 148 * {@link CommandResult}. 149 */ runShellCommandRwe(String cmd)150 protected abstract CommandResult runShellCommandRwe(String cmd); 151 152 // TODO(b/324491709): Provide an abstraction for sdk device level. 153 154 /** Gets the device API level. */ getDeviceApiLevel()155 protected abstract int getDeviceApiLevel(); 156 runShellCommandRS(String cmd)157 private CommandResult runShellCommandRS(String cmd) { 158 String[] argsList = cmd.split("\\s+"); 159 String args = String.join(",", argsList); 160 String res = runShellCommand(startShellActivity(args)); 161 mLog.d("Output for command %s: %s", startShellActivity(args), res); 162 163 String componentName = 164 String.format( 165 "%s/%s", mAdServicesHelper.getAdServicesPackageName(), SHELL_ACTIVITY_NAME); 166 CommandResult commandRes = runGetResultShellCommand(componentName); 167 168 checkShellCommandActivityFinished(componentName); 169 return commandRes; 170 } 171 172 /* Parses the output from dumpsys. 173 174 Sample dumpsys output: 175 TASK 10145:com.google.android.ext.services id=13 userId=0 176 ACTIVITY com.google.android.ext.services/com.android.adservices.shell.ShellCommandActivity 177 -- ShellCommandActivity dump -- 178 CommandStatus: FINISHED 179 CommandRes: 0 180 CommandOut: 181 hello 182 parsed Output: CommandResult(hello,"") 183 */ 184 185 @VisibleForTesting parseResultFromDumpsys(String res)186 CommandResult parseResultFromDumpsys(String res) { 187 String separator = "\n"; 188 String[] splitStr = res.split(separator); 189 int len = splitStr.length; 190 191 boolean activityDumpPresent = false; 192 String out = ""; 193 String err = ""; 194 String commandStatus = STATUS_FINISHED; 195 for (int i = 0; i < len; i++) { 196 if (splitStr[i].equals(SHELL_COMMAND_ACTIVITY_DUMP_STR)) { 197 activityDumpPresent = true; 198 } else if (activityDumpPresent && splitStr[i].startsWith(COMMAND_STATUS)) { 199 commandStatus = splitStr[i].substring(COMMAND_STATUS.length()).strip(); 200 } else { 201 if (activityDumpPresent && splitStr[i].equals(COMMAND_OUT)) { 202 i++; 203 StringBuilder outBuilder = new StringBuilder(); 204 for (; i < len && splitStr[i].startsWith(" "); i++) { 205 if (splitStr[i].length() > 2) { 206 outBuilder.append(splitStr[i].substring(2)); 207 } 208 outBuilder.append('\n'); 209 } 210 out = outBuilder.toString().strip(); 211 } 212 213 if (i < len && activityDumpPresent && splitStr[i].equals(COMMAND_ERR)) { 214 i++; 215 StringBuilder errBuilder = new StringBuilder(); 216 for (; i < len && splitStr[i].startsWith(" "); i++) { 217 if (splitStr[i].length() > 2) { 218 errBuilder.append(splitStr[i].substring(2)); 219 } 220 errBuilder.append("\n"); 221 } 222 err = errBuilder.toString().strip(); 223 } 224 } 225 } 226 227 // Return original input if activity dump string not present. 228 if (!activityDumpPresent) { 229 return new CommandResult(res, "", commandStatus); 230 } 231 return new CommandResult(out, err, commandStatus); 232 } 233 234 @VisibleForTesting getDumpsysGetResultShellCommand(String componentName)235 String getDumpsysGetResultShellCommand(String componentName) { 236 return String.format("dumpsys activity %s cmd %s", componentName, GET_RESULT_ARG); 237 } 238 runGetResultShellCommand(String componentName)239 private CommandResult runGetResultShellCommand(String componentName) { 240 CommandResult commandRes = new CommandResult("", ""); 241 for (int i = 0; i < GET_RESULT_COMMAND_RETRY; i++) { 242 String res = runShellCommand(getDumpsysGetResultShellCommand(componentName)); 243 mLog.d( 244 "Output for command %s running %d times: %s ", 245 getDumpsysGetResultShellCommand(componentName), i + 1, res); 246 commandRes = parseResultFromDumpsys(res); 247 if (!commandRes.isCommandRunning()) { 248 return commandRes; 249 } 250 } 251 return commandRes; 252 } 253 startShellActivity(String args)254 private String startShellActivity(String args) { 255 return String.format( 256 "am start -W -a %s --esa %s %s", SHELL_ACTIVITY_INTENT, CMD_ARGS, args); 257 } 258 isAdServicesManagerServicePublished()259 boolean isAdServicesManagerServicePublished() { 260 String out = runShellCommand(ADSERVICES_MANAGER_SERVICE_CHECK); 261 return !out.contains("not found"); 262 } 263 checkShellCommandActivityFinished(String componentName)264 private void checkShellCommandActivityFinished(String componentName) { 265 mLog.d("Checking if ShellCommandActivity is finished"); 266 tryWaitForSuccess( 267 () -> { 268 String res = runShellCommand(getDumpsysGetResultShellCommand(componentName)); 269 mLog.d( 270 "Output for command %s: %s", 271 getDumpsysGetResultShellCommand(componentName), res); 272 return res.contains(INVALID_COMMAND_OUTPUT); 273 }, 274 "Failed to finish ShellCommandActivity", 275 TIMEOUT_ACTIVITY_FINISH_MILLIS); 276 } 277 278 // TODO(b/328107990): Create a generic method and move this to a CTS helper class. tryWaitForSuccess( Supplier<Boolean> successCondition, String failureMessage, long maxTimeoutMillis)279 private void tryWaitForSuccess( 280 Supplier<Boolean> successCondition, String failureMessage, long maxTimeoutMillis) { 281 long epoch = System.currentTimeMillis(); 282 while (System.currentTimeMillis() - epoch <= maxTimeoutMillis) { 283 try { 284 mLog.d("Sleep for %dms before we check for result", WAIT_SAMPLE_INTERVAL_MILLIS); 285 Thread.sleep(WAIT_SAMPLE_INTERVAL_MILLIS); 286 if (successCondition.get()) { 287 mLog.d("ShellCommandActivity is finished"); 288 return; 289 } 290 } catch (InterruptedException e) { 291 mLog.e("Thread interrupted, %s", failureMessage); 292 Thread.currentThread().interrupt(); 293 } 294 } 295 mLog.e("Timeout %dms happened, %s", maxTimeoutMillis, failureMessage); 296 } 297 } 298