• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.helper.aoa;
17 
18 import com.google.common.annotations.VisibleForTesting;
19 import com.google.common.collect.ImmutableList;
20 import com.google.common.collect.ImmutableSet;
21 import com.google.common.collect.Range;
22 import com.google.common.primitives.Bytes;
23 import com.google.common.util.concurrent.Uninterruptibles;
24 
25 import java.awt.*;
26 import java.time.Duration;
27 import java.util.Arrays;
28 import java.util.Iterator;
29 import java.util.Objects;
30 import java.util.concurrent.TimeUnit;
31 
32 import javax.annotation.Nonnull;
33 
34 /**
35  * USB connected AOAv2-compatible Android device.
36  *
37  * <p>This host-side utility can be used to send commands (e.g. clicks, swipes, keystrokes, and
38  * more) to a connected device without the need for ADB.
39  *
40  * @see <a href="https://source.android.com/devices/accessories/aoa2">Android Open Accessory
41  *     Protocol 2.0</a>
42  */
43 public class AoaDevice implements AutoCloseable {
44 
45     // USB error code
46     static final int DEVICE_NOT_FOUND = -4;
47 
48     // USB request types (direction and vendor type)
49     static final byte INPUT = (byte) (0x80 | (0x02 << 5));
50     static final byte OUTPUT = (byte) (0x00 | (0x02 << 5));
51 
52     // AOA VID and PID
53     static final int GOOGLE_VID = 0x18D1;
54     private static final Range<Integer> AOA_PID = Range.closed(0x2D00, 0x2D05);
55     private static final ImmutableSet<Integer> ADB_PID = ImmutableSet.of(0x2D01, 0x2D03, 0x2D05);
56 
57     // AOA requests
58     static final byte ACCESSORY_GET_PROTOCOL = 51;
59     static final byte ACCESSORY_START = 53;
60     static final byte ACCESSORY_REGISTER_HID = 54;
61     static final byte ACCESSORY_UNREGISTER_HID = 55;
62     static final byte ACCESSORY_SET_HID_REPORT_DESC = 56;
63     static final byte ACCESSORY_SEND_HID_EVENT = 57;
64 
65     // Touch types
66     static final byte TOUCH_UP = 0b00;
67     static final byte TOUCH_DOWN = 0b11;
68 
69     // System buttons
70     static final byte SYSTEM_WAKE = 0b001;
71     static final byte SYSTEM_HOME = 0b010;
72     static final byte SYSTEM_BACK = 0b100;
73 
74     // Durations and steps
75     private static final Duration CONNECTION_TIMEOUT = Duration.ofSeconds(10L);
76     private static final Duration CONFIGURE_DELAY = Duration.ofSeconds(1L);
77     private static final Duration ACTION_DELAY = Duration.ofSeconds(3L);
78     private static final Duration STEP_DELAY = Duration.ofMillis(10L);
79     static final Duration LONG_CLICK = Duration.ofSeconds(1L);
80     static final int SCROLL_STEPS = 40;
81     static final int FLING_STEPS = 10;
82 
83     private final UsbHelper mHelper;
84     private UsbDevice mDelegate;
85     private String mSerialNumber;
86 
AoaDevice(@onnull UsbHelper helper, @Nonnull UsbDevice delegate)87     AoaDevice(@Nonnull UsbHelper helper, @Nonnull UsbDevice delegate) {
88         mHelper = helper;
89         mDelegate = delegate;
90         initialize();
91     }
92 
93     // Configure the device, switching to accessory mode if necessary and registering the HIDs
initialize()94     private void initialize() {
95         if (!isValid()) {
96             throw new UsbException("Invalid device connection");
97         }
98 
99         mSerialNumber = mDelegate.getSerialNumber();
100         if (mSerialNumber == null) {
101             throw new UsbException("Missing serial number");
102         }
103 
104         if (isAccessoryMode()) {
105             registerHIDs();
106         } else {
107             // restart in accessory mode
108             mHelper.checkResult(
109                     mDelegate.controlTransfer(OUTPUT, ACCESSORY_START, 0, 0, new byte[0]));
110             sleep(CONFIGURE_DELAY);
111             resetConnection();
112         }
113     }
114 
115     // Register HIDs
registerHIDs()116     private void registerHIDs() {
117         for (HID hid : HID.values()) {
118             // register HID identifier
119             mHelper.checkResult(
120                     mDelegate.controlTransfer(
121                             OUTPUT,
122                             ACCESSORY_REGISTER_HID,
123                             hid.getId(),
124                             hid.getDescriptor().length,
125                             new byte[0]));
126             // register HID descriptor
127             mHelper.checkResult(
128                     mDelegate.controlTransfer(
129                             OUTPUT,
130                             ACCESSORY_SET_HID_REPORT_DESC,
131                             hid.getId(),
132                             0,
133                             hid.getDescriptor()));
134         }
135         sleep(CONFIGURE_DELAY);
136     }
137 
138     // Unregister HIDs
unregisterHIDs()139     private void unregisterHIDs() {
140         for (HID hid : HID.values()) {
141             mDelegate.controlTransfer(
142                     OUTPUT, ACCESSORY_UNREGISTER_HID, hid.getId(), 0, new byte[0]);
143         }
144     }
145 
146     /**
147      * Close and re-fetch the connection. This is necessary after the USB connection has been reset,
148      * e.g. when toggling accessory mode or USB debugging.
149      */
resetConnection()150     public void resetConnection() {
151         close();
152         mDelegate = mHelper.getDevice(mSerialNumber, CONNECTION_TIMEOUT);
153         initialize();
154     }
155 
156     /** @return true if connection is non-null, but does not check if resetting is necessary */
isValid()157     public boolean isValid() {
158         return mDelegate != null && mDelegate.isValid();
159     }
160 
161     /** @return device's serial number */
162     @Nonnull
getSerialNumber()163     public String getSerialNumber() {
164         return mSerialNumber;
165     }
166 
167     // Checks whether the device is in accessory mode
isAccessoryMode()168     private boolean isAccessoryMode() {
169         return GOOGLE_VID == mDelegate.getVendorId()
170                 && AOA_PID.contains(mDelegate.getProductId());
171     }
172 
173     /** @return true if device has USB debugging enabled */
isAdbEnabled()174     public boolean isAdbEnabled() {
175         return GOOGLE_VID == mDelegate.getVendorId()
176                 && ADB_PID.contains(mDelegate.getProductId());
177     }
178 
179     /** Wait for a specified duration. */
sleep(@onnull Duration duration)180     public void sleep(@Nonnull Duration duration) {
181         Uninterruptibles.sleepUninterruptibly(duration.toNanos(), TimeUnit.NANOSECONDS);
182     }
183 
184     /** Perform a click. */
click(@onnull Point point)185     public void click(@Nonnull Point point) {
186         click(point, Duration.ZERO);
187     }
188 
189     /** Perform a long click. */
longClick(@onnull Point point)190     public void longClick(@Nonnull Point point) {
191         click(point, LONG_CLICK);
192     }
193 
194     // Click and wait at a location.
click(Point point, Duration duration)195     private void click(Point point, Duration duration) {
196         touch(TOUCH_DOWN, point, duration);
197         touch(TOUCH_UP, point, ACTION_DELAY);
198     }
199 
200     /** Scroll from one location to another. */
scroll(@onnull Point from, @Nonnull Point to)201     public void scroll(@Nonnull Point from, @Nonnull Point to) {
202         swipe(from, to, SCROLL_STEPS);
203     }
204 
205     /** Fling from one location to another. */
fling(@onnull Point from, @Nonnull Point to)206     public void fling(@Nonnull Point from, @Nonnull Point to) {
207         swipe(from, to, FLING_STEPS);
208     }
209 
210     /** Drag from one location to another. */
drag(@onnull Point from, @Nonnull Point to)211     public void drag(@Nonnull Point from, @Nonnull Point to) {
212         touch(TOUCH_DOWN, from, LONG_CLICK);
213         scroll(from, to);
214     }
215 
216     // Move from one location to another using discrete steps
swipe(Point from, Point to, int steps)217     private void swipe(Point from, Point to, int steps) {
218         steps = Math.max(steps, 1);
219         float xStep = ((float) (to.x - from.x)) / steps;
220         float yStep = ((float) (to.y - from.y)) / steps;
221 
222         for (int i = 0; i <= steps; i++) {
223             Point point = new Point((int) (from.x + xStep * i), (int) (from.y + yStep * i));
224             touch(TOUCH_DOWN, point, STEP_DELAY);
225         }
226         touch(TOUCH_UP, to, ACTION_DELAY);
227     }
228 
229     // Send a touch event to the device
touch(byte type, Point point, Duration pause)230     private void touch(byte type, Point point, Duration pause) {
231         int x = Math.min(Math.max(point.x, 0), 360);
232         int y = Math.min(Math.max(point.y, 0), 640);
233         byte[] data = new byte[] {type, (byte) x, (byte) (x >> 8), (byte) y, (byte) (y >> 8)};
234         send(HID.TOUCH_SCREEN, data, pause);
235     }
236 
237     /**
238      * Write a string by pressing keys. Only alphanumeric characters and whitespace is supported.
239      *
240      * @param value string to write
241      */
write(@onnull String value)242     public void write(@Nonnull String value) {
243         // map characters to HID usages
244         Integer[] keyCodes =
245                 value.codePoints()
246                         .mapToObj(
247                                 c -> {
248                                     if (Character.isSpaceChar(c)) {
249                                         return 0x2C;
250                                     } else if (Character.isAlphabetic(c)) {
251                                         return Character.toLowerCase(c) - 'a' + 0x04;
252                                     } else if (Character.isDigit(c)) {
253                                         return c == '0' ? 0x27 : c - '1' + 0x1E;
254                                     }
255                                     return null;
256                                 })
257                         .toArray(Integer[]::new);
258         // press the keys
259         key(keyCodes);
260     }
261 
262     /**
263      * Press a key.
264      *
265      * @param keyCodes key HID usages, see <a
266      *     https://source.android.com/devices/input/keyboard-devices">Keyboard devices</a>
267      */
key(Integer... keyCodes)268     public void key(Integer... keyCodes) {
269         Iterator<Integer> it = Arrays.stream(keyCodes).filter(Objects::nonNull).iterator();
270         while (it.hasNext()) {
271             Integer keyCode = it.next();
272             send(HID.KEYBOARD, new byte[] {keyCode.byteValue()}, STEP_DELAY);
273             send(HID.KEYBOARD, new byte[] {(byte) 0}, it.hasNext() ? STEP_DELAY : ACTION_DELAY);
274         }
275     }
276 
277     /** Wake up the device if it is sleeping. */
wakeUp()278     public void wakeUp() {
279         send(AoaDevice.HID.SYSTEM, new byte[] {SYSTEM_WAKE}, ACTION_DELAY);
280     }
281 
282     /** Press the device's home button. */
goHome()283     public void goHome() {
284         send(AoaDevice.HID.SYSTEM, new byte[] {SYSTEM_HOME}, ACTION_DELAY);
285     }
286 
287     /** Press the device's back button. */
goBack()288     public void goBack() {
289         send(AoaDevice.HID.SYSTEM, new byte[] {SYSTEM_BACK}, ACTION_DELAY);
290     }
291 
292     // Send a HID event to the device
send(HID hid, byte[] data, Duration pause)293     private void send(HID hid, byte[] data, Duration pause) {
294         int result =
295                 mDelegate.controlTransfer(OUTPUT, ACCESSORY_SEND_HID_EVENT, hid.getId(), 0, data);
296         if (result == DEVICE_NOT_FOUND) {
297             // device not found, reset the connection and retry
298             resetConnection();
299             result =
300                     mDelegate.controlTransfer(
301                             OUTPUT, ACCESSORY_SEND_HID_EVENT, hid.getId(), 0, data);
302         }
303         mHelper.checkResult(result);
304         sleep(pause);
305     }
306 
307     /** Close the device connection. */
308     @Override
close()309     public void close() {
310         if (isValid()) {
311             if (isAccessoryMode()) {
312                 unregisterHIDs();
313             }
314             mDelegate.close();
315             mDelegate = null;
316         }
317     }
318 
319     /**
320      * Human interface device descriptors.
321      *
322      * @see <a href="https://www.usb.org/hid">USB HID information</a>
323      */
324     @VisibleForTesting
325     enum HID {
326         /** 360 x 640 touch screen: 6-bit padding, 2-bit type, 16-bit X coord., 16-bit Y coord. */
327         TOUCH_SCREEN(
328                 new Integer[] {
329                     0x05, 0x0D, //      Usage Page (Digitizer)
330                     0x09, 0x04, //      Usage (Touch Screen)
331                     0xA1, 0x01, //      Collection (Application)
332                     0x09, 0x32, //          Usage (In Range) - proximity to screen
333                     0x09, 0x33, //          Usage (Touch) - contact with screen
334                     0x15, 0x00, //          Logical Minimum (0)
335                     0x25, 0x01, //          Logical Maximum (1)
336                     0x75, 0x01, //          Report Size (1)
337                     0x95, 0x02, //          Report Count (2)
338                     0x81, 0x02, //          Input (Data, Variable, Absolute)
339                     0x75, 0x01, //          Report Size (1)
340                     0x95, 0x06, //          Report Count (6) - padding
341                     0x81, 0x01, //          Input (Constant)
342                     0x05, 0x01, //          Usage Page (Generic)
343                     0x09, 0x30, //          Usage (X)
344                     0x15, 0x00, //          Logical Minimum (0)
345                     0x26, 0x68, 0x01, //    Logical Maximum (360)
346                     0x75, 0x10, //          Report Size (16)
347                     0x95, 0x01, //          Report Count (1)
348                     0x81, 0x02, //          Input (Data, Variable, Absolute)
349                     0x09, 0x31, //          Usage (Y)
350                     0x15, 0x00, //          Logical Minimum (0)
351                     0x26, 0x80, 0x02, //    Logical Maximum (640)
352                     0x75, 0x10, //          Report Size (16)
353                     0x95, 0x01, //          Report Count (1)
354                     0x81, 0x02, //          Input (Data, Variable, Absolute)
355                     0xC0, //            End Collection
356                 }),
357 
358         /** 101-key keyboard: 8-bit keycode. */
359         KEYBOARD(
360                 new Integer[] {
361                     0x05, 0x01, //      Usage Page (Generic)
362                     0x09, 0x06, //      Usage (Keyboard)
363                     0xA1, 0x01, //      Collection (Application)
364                     0x05, 0x07, //          Usage Page (Key Codes)
365                     0x19, 0x00, //          Usage Minimum (0)
366                     0x29, 0x65, //          Usage Maximum (101)
367                     0x15, 0x00, //          Logical Minimum (0)
368                     0x25, 0x65, //          Logical Maximum (101)
369                     0x75, 0x08, //          Report Size (8)
370                     0x95, 0x01, //          Report Count (1)
371                     0x81, 0x00, //          Input (Data, Array, Absolute)
372                     0xC0, //            End Collection
373                 }),
374 
375         /** System buttons: 5-bit padding, 3-bit flags (wake, home, back). */
376         SYSTEM(
377                 new Integer[] {
378                     0x05, 0x01, //      Usage Page (Generic)
379                     0x09, 0x80, //      Usage (System Control)
380                     0xA1, 0x01, //      Collection (Application)
381                     0x15, 0x00, //          Logical Minimum (0)
382                     0x25, 0x01, //          Logical Maximum (1)
383                     0x75, 0x01, //          Report Size (1)
384                     0x95, 0x01, //          Report Count (1)
385                     0x09, 0x83, //          Usage (Wake)
386                     0x81, 0x06, //          Input (Data, Variable, Relative)
387                     0xC0, //            End Collection
388                     0x05, 0x0C, //      Usage Page (Consumer)
389                     0x09, 0x01, //      Usage (Consumer Control)
390                     0xA1, 0x01, //      Collection (Application)
391                     0x15, 0x00, //          Logical Minimum (0)
392                     0x25, 0x01, //          Logical Maximum (1)
393                     0x75, 0x01, //          Report Size (1)
394                     0x95, 0x01, //          Report Count (1)
395                     0x0A, 0x23, 0x02, //    Usage (Home)
396                     0x81, 0x06, //          Input (Data, Variable, Relative)
397                     0x0A, 0x24, 0x02, //    Usage (Back)
398                     0x81, 0x06, //          Input (Data, Variable, Relative)
399                     0x75, 0x01, //          Report Size (1)
400                     0x95, 0x05, //          Report Count (5) - padding
401                     0x81, 0x01, //          Input (Constant)
402                     0xC0, //            End Collection
403                 });
404 
405         private final ImmutableList<Integer> mDescriptor;
406 
HID(Integer[] descriptor)407         HID(Integer[] descriptor) {
408             mDescriptor = ImmutableList.copyOf(descriptor);
409         }
410 
getId()411         int getId() {
412             return ordinal();
413         }
414 
getDescriptor()415         byte[] getDescriptor() {
416             return Bytes.toArray(mDescriptor);
417         }
418     }
419 }
420