blob: e45d9bcb44e019cac679730c24a565c730f6a352 [file] [log] [blame]
/*
* 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 static com.android.helper.aoa.AoaDevice.ACCESSORY_REGISTER_HID;
import static com.android.helper.aoa.AoaDevice.ACCESSORY_SEND_HID_EVENT;
import static com.android.helper.aoa.AoaDevice.ACCESSORY_SET_HID_REPORT_DESC;
import static com.android.helper.aoa.AoaDevice.ACCESSORY_START;
import static com.android.helper.aoa.AoaDevice.ACCESSORY_START_MAX_RETRIES;
import static com.android.helper.aoa.AoaDevice.ACCESSORY_UNREGISTER_HID;
import static com.android.helper.aoa.AoaDevice.DEVICE_NOT_FOUND;
import static com.android.helper.aoa.AoaDevice.GOOGLE_VID;
import static com.android.helper.aoa.AoaDevice.LONG_CLICK;
import static com.android.helper.aoa.AoaDevice.SYSTEM_BACK;
import static com.android.helper.aoa.AoaDevice.SYSTEM_HOME;
import static com.android.helper.aoa.AoaDevice.SYSTEM_WAKE;
import static com.android.helper.aoa.AoaDevice.TOUCH_DOWN;
import static com.android.helper.aoa.AoaDevice.TOUCH_UP;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyByte;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.primitives.Shorts;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.verification.VerificationMode;
import java.awt.*;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
/** Unit tests for {@link AoaDevice} */
@RunWith(JUnit4.class)
public class AoaDeviceTest {
private static final int HID_COUNT = AoaHID.values().length;
private static final String SERIAL_NUMBER = "serial-number";
private static final int INVALID_VID = 0x0000;
private static final int ADB_DISABLED_PID = 0x2D00;
private static final int ADB_ENABLED_PID = 0x2D01;
private static final int AUDIO_ENABLED_PID = 0x2D05;
private AoaDevice mDevice;
private UsbHelper mHelper;
private UsbDevice mDelegate;
@Before
public void setUp() {
// valid accessory mode device by default
mDelegate = mock(UsbDevice.class);
when(mDelegate.isValid()).thenReturn(true);
when(mDelegate.getSerialNumber()).thenReturn(SERIAL_NUMBER);
when(mDelegate.getVendorId()).thenReturn(GOOGLE_VID);
when(mDelegate.getProductId()).thenReturn(ADB_DISABLED_PID);
mHelper = mock(UsbHelper.class);
when(mHelper.getDevice(anyString(), any())).thenReturn(mDelegate);
}
// Initialization
@Test
public void testRegistersHIDsAutomatically() {
mDevice = createDevice();
// already in accessory mode
verifyRequest(never(), ACCESSORY_START);
// registers HIDs
verifyRequest(never(), ACCESSORY_UNREGISTER_HID);
verifyRequest(times(HID_COUNT), ACCESSORY_REGISTER_HID);
verifyRequest(times(HID_COUNT), ACCESSORY_SET_HID_REPORT_DESC);
}
@Test
public void testDetectsAccessoryMode() {
// not in accessory mode initially
when(mDelegate.getVendorId())
.thenReturn(INVALID_VID)
.thenReturn(GOOGLE_VID);
mDevice = createDevice();
// restarts in accessory mode
verifyRequest(times(1), ACCESSORY_START);
// registers HIDs afterwards
verifyRequest(never(), ACCESSORY_UNREGISTER_HID);
verifyRequest(times(HID_COUNT), ACCESSORY_REGISTER_HID);
verifyRequest(times(HID_COUNT), ACCESSORY_SET_HID_REPORT_DESC);
}
@Test
public void testRetriesAccessoryMode() {
// never in accessory mode
when(mDelegate.getVendorId()).thenReturn(INVALID_VID);
mDevice = createDevice();
// retried starting accessory mode before giving up
verifyRequest(times(ACCESSORY_START_MAX_RETRIES), ACCESSORY_START);
}
@Test
public void testResetConnection() {
mDevice = createDevice();
clearInvocations(mDelegate);
mDevice.resetConnection();
verify(mHelper, times(1)).getDevice(eq(SERIAL_NUMBER), any());
// re-registers HIDs
verifyRequest(times(HID_COUNT), ACCESSORY_UNREGISTER_HID);
verifyRequest(times(HID_COUNT), ACCESSORY_REGISTER_HID);
verifyRequest(times(HID_COUNT), ACCESSORY_SET_HID_REPORT_DESC);
}
@Test(expected = UsbException.class)
public void testThrowsIfConnectionInvalid() {
when(mDelegate.isValid()).thenReturn(false);
mDevice = createDevice();
}
@Test(expected = UsbException.class)
public void testThrowsIfSerialNumberMissing() {
when(mDelegate.getSerialNumber()).thenReturn(null);
mDevice = createDevice();
}
@Test
public void testGetSerialNumber() {
mDevice = createDevice();
assertEquals(SERIAL_NUMBER, mDevice.getSerialNumber());
}
@Test
public void testIsAdbEnabled() {
mDevice = createDevice();
// invalid VID and valid PID
when(mDelegate.getVendorId()).thenReturn(INVALID_VID);
when(mDelegate.getProductId()).thenReturn(ADB_ENABLED_PID);
assertFalse(mDevice.isAdbEnabled());
// valid VID and invalid PID
when(mDelegate.getVendorId()).thenReturn(GOOGLE_VID);
when(mDelegate.getProductId()).thenReturn(ADB_DISABLED_PID);
assertFalse(mDevice.isAdbEnabled());
// both valid
when(mDelegate.getVendorId()).thenReturn(GOOGLE_VID);
when(mDelegate.getProductId()).thenReturn(ADB_ENABLED_PID);
assertTrue(mDevice.isAdbEnabled());
}
@Test
public void testIsAudioEnabled() {
mDevice = createDevice();
// invalid PID
assertFalse(mDevice.isAudioEnabled());
// both valid
when(mDelegate.getProductId()).thenReturn(AUDIO_ENABLED_PID);
assertTrue(mDevice.isAudioEnabled());
}
@Test
public void testClose() {
mDevice = createDevice();
mDevice.close();
// unregisters descriptors and closes connection
verifyRequest(times(HID_COUNT), ACCESSORY_UNREGISTER_HID);
verify(mDelegate, times(1)).close();
}
// Actions
@Test
public void testClick() {
mDevice = createDevice();
clearInvocations(mDevice);
mDevice.click(new Point(12, 34));
verifyTouches(new Touch(TOUCH_DOWN, 12, 34), new Touch(TOUCH_UP, 12, 34));
}
@Test
public void testLongClick() {
mDevice = createDevice();
clearInvocations(mDevice);
mDevice.longClick(new Point(23, 45));
verifyTouches(new Touch(TOUCH_DOWN, 23, 45), new Touch(TOUCH_UP, 23, 45));
verify(mDevice, atLeastOnce()).sleep(LONG_CLICK);
}
@Test
public void testSwipe() {
mDevice = createDevice();
mDevice.swipe(new Point(20, 0), new Point(70, 100), Duration.ofMillis(50));
List<Touch> events =
List.of(
new Touch(TOUCH_DOWN, 20, 0),
new Touch(TOUCH_DOWN, 30, 20),
new Touch(TOUCH_DOWN, 40, 40),
new Touch(TOUCH_DOWN, 50, 60),
new Touch(TOUCH_DOWN, 60, 80),
new Touch(TOUCH_DOWN, 70, 100),
new Touch(TOUCH_UP, 70, 100));
verifyTouches(events);
}
@Test
public void testPressKeys() {
mDevice = createDevice();
mDevice.pressKeys(new AoaKey(1), null, new AoaKey(2, AoaKey.Modifier.SHIFT));
InOrder order = inOrder(mDelegate);
// press and release 1
verifyHidRequest(order, times(1), AoaHID.KEYBOARD, (byte) 0, (byte) 1);
verifyHidRequest(order, times(1), AoaHID.KEYBOARD, (byte) 0, (byte) 0);
// skip null, and press and release 2
verifyHidRequest(order, times(1), AoaHID.KEYBOARD, (byte) 2, (byte) 2);
verifyHidRequest(order, times(1), AoaHID.KEYBOARD, (byte) 0, (byte) 0);
}
@Test
public void testWakeUp() {
mDevice = createDevice();
mDevice.wakeUp();
verifyHidRequest(times(1), AoaHID.SYSTEM, SYSTEM_WAKE);
}
@Test
public void testGoHome() {
mDevice = createDevice();
mDevice.goHome();
verifyHidRequest(times(1), AoaHID.SYSTEM, SYSTEM_HOME);
}
@Test
public void testGoBack() {
mDevice = createDevice();
mDevice.goBack();
verifyHidRequest(times(1), AoaHID.SYSTEM, SYSTEM_BACK);
}
@Test
public void testRetryAction() {
mDevice = createDevice();
// first attempt will fail to find device
when(mDelegate.controlTransfer(anyByte(), anyByte(), anyInt(), anyInt(), any()))
.thenReturn(DEVICE_NOT_FOUND)
.thenReturn(0);
mDevice.click(new Point(34, 56));
// reset the connection
verify(mHelper, times(1)).getDevice(eq(SERIAL_NUMBER), any());
verifyTouches(
new Touch(TOUCH_DOWN, 34, 56), // failed
new Touch(TOUCH_DOWN, 34, 56), // retry after resetting connection
new Touch(TOUCH_UP, 34, 56));
}
// Helpers
/** Creates a mock device with predictable timestamps. */
private AoaDevice createDevice() {
AoaDevice device =
new AoaDevice(mHelper, mDelegate) {
private Instant mInstant;
@Override
Instant now() {
if (mInstant == null) {
mInstant = Instant.MIN;
}
return mInstant;
}
@Override
public void sleep(@Nonnull Duration duration) {
mInstant = now().plus(duration);
}
};
return spy(device);
}
private void verifyRequest(VerificationMode mode, byte request) {
verify(mDelegate, mode)
.controlTransfer(anyByte(), eq(request), anyInt(), anyInt(), any());
}
private void verifyHidRequest(VerificationMode mode, AoaHID hid, byte... data) {
verifyHidRequest(null, mode, hid, data);
}
private void verifyHidRequest(InOrder order, VerificationMode mode, AoaHID hid, byte... data) {
UsbDevice verifier =
order == null ? verify(mDelegate, mode) : order.verify(mDelegate, mode);
verifier.controlTransfer(
anyByte(), eq(ACCESSORY_SEND_HID_EVENT), eq(hid.getId()), anyInt(), eq(data));
}
private void verifyTouches(Touch... expected) {
verifyTouches(Arrays.asList(expected));
}
private void verifyTouches(List<Touch> expected) {
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(mDelegate, times(expected.size()))
.controlTransfer(
anyByte(),
eq(ACCESSORY_SEND_HID_EVENT),
eq(AoaHID.TOUCH_SCREEN.getId()),
anyInt(),
captor.capture());
List<Touch> events =
captor.getAllValues().stream().map(Touch::new).collect(Collectors.toList());
assertEquals(expected, events);
}
/** Touch HID event. */
private static class Touch {
private final byte mType;
private final int mX;
private final int mY;
private Touch(byte type, int x, int y) {
mType = type;
mX = x;
mY = y;
}
private Touch(byte[] data) {
mType = data[0];
mX = Shorts.fromBytes(data[2], data[1]);
mY = Shorts.fromBytes(data[4], data[3]);
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof Touch)) {
return false;
}
Touch event = (Touch) object;
return mType == event.mType && mX == event.mX && mY == event.mY;
}
@Override
public int hashCode() {
return Objects.hash(mType, mX, mY);
}
@Override
public String toString() {
return String.format("Touch{%d, %d, %d}", mType, mX, mY);
}
}
}