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