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