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 static com.android.helper.aoa.AoaDevice.ACCESSORY_REGISTER_HID; 19 import static com.android.helper.aoa.AoaDevice.ACCESSORY_SEND_HID_EVENT; 20 import static com.android.helper.aoa.AoaDevice.ACCESSORY_SET_HID_REPORT_DESC; 21 import static com.android.helper.aoa.AoaDevice.ACCESSORY_START; 22 import static com.android.helper.aoa.AoaDevice.ACCESSORY_UNREGISTER_HID; 23 import static com.android.helper.aoa.AoaDevice.DEVICE_NOT_FOUND; 24 import static com.android.helper.aoa.AoaDevice.FLING_STEPS; 25 import static com.android.helper.aoa.AoaDevice.GOOGLE_VID; 26 import static com.android.helper.aoa.AoaDevice.LONG_CLICK; 27 import static com.android.helper.aoa.AoaDevice.SCROLL_STEPS; 28 import static com.android.helper.aoa.AoaDevice.SYSTEM_BACK; 29 import static com.android.helper.aoa.AoaDevice.SYSTEM_HOME; 30 import static com.android.helper.aoa.AoaDevice.SYSTEM_WAKE; 31 import static com.android.helper.aoa.AoaDevice.TOUCH_DOWN; 32 import static com.android.helper.aoa.AoaDevice.TOUCH_UP; 33 34 import static org.junit.Assert.assertEquals; 35 import static org.junit.Assert.assertFalse; 36 import static org.junit.Assert.assertTrue; 37 import static org.mockito.ArgumentMatchers.any; 38 import static org.mockito.ArgumentMatchers.anyByte; 39 import static org.mockito.ArgumentMatchers.anyInt; 40 import static org.mockito.ArgumentMatchers.anyString; 41 import static org.mockito.ArgumentMatchers.eq; 42 import static org.mockito.Mockito.atLeastOnce; 43 import static org.mockito.Mockito.clearInvocations; 44 import static org.mockito.Mockito.inOrder; 45 import static org.mockito.Mockito.mock; 46 import static org.mockito.Mockito.never; 47 import static org.mockito.Mockito.spy; 48 import static org.mockito.Mockito.times; 49 import static org.mockito.Mockito.verify; 50 import static org.mockito.Mockito.when; 51 52 import com.google.common.primitives.Shorts; 53 54 import org.junit.Before; 55 import org.junit.Test; 56 import org.junit.runner.RunWith; 57 import org.junit.runners.JUnit4; 58 import org.mockito.ArgumentCaptor; 59 import org.mockito.InOrder; 60 import org.mockito.verification.VerificationMode; 61 62 import java.awt.*; 63 import java.time.Duration; 64 import java.util.Arrays; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.stream.Collectors; 68 import java.util.stream.Stream; 69 70 import javax.annotation.Nonnull; 71 72 /** Unit tests for {@link AoaDevice} */ 73 @RunWith(JUnit4.class) 74 public class AoaDeviceTest { 75 76 private static final int HID_COUNT = AoaDevice.HID.values().length; 77 private static final String SERIAL_NUMBER = "serial-number"; 78 private static final int INVALID_VID = 0x0000; 79 private static final int ADB_DISABLED_PID = 0x2D00; 80 private static final int ADB_ENABLED_PID = 0x2D01; 81 82 private AoaDevice mDevice; 83 84 private UsbHelper mHelper; 85 private UsbDevice mDelegate; 86 87 @Before setUp()88 public void setUp() { 89 // valid accessory mode device by default 90 mDelegate = mock(UsbDevice.class); 91 when(mDelegate.isValid()).thenReturn(true); 92 when(mDelegate.getSerialNumber()).thenReturn(SERIAL_NUMBER); 93 when(mDelegate.getVendorId()).thenReturn(GOOGLE_VID); 94 when(mDelegate.getProductId()).thenReturn(ADB_DISABLED_PID); 95 96 mHelper = mock(UsbHelper.class); 97 when(mHelper.getDevice(anyString(), any())).thenReturn(mDelegate); 98 } 99 100 // Initialization 101 102 @Test testRegistersHIDsAutomatically()103 public void testRegistersHIDsAutomatically() { 104 mDevice = createDevice(); 105 106 // already in accessory mode 107 verifyRequest(never(), ACCESSORY_START); 108 109 // registers HIDs 110 verifyRequest(never(), ACCESSORY_UNREGISTER_HID); 111 verifyRequest(times(HID_COUNT), ACCESSORY_REGISTER_HID); 112 verifyRequest(times(HID_COUNT), ACCESSORY_SET_HID_REPORT_DESC); 113 } 114 115 @Test testDetectsAccessoryMode()116 public void testDetectsAccessoryMode() { 117 // not in accessory mode initially 118 when(mDelegate.getVendorId()) 119 .thenReturn(INVALID_VID) 120 .thenReturn(INVALID_VID) 121 .thenReturn(GOOGLE_VID); 122 123 mDevice = createDevice(); 124 125 // restarts in accessory mode 126 verifyRequest(times(1), ACCESSORY_START); 127 128 // registers HIDs afterwards 129 verifyRequest(never(), ACCESSORY_UNREGISTER_HID); 130 verifyRequest(times(HID_COUNT), ACCESSORY_REGISTER_HID); 131 verifyRequest(times(HID_COUNT), ACCESSORY_SET_HID_REPORT_DESC); 132 } 133 134 @Test testResetConnection()135 public void testResetConnection() { 136 mDevice = createDevice(); 137 clearInvocations(mDelegate); 138 mDevice.resetConnection(); 139 140 verify(mHelper, times(1)).getDevice(eq(SERIAL_NUMBER), any()); 141 142 // re-registers HIDs 143 verifyRequest(times(HID_COUNT), ACCESSORY_UNREGISTER_HID); 144 verifyRequest(times(HID_COUNT), ACCESSORY_REGISTER_HID); 145 verifyRequest(times(HID_COUNT), ACCESSORY_SET_HID_REPORT_DESC); 146 } 147 148 @Test(expected = UsbException.class) testThrowsIfConnectionInvalid()149 public void testThrowsIfConnectionInvalid() { 150 when(mDelegate.isValid()).thenReturn(false); 151 mDevice = createDevice(); 152 } 153 154 @Test(expected = UsbException.class) testThrowsIfSerialNumberMissing()155 public void testThrowsIfSerialNumberMissing() { 156 when(mDelegate.getSerialNumber()).thenReturn(null); 157 mDevice = createDevice(); 158 } 159 160 @Test testGetSerialNumber()161 public void testGetSerialNumber() { 162 mDevice = createDevice(); 163 assertEquals(SERIAL_NUMBER, mDevice.getSerialNumber()); 164 } 165 166 @Test testIsAdbEnabled()167 public void testIsAdbEnabled() { 168 mDevice = createDevice(); 169 170 // invalid VID and valid PID 171 when(mDelegate.getVendorId()).thenReturn(INVALID_VID); 172 when(mDelegate.getProductId()).thenReturn(ADB_ENABLED_PID); 173 assertFalse(mDevice.isAdbEnabled()); 174 175 // valid VID and invalid PID 176 when(mDelegate.getVendorId()).thenReturn(GOOGLE_VID); 177 when(mDelegate.getProductId()).thenReturn(ADB_DISABLED_PID); 178 assertFalse(mDevice.isAdbEnabled()); 179 180 // both valid 181 when(mDelegate.getVendorId()).thenReturn(GOOGLE_VID); 182 when(mDelegate.getProductId()).thenReturn(ADB_ENABLED_PID); 183 assertTrue(mDevice.isAdbEnabled()); 184 } 185 186 @Test testClose()187 public void testClose() { 188 mDevice = createDevice(); 189 mDevice.close(); 190 191 // unregisters descriptors and closes connection 192 verifyRequest(times(HID_COUNT), ACCESSORY_UNREGISTER_HID); 193 verify(mDelegate, times(1)).close(); 194 } 195 196 // Actions 197 198 @Test testClick()199 public void testClick() { 200 mDevice = createDevice(); 201 clearInvocations(mDevice); 202 mDevice.click(new Point(12, 34)); 203 204 verifyTouches(new Touch(TOUCH_DOWN, 12, 34), new Touch(TOUCH_UP, 12, 34)); 205 } 206 207 @Test testLongClick()208 public void testLongClick() { 209 mDevice = createDevice(); 210 clearInvocations(mDevice); 211 mDevice.longClick(new Point(23, 45)); 212 213 verifyTouches(new Touch(TOUCH_DOWN, 23, 45), new Touch(TOUCH_UP, 23, 45)); 214 verify(mDevice, atLeastOnce()).sleep(LONG_CLICK); 215 } 216 217 @Test testScroll()218 public void testScroll() { 219 mDevice = createDevice(); 220 mDevice.scroll(new Point(0, 0), new Point(0, SCROLL_STEPS)); 221 222 // generate an event for each step, spaced one pixel apart 223 List<Touch> events = 224 Stream.iterate(0, i -> i + 1) 225 .limit(SCROLL_STEPS + 1) 226 .map(i -> new Touch(TOUCH_DOWN, 0, i)) 227 .collect(Collectors.toList()); 228 events.add(new Touch(TOUCH_UP, 0, SCROLL_STEPS)); 229 230 verifyTouches(events); 231 } 232 233 @Test testFling()234 public void testFling() { 235 mDevice = createDevice(); 236 mDevice.fling(new Point(0, 0), new Point(FLING_STEPS, 0)); 237 238 // generate an event for each step, spaced one pixel apart 239 List<Touch> events = 240 Stream.iterate(0, i -> i + 1) 241 .limit(FLING_STEPS + 1) 242 .map(i -> new Touch(TOUCH_DOWN, i, 0)) 243 .collect(Collectors.toList()); 244 events.add(new Touch(TOUCH_UP, FLING_STEPS, 0)); 245 246 verifyTouches(events); 247 } 248 249 @Test testDrag()250 public void testDrag() { 251 mDevice = createDevice(); 252 mDevice.drag(new Point(0, 0), new Point(SCROLL_STEPS, 0)); 253 254 // generate an event for each step, spaced one pixel apart 255 List<Touch> events = 256 Stream.iterate(0, i -> i + 1) 257 .limit(SCROLL_STEPS + 1) 258 .map(i -> new Touch(TOUCH_DOWN, i, 0)) 259 .collect(Collectors.toList()); 260 // drag is a long click followed by a scroll 261 events.add(0, new Touch(TOUCH_DOWN, 0, 0)); 262 events.add(new Touch(TOUCH_UP, SCROLL_STEPS, 0)); 263 264 verifyTouches(events); 265 } 266 267 @Test testWrite()268 public void testWrite() { 269 mDevice = spy(createDevice()); 270 mDevice.write("Test #0123!"); 271 272 verify(mDevice).key(0x17, 0x08, 0x16, 0x17, 0x2C, null, 0x27, 0x1E, 0x1F, 0x20, null); 273 } 274 275 @Test testKey()276 public void testKey() { 277 mDevice = createDevice(); 278 mDevice.key(1, null, 2); 279 280 InOrder order = inOrder(mDelegate); 281 // press and release 1 282 verifyHidRequest(order, times(1), AoaDevice.HID.KEYBOARD, (byte) 1); 283 verifyHidRequest(order, times(1), AoaDevice.HID.KEYBOARD, (byte) 0); 284 // skip null, and press and release 2 285 verifyHidRequest(order, times(1), AoaDevice.HID.KEYBOARD, (byte) 2); 286 verifyHidRequest(order, times(1), AoaDevice.HID.KEYBOARD, (byte) 0); 287 } 288 289 @Test testWakeUp()290 public void testWakeUp() { 291 mDevice = createDevice(); 292 mDevice.wakeUp(); 293 294 verifyHidRequest(times(1), AoaDevice.HID.SYSTEM, SYSTEM_WAKE); 295 } 296 297 @Test testGoHome()298 public void testGoHome() { 299 mDevice = createDevice(); 300 mDevice.goHome(); 301 302 verifyHidRequest(times(1), AoaDevice.HID.SYSTEM, SYSTEM_HOME); 303 } 304 305 @Test testGoBack()306 public void testGoBack() { 307 mDevice = createDevice(); 308 mDevice.goBack(); 309 310 verifyHidRequest(times(1), AoaDevice.HID.SYSTEM, SYSTEM_BACK); 311 } 312 313 @Test testRetryAction()314 public void testRetryAction() { 315 mDevice = createDevice(); 316 317 // first attempt will fail to find device 318 when(mDelegate.controlTransfer(anyByte(), anyByte(), anyInt(), anyInt(), any())) 319 .thenReturn(DEVICE_NOT_FOUND) 320 .thenReturn(0); 321 322 mDevice.click(new Point(34, 56)); 323 324 // reset the connection 325 verify(mHelper, times(1)).getDevice(eq(SERIAL_NUMBER), any()); 326 327 verifyTouches( 328 new Touch(TOUCH_DOWN, 34, 56), // failed 329 new Touch(TOUCH_DOWN, 34, 56), // retry after resetting connection 330 new Touch(TOUCH_UP, 34, 56)); 331 } 332 333 // Helpers 334 createDevice()335 private AoaDevice createDevice() { 336 AoaDevice device = new AoaDevice(mHelper, mDelegate) { 337 @Override 338 public void sleep(@Nonnull Duration duration) {} 339 }; 340 return spy(device); 341 } 342 verifyRequest(VerificationMode mode, byte request)343 private void verifyRequest(VerificationMode mode, byte request) { 344 verify(mDelegate, mode) 345 .controlTransfer(anyByte(), eq(request), anyInt(), anyInt(), any()); 346 } 347 verifyHidRequest(VerificationMode mode, AoaDevice.HID hid, byte... data)348 private void verifyHidRequest(VerificationMode mode, AoaDevice.HID hid, byte... data) { 349 verifyHidRequest(null, mode, hid, data); 350 } 351 verifyHidRequest( InOrder order, VerificationMode mode, AoaDevice.HID hid, byte... data)352 private void verifyHidRequest( 353 InOrder order, VerificationMode mode, AoaDevice.HID hid, byte... data) { 354 UsbDevice verifier = 355 order == null ? verify(mDelegate, mode) : order.verify(mDelegate, mode); 356 verifier.controlTransfer( 357 anyByte(), eq(ACCESSORY_SEND_HID_EVENT), eq(hid.getId()), anyInt(), eq(data)); 358 } 359 verifyTouches(Touch... expected)360 private void verifyTouches(Touch... expected) { 361 verifyTouches(Arrays.asList(expected)); 362 } 363 verifyTouches(List<Touch> expected)364 private void verifyTouches(List<Touch> expected) { 365 ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class); 366 367 verify(mDelegate, times(expected.size())) 368 .controlTransfer( 369 anyByte(), 370 eq(ACCESSORY_SEND_HID_EVENT), 371 eq(AoaDevice.HID.TOUCH_SCREEN.getId()), 372 anyInt(), 373 captor.capture()); 374 375 List<Touch> events = 376 captor.getAllValues().stream().map(Touch::new).collect(Collectors.toList()); 377 assertEquals(expected, events); 378 } 379 380 /** Touch HID event. */ 381 private static class Touch { 382 383 private final byte mType; 384 private final int mX; 385 private final int mY; 386 Touch(byte type, int x, int y)387 private Touch(byte type, int x, int y) { 388 mType = type; 389 mX = x; 390 mY = y; 391 } 392 Touch(byte[] data)393 private Touch(byte[] data) { 394 mType = data[0]; 395 mX = Shorts.fromBytes(data[2], data[1]); 396 mY = Shorts.fromBytes(data[4], data[3]); 397 } 398 399 @Override equals(Object object)400 public boolean equals(Object object) { 401 if (this == object) { 402 return true; 403 } 404 if (!(object instanceof Touch)) { 405 return false; 406 } 407 Touch event = (Touch) object; 408 return mType == event.mType && mX == event.mX && mY == event.mY; 409 } 410 411 @Override hashCode()412 public int hashCode() { 413 return Objects.hash(mType, mX, mY); 414 } 415 416 @Override toString()417 public String toString() { 418 return String.format("Touch{%d, %d, %d}", mType, mX, mY); 419 } 420 } 421 } 422