• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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