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.shell; 18 19 import android.annotation.IntDef; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.content.Intent; 23 import android.os.Bundle; 24 import android.util.Log; 25 26 import com.android.adservices.api.R; 27 import com.android.adservices.concurrency.AdServicesExecutors; 28 import com.android.adservices.service.DebugFlags; 29 import com.android.adservices.service.shell.AdServicesShellCommandHandler; 30 import com.android.adservices.service.shell.AdservicesShellCommandFactorySupplier; 31 import com.android.adservices.service.stats.AdServicesLoggerImpl; 32 33 import com.google.common.util.concurrent.ListeningExecutorService; 34 35 import java.io.FileDescriptor; 36 import java.io.PrintWriter; 37 import java.io.StringWriter; 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.Arrays; 41 import java.util.concurrent.CountDownLatch; 42 import java.util.concurrent.TimeUnit; 43 import java.util.stream.Collectors; 44 45 /** 46 * Activity to run shell command for Android R/S. 47 * 48 * <p>It is disabled by default in manifest and is also flag guarded. It doesn't have a launcher 49 * icon. It is first enabled using shell commands. We start an activity by sending an intent with 50 * all the arguments to run the adservices shell command. Then we use dumpsys to get the result of 51 * the already run command. After querying the result, the activity is closed and restored back to 52 * disabled state. Essentially Activity is only alive for the duration of the one command and once 53 * dump cmd is run, it is closed. 54 */ 55 public final class ShellCommandActivity extends Activity { 56 // TODO(b/308009734): Add data/info to the UI (input, status, output). 57 // TODO(b/308009734): Add an option to startForResult() that way CTS can use this instead of 58 // dumpsys. 59 private static final String TAG = "AdServicesShellCommand"; 60 private static final String CMD_ARGS = "cmd-args"; 61 private static final String GET_RESULT_ARG = "get-result"; 62 // This timeout is used to get result using dumpsys. The timeout should not be more than 5000 63 // millisecond. Otherwise dumpsys will throw timeout exception. 64 private static final long MAX_COMMAND_DURATION_MILLIS = 3000L; 65 66 private static final String SHELL_COMMAND_ACTIVITY_DUMP = "-- ShellCommandActivity dump --"; 67 68 private static final String COMMAND_STATUS = "CommandStatus"; 69 private static final String COMMAND_RES = "CommandRes"; 70 private static final String COMMAND_OUT = "CommandOut"; 71 private static final String COMMAND_ERR = "CommandErr"; 72 73 private static final int STATUS_RUNNING = 1; 74 private static final int STATUS_FINISHED = 2; 75 private static final int STATUS_TIMEOUT = 3; 76 77 @IntDef(value = {STATUS_RUNNING, STATUS_FINISHED, STATUS_TIMEOUT}) 78 @Retention(RetentionPolicy.SOURCE) 79 private @interface Status {} 80 81 private final StringWriter mOutSw = new StringWriter(); 82 private final PrintWriter mOutPw = new PrintWriter(mOutSw); 83 84 private final StringWriter mErrSw = new StringWriter(); 85 private final PrintWriter mErrPw = new PrintWriter(mErrSw); 86 87 private final CountDownLatch mLatch = new CountDownLatch(1); 88 89 private int mRes = -1; 90 private ListeningExecutorService mExecutorService; 91 private boolean mShellCommandEnabled; 92 runShellCommand()93 private void runShellCommand() { 94 if (!mShellCommandEnabled) { 95 Log.e(TAG, "Activity started when shell command is disabled"); 96 finishSelf(); 97 return; 98 } 99 100 Intent intent = getIntent(); 101 String[] args = intent.getStringArrayExtra(CMD_ARGS); 102 if (args == null || args.length == 0) { 103 Log.e(TAG, "command args is null or empty"); 104 finishSelf(); 105 return; 106 } 107 AdServicesShellCommandHandler handler = 108 new AdServicesShellCommandHandler( 109 mOutPw, 110 mErrPw, 111 new AdservicesShellCommandFactorySupplier(), 112 AdServicesLoggerImpl.getInstance()); 113 var unused = 114 mExecutorService.submit( 115 () -> { 116 mRes = handler.run(args); 117 Log.d(TAG, "Shell command completed with status: " + mRes); 118 mLatch.countDown(); 119 }); 120 } 121 122 @Override onCreate(Bundle savedInstanceState)123 public void onCreate(Bundle savedInstanceState) { 124 super.onCreate(savedInstanceState); 125 setContentView(R.layout.shell_command_activity); 126 127 mShellCommandEnabled = DebugFlags.getInstance().getAdServicesShellCommandEnabled(); 128 mExecutorService = AdServicesExecutors.getLightWeightExecutor(); 129 runShellCommand(); 130 } 131 waitAndGetResult(PrintWriter writer)132 private @Status int waitAndGetResult(PrintWriter writer) { 133 try { 134 if (mLatch.await(MAX_COMMAND_DURATION_MILLIS, TimeUnit.MILLISECONDS)) { 135 formatAndPrintResult(STATUS_FINISHED, mOutSw.toString(), mErrSw.toString(), writer); 136 return STATUS_FINISHED; 137 } 138 Log.e( 139 TAG, 140 String.format( 141 "Elapsed time: %d Millisecond. Command is still running. Please" 142 + " retry by calling get-result shell command\n", 143 MAX_COMMAND_DURATION_MILLIS)); 144 formatAndPrintResult(STATUS_RUNNING, mOutSw.toString(), mErrSw.toString(), writer); 145 return STATUS_RUNNING; 146 } catch (InterruptedException e) { 147 writer.println("Thread interrupted, failed to complete shell command"); 148 Thread.currentThread().interrupt(); 149 } 150 return STATUS_FINISHED; 151 } 152 153 @Override dump( String prefix, @Nullable FileDescriptor fd, PrintWriter writer, @Nullable String[] args)154 public void dump( 155 String prefix, 156 @Nullable FileDescriptor fd, 157 PrintWriter writer, 158 @Nullable String[] args) { 159 // Get the result after the starting activity using dumpsys. 160 if (args != null && args.length > 0 && args[0].equals("cmd")) { 161 getShellCommandResult(args, writer); 162 return; 163 } 164 165 super.dump(prefix, fd, writer, args); 166 } 167 getShellCommandResult(String[] args, PrintWriter writer)168 private void getShellCommandResult(String[] args, PrintWriter writer) { 169 if (!mShellCommandEnabled) { 170 Log.e( 171 TAG, 172 String.format( 173 "dump(%s) called on ShellCommandActivity when shell" 174 + " command flag was disabled", 175 Arrays.toString(args))); 176 return; 177 } 178 179 if (args.length == 2 && args[1].equals(GET_RESULT_ARG)) { 180 @Status int status = waitAndGetResult(writer); 181 if (status != STATUS_RUNNING) { 182 finishSelf(); 183 } 184 } else { 185 writer.printf( 186 "Invalid args %s. Supported args are 'cmd get-result'\n", 187 Arrays.toString(args)); 188 finishSelf(); 189 } 190 } 191 192 /* 193 Example formatted result: 194 -- ShellCommandActivity dump -- 195 CommandStatus: FINISHED 196 CommandRes: 0 197 CommandOut: 198 hello 199 CommandErr: 200 Something went wrong 201 */ formatAndPrintResult( @tatus int status, String out, String err, PrintWriter writer)202 private void formatAndPrintResult( 203 @Status int status, String out, String err, PrintWriter writer) { 204 writer.println(SHELL_COMMAND_ACTIVITY_DUMP); 205 writer.printf("%s: %s\n", COMMAND_STATUS, statusToString(status)); 206 207 if (status == STATUS_FINISHED || status == STATUS_TIMEOUT) { 208 writer.printf("%s: %d\n", COMMAND_RES, mRes); 209 } 210 if (!out.isEmpty()) { 211 indentAndPrefixStringWithSpace(out, COMMAND_OUT, writer); 212 } 213 if (!err.isEmpty()) { 214 indentAndPrefixStringWithSpace(err, COMMAND_ERR, writer); 215 } 216 } 217 indentAndPrefixStringWithSpace(String input, String name, PrintWriter writer)218 private void indentAndPrefixStringWithSpace(String input, String name, PrintWriter writer) { 219 String formattedOut = 220 Arrays.stream(input.strip().split("\n")) 221 .collect(Collectors.joining("\n ", " ", "")); 222 writer.printf("%s:\n%s\n", name, formattedOut); 223 } 224 statusToString(int status)225 private String statusToString(int status) { 226 switch (status) { 227 case STATUS_RUNNING: 228 return "RUNNING"; 229 case STATUS_FINISHED: 230 return "FINISHED"; 231 case STATUS_TIMEOUT: 232 return "TIMEOUT"; 233 default: 234 return "UNKNOWN-" + status; 235 } 236 } 237 finishSelf()238 private void finishSelf() { 239 mOutPw.close(); 240 finish(); 241 } 242 } 243