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