/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.helper.aoa; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import com.google.common.primitives.Bytes; import com.google.common.util.concurrent.Uninterruptibles; import java.awt.*; import java.time.Duration; import java.util.Arrays; import java.util.Iterator; import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; /** * USB connected AOAv2-compatible Android device. * *

This host-side utility can be used to send commands (e.g. clicks, swipes, keystrokes, and * more) to a connected device without the need for ADB. * * @see Android Open Accessory * Protocol 2.0 */ public class AoaDevice implements AutoCloseable { // USB error code static final int DEVICE_NOT_FOUND = -4; // USB request types (direction and vendor type) static final byte INPUT = (byte) (0x80 | (0x02 << 5)); static final byte OUTPUT = (byte) (0x00 | (0x02 << 5)); // AOA VID and PID static final int GOOGLE_VID = 0x18D1; private static final Range AOA_PID = Range.closed(0x2D00, 0x2D05); private static final ImmutableSet ADB_PID = ImmutableSet.of(0x2D01, 0x2D03, 0x2D05); // AOA requests static final byte ACCESSORY_GET_PROTOCOL = 51; static final byte ACCESSORY_START = 53; static final byte ACCESSORY_REGISTER_HID = 54; static final byte ACCESSORY_UNREGISTER_HID = 55; static final byte ACCESSORY_SET_HID_REPORT_DESC = 56; static final byte ACCESSORY_SEND_HID_EVENT = 57; // Touch types static final byte TOUCH_UP = 0b00; static final byte TOUCH_DOWN = 0b11; // System buttons static final byte SYSTEM_WAKE = 0b001; static final byte SYSTEM_HOME = 0b010; static final byte SYSTEM_BACK = 0b100; // Durations and steps private static final Duration CONNECTION_TIMEOUT = Duration.ofSeconds(10L); private static final Duration CONFIGURE_DELAY = Duration.ofSeconds(1L); private static final Duration ACTION_DELAY = Duration.ofSeconds(3L); private static final Duration STEP_DELAY = Duration.ofMillis(10L); static final Duration LONG_CLICK = Duration.ofSeconds(1L); static final int SCROLL_STEPS = 40; static final int FLING_STEPS = 10; private final UsbHelper mHelper; private UsbDevice mDelegate; private String mSerialNumber; AoaDevice(@Nonnull UsbHelper helper, @Nonnull UsbDevice delegate) { mHelper = helper; mDelegate = delegate; initialize(); } // Configure the device, switching to accessory mode if necessary and registering the HIDs private void initialize() { if (!isValid()) { throw new UsbException("Invalid device connection"); } mSerialNumber = mDelegate.getSerialNumber(); if (mSerialNumber == null) { throw new UsbException("Missing serial number"); } if (isAccessoryMode()) { registerHIDs(); } else { // restart in accessory mode mHelper.checkResult( mDelegate.controlTransfer(OUTPUT, ACCESSORY_START, 0, 0, new byte[0])); sleep(CONFIGURE_DELAY); resetConnection(); } } // Register HIDs private void registerHIDs() { for (HID hid : HID.values()) { // register HID identifier mHelper.checkResult( mDelegate.controlTransfer( OUTPUT, ACCESSORY_REGISTER_HID, hid.getId(), hid.getDescriptor().length, new byte[0])); // register HID descriptor mHelper.checkResult( mDelegate.controlTransfer( OUTPUT, ACCESSORY_SET_HID_REPORT_DESC, hid.getId(), 0, hid.getDescriptor())); } sleep(CONFIGURE_DELAY); } // Unregister HIDs private void unregisterHIDs() { for (HID hid : HID.values()) { mDelegate.controlTransfer( OUTPUT, ACCESSORY_UNREGISTER_HID, hid.getId(), 0, new byte[0]); } } /** * Close and re-fetch the connection. This is necessary after the USB connection has been reset, * e.g. when toggling accessory mode or USB debugging. */ public void resetConnection() { close(); mDelegate = mHelper.getDevice(mSerialNumber, CONNECTION_TIMEOUT); initialize(); } /** @return true if connection is non-null, but does not check if resetting is necessary */ public boolean isValid() { return mDelegate != null && mDelegate.isValid(); } /** @return device's serial number */ @Nonnull public String getSerialNumber() { return mSerialNumber; } // Checks whether the device is in accessory mode private boolean isAccessoryMode() { return GOOGLE_VID == mDelegate.getVendorId() && AOA_PID.contains(mDelegate.getProductId()); } /** @return true if device has USB debugging enabled */ public boolean isAdbEnabled() { return GOOGLE_VID == mDelegate.getVendorId() && ADB_PID.contains(mDelegate.getProductId()); } /** Wait for a specified duration. */ public void sleep(@Nonnull Duration duration) { Uninterruptibles.sleepUninterruptibly(duration.toNanos(), TimeUnit.NANOSECONDS); } /** Perform a click. */ public void click(@Nonnull Point point) { click(point, Duration.ZERO); } /** Perform a long click. */ public void longClick(@Nonnull Point point) { click(point, LONG_CLICK); } // Click and wait at a location. private void click(Point point, Duration duration) { touch(TOUCH_DOWN, point, duration); touch(TOUCH_UP, point, ACTION_DELAY); } /** Scroll from one location to another. */ public void scroll(@Nonnull Point from, @Nonnull Point to) { swipe(from, to, SCROLL_STEPS); } /** Fling from one location to another. */ public void fling(@Nonnull Point from, @Nonnull Point to) { swipe(from, to, FLING_STEPS); } /** Drag from one location to another. */ public void drag(@Nonnull Point from, @Nonnull Point to) { touch(TOUCH_DOWN, from, LONG_CLICK); scroll(from, to); } // Move from one location to another using discrete steps private void swipe(Point from, Point to, int steps) { steps = Math.max(steps, 1); float xStep = ((float) (to.x - from.x)) / steps; float yStep = ((float) (to.y - from.y)) / steps; for (int i = 0; i <= steps; i++) { Point point = new Point((int) (from.x + xStep * i), (int) (from.y + yStep * i)); touch(TOUCH_DOWN, point, STEP_DELAY); } touch(TOUCH_UP, to, ACTION_DELAY); } // Send a touch event to the device private void touch(byte type, Point point, Duration pause) { int x = Math.min(Math.max(point.x, 0), 360); int y = Math.min(Math.max(point.y, 0), 640); byte[] data = new byte[] {type, (byte) x, (byte) (x >> 8), (byte) y, (byte) (y >> 8)}; send(HID.TOUCH_SCREEN, data, pause); } /** * Write a string by pressing keys. Only alphanumeric characters and whitespace is supported. * * @param value string to write */ public void write(@Nonnull String value) { // map characters to HID usages Integer[] keyCodes = value.codePoints() .mapToObj( c -> { if (Character.isSpaceChar(c)) { return 0x2C; } else if (Character.isAlphabetic(c)) { return Character.toLowerCase(c) - 'a' + 0x04; } else if (Character.isDigit(c)) { return c == '0' ? 0x27 : c - '1' + 0x1E; } return null; }) .toArray(Integer[]::new); // press the keys key(keyCodes); } /** * Press a key. * * @param keyCodes key HID usages, see Keyboard devices */ public void key(Integer... keyCodes) { Iterator it = Arrays.stream(keyCodes).filter(Objects::nonNull).iterator(); while (it.hasNext()) { Integer keyCode = it.next(); send(HID.KEYBOARD, new byte[] {keyCode.byteValue()}, STEP_DELAY); send(HID.KEYBOARD, new byte[] {(byte) 0}, it.hasNext() ? STEP_DELAY : ACTION_DELAY); } } /** Wake up the device if it is sleeping. */ public void wakeUp() { send(AoaDevice.HID.SYSTEM, new byte[] {SYSTEM_WAKE}, ACTION_DELAY); } /** Press the device's home button. */ public void goHome() { send(AoaDevice.HID.SYSTEM, new byte[] {SYSTEM_HOME}, ACTION_DELAY); } /** Press the device's back button. */ public void goBack() { send(AoaDevice.HID.SYSTEM, new byte[] {SYSTEM_BACK}, ACTION_DELAY); } // Send a HID event to the device private void send(HID hid, byte[] data, Duration pause) { int result = mDelegate.controlTransfer(OUTPUT, ACCESSORY_SEND_HID_EVENT, hid.getId(), 0, data); if (result == DEVICE_NOT_FOUND) { // device not found, reset the connection and retry resetConnection(); result = mDelegate.controlTransfer( OUTPUT, ACCESSORY_SEND_HID_EVENT, hid.getId(), 0, data); } mHelper.checkResult(result); sleep(pause); } /** Close the device connection. */ @Override public void close() { if (isValid()) { if (isAccessoryMode()) { unregisterHIDs(); } mDelegate.close(); mDelegate = null; } } /** * Human interface device descriptors. * * @see USB HID information */ @VisibleForTesting enum HID { /** 360 x 640 touch screen: 6-bit padding, 2-bit type, 16-bit X coord., 16-bit Y coord. */ TOUCH_SCREEN( new Integer[] { 0x05, 0x0D, // Usage Page (Digitizer) 0x09, 0x04, // Usage (Touch Screen) 0xA1, 0x01, // Collection (Application) 0x09, 0x32, // Usage (In Range) - proximity to screen 0x09, 0x33, // Usage (Touch) - contact with screen 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x02, // Report Count (2) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x75, 0x01, // Report Size (1) 0x95, 0x06, // Report Count (6) - padding 0x81, 0x01, // Input (Constant) 0x05, 0x01, // Usage Page (Generic) 0x09, 0x30, // Usage (X) 0x15, 0x00, // Logical Minimum (0) 0x26, 0x68, 0x01, // Logical Maximum (360) 0x75, 0x10, // Report Size (16) 0x95, 0x01, // Report Count (1) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x09, 0x31, // Usage (Y) 0x15, 0x00, // Logical Minimum (0) 0x26, 0x80, 0x02, // Logical Maximum (640) 0x75, 0x10, // Report Size (16) 0x95, 0x01, // Report Count (1) 0x81, 0x02, // Input (Data, Variable, Absolute) 0xC0, // End Collection }), /** 101-key keyboard: 8-bit keycode. */ KEYBOARD( new Integer[] { 0x05, 0x01, // Usage Page (Generic) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0x00, // Usage Minimum (0) 0x29, 0x65, // Usage Maximum (101) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x00, // Input (Data, Array, Absolute) 0xC0, // End Collection }), /** System buttons: 5-bit padding, 3-bit flags (wake, home, back). */ SYSTEM( new Integer[] { 0x05, 0x01, // Usage Page (Generic) 0x09, 0x80, // Usage (System Control) 0xA1, 0x01, // Collection (Application) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x01, // Report Count (1) 0x09, 0x83, // Usage (Wake) 0x81, 0x06, // Input (Data, Variable, Relative) 0xC0, // End Collection 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x01, // Report Count (1) 0x0A, 0x23, 0x02, // Usage (Home) 0x81, 0x06, // Input (Data, Variable, Relative) 0x0A, 0x24, 0x02, // Usage (Back) 0x81, 0x06, // Input (Data, Variable, Relative) 0x75, 0x01, // Report Size (1) 0x95, 0x05, // Report Count (5) - padding 0x81, 0x01, // Input (Constant) 0xC0, // End Collection }); private final ImmutableList mDescriptor; HID(Integer[] descriptor) { mDescriptor = ImmutableList.copyOf(descriptor); } int getId() { return ordinal(); } byte[] getDescriptor() { return Bytes.toArray(mDescriptor); } } }