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