1 /* 2 * Copyright (C) 2018 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 package com.android.tradefed.targetprep; 17 18 import com.android.helper.aoa.AoaDevice; 19 import com.android.helper.aoa.UsbHelper; 20 import com.android.tradefed.config.Option; 21 import com.android.tradefed.config.OptionClass; 22 import com.android.tradefed.device.DeviceNotAvailableException; 23 import com.android.tradefed.device.ITestDevice; 24 import com.android.tradefed.invoker.TestInformation; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.util.RegexTrie; 27 28 import com.google.common.annotations.VisibleForTesting; 29 30 import java.awt.Point; 31 import java.time.Duration; 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.HashMap; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.function.BiConsumer; 38 import java.util.regex.Matcher; 39 import java.util.regex.Pattern; 40 import java.util.stream.Collectors; 41 import java.util.stream.Stream; 42 43 /** 44 * {@link ITargetPreparer} that executes a series of actions (e.g. clicks and swipes) using the 45 * Android Open Accessory (AOAv2) protocol. This allows controlling an Android device without 46 * enabling USB debugging. 47 * 48 * <p>Accepts a list of strings which correspond to {@link AoaDevice} methods: 49 * 50 * <ul> 51 * <li>Click using x and y coordinates, e.g. "click 0 0" or "longClick 360 640". 52 * <li>Swipe between two sets of coordinates in a specified number of milliseconds, e.g. "swipe 0 53 * 0 100 360 640" to swipe from (0, 0) to (360, 640) in 100 milliseconds. 54 * <li>Write a string of alphanumeric text, e.g. "write hello world". 55 * <li>Press a combination of keys, e.g. "key RIGHT 2*TAB ENTER". 56 * <li>Wake up the device with "wake". 57 * <li>Press the home button with "home". 58 * <li>Press the back button with "back". 59 * <li>Wait for specified number of milliseconds, e.g. "sleep 1000" to wait for 1000 milliseconds. 60 * </ul> 61 */ 62 @OptionClass(alias = "aoa-preparer") 63 public class AoaTargetPreparer extends BaseTargetPreparer { 64 65 private static final String POINT = "(\\d{1,3}) (\\d{1,3})"; 66 private static final Pattern KEY = Pattern.compile("\\s+(?:(\\d+)\\*)?([a-zA-Z0-9]+)"); 67 68 @FunctionalInterface 69 private interface Action extends BiConsumer<AoaDevice, List<String>> {} 70 71 // Trie of possible actions, parses the input string and determines the operation to execute 72 private static final RegexTrie<Action> ACTIONS = new RegexTrie<>(); 73 74 static { 75 // clicks 76 ACTIONS.put( 77 (device, args) -> device.click(parsePoint(args.get(0), args.get(1))), 78 String.format("click %s", POINT)); 79 ACTIONS.put( 80 (device, args) -> device.longClick(parsePoint(args.get(0), args.get(1))), 81 String.format("longClick %s", POINT)); 82 83 // swipes ACTIONS.put(device, args)84 ACTIONS.put( 85 (device, args) -> { 86 Point from = parsePoint(args.get(0), args.get(1)); 87 Duration duration = parseMillis(args.get(2)); 88 Point to = parsePoint(args.get(3), args.get(4)); 89 device.swipe(from, to, duration); 90 }, 91 String.format("swipe %s (\\d+) %s", POINT, POINT)); 92 93 // keyboard ACTIONS.put(device, args)94 ACTIONS.put( 95 (device, args) -> { 96 List<Integer> keys = 97 Stream.of(args.get(0).split("")) 98 .map(AoaTargetPreparer::parseKeycode) 99 .collect(Collectors.toList()); 100 device.pressKeys(keys); 101 }, 102 "write ([a-zA-Z0-9\\s]+)"); ACTIONS.put(device, args)103 ACTIONS.put( 104 (device, args) -> { 105 List<Integer> keys = new ArrayList<>(); 106 Matcher matcher = KEY.matcher(args.get(0)); 107 while (matcher.find()) { 108 int count = matcher.group(1) == null ? 1 : Integer.decode(matcher.group(1)); 109 Integer keycode = parseKeycode(matcher.group(2)); 110 keys.addAll(Collections.nCopies(count, keycode)); 111 } 112 device.pressKeys(keys); 113 }, 114 "key((?: (?:\\d+\\*)?[a-zA-Z0-9]+)+)"); 115 116 // other 117 ACTIONS.put((device, args) -> device.wakeUp(), "wake"); 118 ACTIONS.put((device, args) -> device.goHome(), "home"); 119 ACTIONS.put((device, args) -> device.goBack(), "back"); ACTIONS.put(device, args)120 ACTIONS.put( 121 (device, args) -> { 122 Duration duration = parseMillis(args.get(0)); 123 device.sleep(duration); 124 }, 125 "sleep (\\d+)"); 126 } 127 128 @Option(name = "device-timeout", description = "Maximum time to wait for device") 129 private Duration mDeviceTimeout = Duration.ofMinutes(1L); 130 131 @Option( 132 name = "wait-for-device-online", 133 description = "Checks whether the device is online after preparation." 134 ) 135 private boolean mWaitForDeviceOnline = true; 136 137 @Option(name = "action", description = "AOAv2 action to perform. Can be repeated.") 138 private List<String> mActions = new ArrayList<>(); 139 140 @Override setUp(TestInformation testInfo)141 public void setUp(TestInformation testInfo) 142 throws TargetSetupError, BuildError, DeviceNotAvailableException { 143 if (mActions.isEmpty()) { 144 return; 145 } 146 ITestDevice device = testInfo.getDevice(); 147 try { 148 configure(device.getSerialNumber()); 149 } catch (RuntimeException e) { 150 throw new TargetSetupError(e.getMessage(), e, device.getDeviceDescriptor()); 151 } 152 153 if (mWaitForDeviceOnline) { 154 // Verify that the device is online after executing AOA actions 155 device.waitForDeviceOnline(); 156 } 157 } 158 159 // Connect to device using its serial number and perform actions configure(String serialNumber)160 private void configure(String serialNumber) throws DeviceNotAvailableException { 161 try (UsbHelper usb = getUsbHelper(); 162 AoaDevice device = usb.getAoaDevice(serialNumber, mDeviceTimeout)) { 163 if (device == null) { 164 throw new DeviceNotAvailableException( 165 "AOAv2-compatible device not found", serialNumber); 166 } 167 CLog.d("Performing %d actions on device %s", mActions.size(), serialNumber); 168 mActions.forEach(action -> execute(device, action)); 169 } 170 } 171 172 @VisibleForTesting getUsbHelper()173 UsbHelper getUsbHelper() { 174 return new UsbHelper(); 175 } 176 177 // Parse and execute an action 178 @VisibleForTesting execute(AoaDevice device, String input)179 void execute(AoaDevice device, String input) { 180 CLog.v("Executing '%s' on %s", input, device.getSerialNumber()); 181 List<List<String>> args = new ArrayList<>(); 182 Action action = ACTIONS.retrieve(args, input); 183 if (action == null) { 184 throw new IllegalArgumentException(String.format("Invalid action %s", input)); 185 } 186 action.accept(device, args.get(0)); 187 } 188 189 // Construct point from string coordinates parsePoint(String x, String y)190 private static Point parsePoint(String x, String y) { 191 return new Point(Integer.decode(x), Integer.decode(y)); 192 } 193 194 // Construct duration from string milliseconds parseMillis(String millis)195 private static Duration parseMillis(String millis) { 196 return Duration.ofMillis(Long.parseLong(millis)); 197 } 198 199 // Convert a string value into a HID keycode parseKeycode(String key)200 private static Integer parseKeycode(String key) { 201 if (key == null || key.isEmpty()) { 202 return null; 203 } 204 if (key.matches("\\s+")) { 205 return 0x2C; // Convert whitespace to the space character 206 } 207 // Lookup keycode or try to parse into an integer 208 Integer keycode = KEYCODES.get(key.toLowerCase()); 209 return keycode != null ? keycode : Integer.decode(key); 210 } 211 212 // Map of characters to HID keycodes 213 private static final Map<String, Integer> KEYCODES = new HashMap<>(); 214 215 static { 216 // Letters 217 KEYCODES.put("a", 0x04); 218 KEYCODES.put("b", 0x05); 219 KEYCODES.put("c", 0x06); 220 KEYCODES.put("d", 0x07); 221 KEYCODES.put("e", 0x08); 222 KEYCODES.put("f", 0x09); 223 KEYCODES.put("g", 0x0A); 224 KEYCODES.put("h", 0x0B); 225 KEYCODES.put("i", 0x0C); 226 KEYCODES.put("j", 0x0D); 227 KEYCODES.put("k", 0x0E); 228 KEYCODES.put("l", 0x0F); 229 KEYCODES.put("m", 0x10); 230 KEYCODES.put("n", 0x11); 231 KEYCODES.put("o", 0x12); 232 KEYCODES.put("p", 0x13); 233 KEYCODES.put("q", 0x14); 234 KEYCODES.put("r", 0x15); 235 KEYCODES.put("s", 0x16); 236 KEYCODES.put("t", 0x17); 237 KEYCODES.put("u", 0x18); 238 KEYCODES.put("v", 0x19); 239 KEYCODES.put("w", 0x1A); 240 KEYCODES.put("x", 0x1B); 241 KEYCODES.put("y", 0x1C); 242 KEYCODES.put("z", 0x1D); 243 // Numbers 244 KEYCODES.put("1", 0x1E); 245 KEYCODES.put("2", 0x1F); 246 KEYCODES.put("3", 0x20); 247 KEYCODES.put("4", 0x21); 248 KEYCODES.put("5", 0x22); 249 KEYCODES.put("6", 0x23); 250 KEYCODES.put("7", 0x24); 251 KEYCODES.put("8", 0x25); 252 KEYCODES.put("9", 0x26); 253 KEYCODES.put("0", 0x27); 254 // Additional keys 255 KEYCODES.put("enter", 0x28); 256 KEYCODES.put("tab", 0x2B); 257 KEYCODES.put("space", 0x2C); 258 KEYCODES.put("right", 0x4F); 259 KEYCODES.put("left", 0x50); 260 KEYCODES.put("down", 0x51); 261 KEYCODES.put("up", 0x52); 262 } 263 } 264