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 17import math 18import os 19import subprocess 20import sys 21import time 22from .handle_input import HandleInput 23from .validation_error import ValidationError 24 25ADB_ROOT_TIMED_OUT_LIMIT_SECS = 5 26ADB_BOOT_COMPLETED_TIMED_OUT_LIMIT_SECS = 30 27POLLING_INTERVAL_SECS = 0.5 28SIMPLEPERF_TRACE_FILE = "/data/misc/perfetto-traces/perf.data" 29 30class AdbDevice: 31 """ 32 Class representing a device. APIs interact with the current device through 33 the adb bridge. 34 """ 35 def __init__(self, serial): 36 self.serial = serial 37 38 @staticmethod 39 def get_adb_devices(): 40 """ 41 Returns a list of devices connected to the adb bridge. 42 The output of the command 'adb devices' is expected to be of the form: 43 List of devices attached 44 SOMEDEVICE1234 device 45 device2:5678 device 46 """ 47 command_output = subprocess.run(["adb", "devices"], capture_output=True) 48 output_lines = command_output.stdout.decode("utf-8").split("\n") 49 devices = [] 50 for line in output_lines[:-2]: 51 if line[0] == "*" or line == "List of devices attached": 52 continue 53 words_in_line = line.split('\t') 54 if words_in_line[1] == "device": 55 devices.append(words_in_line[0]) 56 return devices 57 58 def check_device_connection(self): 59 devices = self.get_adb_devices() 60 if len(devices) == 0: 61 return ValidationError("There are currently no devices connected.", None) 62 if self.serial is not None: 63 if self.serial not in devices: 64 return ValidationError(("Device with serial %s is not connected." 65 % self.serial), None) 66 elif "ANDROID_SERIAL" in os.environ: 67 if os.environ["ANDROID_SERIAL"] not in devices: 68 return ValidationError(("Device with serial %s is set as environment" 69 " variable, ANDROID_SERIAL, but is not" 70 " connected." 71 % os.environ["ANDROID_SERIAL"]), None) 72 self.serial = os.environ["ANDROID_SERIAL"] 73 elif len(devices) == 1: 74 self.serial = devices[0] 75 else: 76 options = "" 77 choices = {} 78 for i, device in enumerate(devices): 79 options += ("%d: torq --serial %s %s\n\t" 80 % (i, device, " ".join(sys.argv[1:]))) 81 # Lambdas are bound to local scope, so assign var d to prevent 82 # future values of device from overriding the current value we want 83 choices[str(i)] = lambda d=device: d 84 # Remove last \t 85 options = options[:-1] 86 chosen_serial = (HandleInput("There is more than one device currently " 87 "connected. Press the corresponding number " 88 "for the following options to choose the " 89 "device you want to use.\n\t%sSelect " 90 "device[0-%d]: " 91 % (options, len(devices) - 1), 92 "Please select a valid option.", 93 choices) 94 .handle_input()) 95 if isinstance(chosen_serial, ValidationError): 96 return chosen_serial 97 print("Using device with serial %s" % chosen_serial) 98 self.serial = chosen_serial 99 return None 100 101 @staticmethod 102 def poll_is_task_completed(timed_out_limit, interval, check_is_completed): 103 start_time = time.time() 104 while True: 105 time.sleep(interval) 106 if check_is_completed(): 107 return True 108 if time.time() - start_time > timed_out_limit: 109 return False 110 111 def root_device(self): 112 subprocess.run(["adb", "-s", self.serial, "root"]) 113 if not self.poll_is_task_completed(ADB_ROOT_TIMED_OUT_LIMIT_SECS, 114 POLLING_INTERVAL_SECS, 115 lambda: self.serial in 116 self.get_adb_devices()): 117 raise Exception(("Device with serial %s took too long to reconnect after" 118 " being rooted." % self.serial)) 119 120 def remove_file(self, file_path): 121 subprocess.run(["adb", "-s", self.serial, "shell", "rm", "-f", file_path]) 122 123 def start_perfetto_trace(self, config): 124 return subprocess.Popen(("adb -s %s shell perfetto -c - --txt -o" 125 " /data/misc/perfetto-traces/" 126 "trace.perfetto-trace %s" 127 % (self.serial, config)), shell=True) 128 129 def start_simpleperf_trace(self, command): 130 events_param = "-e " + ",".join(command.simpleperf_event) 131 return subprocess.Popen(("adb -s %s shell simpleperf record -a -f 1000 " 132 "--exclude-perf --post-unwind=yes -m 8192 -g " 133 "--duration %d %s -o %s" 134 % (self.serial, 135 int(math.ceil(command.dur_ms/1000)), 136 events_param, SIMPLEPERF_TRACE_FILE)), 137 shell=True) 138 139 def pull_file(self, file_path, host_file): 140 subprocess.run(["adb", "-s", self.serial, "pull", file_path, host_file]) 141 142 def get_all_users(self): 143 command_output = subprocess.run(["adb", "-s", self.serial, "shell", "pm", 144 "list", "users"], capture_output=True) 145 output_lines = command_output.stdout.decode("utf-8").split("\n")[1:-1] 146 return [int((line.split("{", 1)[1]).split(":", 1)[0]) for line in 147 output_lines] 148 149 def user_exists(self, user): 150 users = self.get_all_users() 151 if user not in users: 152 return ValidationError(("User ID %s does not exist on device with serial" 153 " %s." % (user, self.serial)), 154 ("Select from one of the following user IDs on" 155 " device with serial %s: %s" 156 % (self.serial, ", ".join(map(str, users))))) 157 return None 158 159 def get_current_user(self): 160 command_output = subprocess.run(["adb", "-s", self.serial, "shell", "am", 161 "get-current-user"], capture_output=True) 162 return int(command_output.stdout.decode("utf-8").split()[0]) 163 164 def perform_user_switch(self, user): 165 subprocess.run(["adb", "-s", self.serial, "shell", "am", "switch-user", 166 str(user)]) 167 168 def write_to_file(self, file_path, host_file_string): 169 subprocess.run(("adb -s %s shell 'cat > %s %s'" 170 % (self.serial, file_path, host_file_string)), shell=True) 171 172 def set_prop(self, prop, value): 173 subprocess.run(["adb", "-s", self.serial, "shell", "setprop", prop, value]) 174 175 def clear_prop(self, prop): 176 subprocess.run(["adb", "-s", self.serial, "shell", "setprop", prop, "\"\""]) 177 178 def reboot(self): 179 subprocess.run(["adb", "-s", self.serial, "reboot"]) 180 if not self.poll_is_task_completed(ADB_ROOT_TIMED_OUT_LIMIT_SECS, 181 POLLING_INTERVAL_SECS, 182 lambda: self.serial not in 183 self.get_adb_devices()): 184 raise Exception(("Device with serial %s took too long to start" 185 " rebooting." % self.serial)) 186 187 def wait_for_device(self): 188 subprocess.run(["adb", "-s", self.serial, "wait-for-device"]) 189 190 def is_boot_completed(self): 191 command_output = subprocess.run(["adb", "-s", self.serial, "shell", 192 "getprop", "sys.boot_completed"], 193 capture_output=True) 194 return command_output.stdout.decode("utf-8").strip() == "1" 195 196 def wait_for_boot_to_complete(self): 197 if not self.poll_is_task_completed(ADB_BOOT_COMPLETED_TIMED_OUT_LIMIT_SECS, 198 POLLING_INTERVAL_SECS, 199 self.is_boot_completed): 200 raise Exception(("Device with serial %s took too long to finish" 201 " rebooting." % self.serial)) 202 203 def get_packages(self): 204 return [package.removeprefix("package:") for package in subprocess.run( 205 ["adb", "-s", self.serial, "shell", "pm", "list", "packages"], 206 capture_output=True).stdout.decode("utf-8").splitlines()] 207 208 def get_pid(self, package): 209 return subprocess.run("adb -s %s shell pidof %s" % (self.serial, package), 210 shell=True, capture_output=True 211 ).stdout.decode("utf-8").split("\n")[0] 212 213 def is_package_running(self, package): 214 return self.get_pid(package) != "" 215 216 def start_package(self, package): 217 if subprocess.run( 218 ["adb", "-s", self.serial, "shell", "am", "start", package], 219 capture_output=True).stderr.decode("utf-8").split("\n")[0] != "": 220 return ValidationError(("Cannot start package %s on device with" 221 " serial %s because %s is a service package," 222 " which doesn't implement a MAIN activity." 223 % (package, self.serial, package)), None) 224 return None 225 226 def kill_pid(self, package): 227 pid = self.get_pid(package) 228 if pid != "": 229 subprocess.run(["adb", "-s", self.serial, "shell", "kill", "-9", pid]) 230 231 def force_stop_package(self, package): 232 subprocess.run(["adb", "-s", self.serial, "shell", "am", "force-stop", 233 package]) 234 235 def get_prop(self, prop): 236 return subprocess.run( 237 ["adb", "-s", self.serial, "shell", "getprop", prop], 238 capture_output=True).stdout.decode("utf-8").split("\n")[0] 239 240 def get_android_sdk_version(self): 241 return int(self.get_prop("ro.build.version.sdk")) 242 243 def simpleperf_event_exists(self, simpleperf_events): 244 events_copy = simpleperf_events.copy() 245 grep_command = "grep" 246 for event in simpleperf_events: 247 grep_command += " -e " + event.lower() 248 249 output = subprocess.run(["adb", "-s", self.serial, "shell", 250 "simpleperf", "list", "|", grep_command], 251 capture_output=True) 252 253 if output is None or len(output.stdout) == 0: 254 if "not found" in output.stderr.decode("utf-8"): 255 return ValidationError("Simpleperf was not found in the device", 256 "Push the simpleperf binary to the device") 257 raise Exception("Error while validating simpleperf events.") 258 lines = output.stdout.decode("utf-8").split("\n") 259 260 # Anything that does not start with two spaces is not a command. 261 # Any command with a space will have the command before the first space. 262 for line in lines: 263 if len(line) <= 3 or line[:2] != " " or line[2] == "#": 264 # Line doesn't contain a simpleperf event 265 continue 266 event = line[2:].split(" ")[0] 267 if event in events_copy: 268 events_copy.remove(event) 269 if len(events_copy) == 0: 270 # All of the events exist, exit early 271 break 272 273 if len(events_copy) > 0: 274 return ValidationError("The following simpleperf event(s) are invalid:" 275 " %s." 276 % events_copy, 277 "Run adb shell simpleperf list to" 278 " see valid simpleperf events.") 279 return None 280