Make virtual input device creation synchronous am: eb2d1e65b5
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17475784 Change-Id: Ibba5b09bcb1b9c3c7de2b2bb05959b634ba111b6 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import android.annotation.StringDef;
|
|||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.graphics.PointF;
|
import android.graphics.PointF;
|
||||||
import android.hardware.display.DisplayManagerInternal;
|
import android.hardware.display.DisplayManagerInternal;
|
||||||
|
import android.hardware.input.InputDeviceIdentifier;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.hardware.input.InputManagerInternal;
|
import android.hardware.input.InputManagerInternal;
|
||||||
import android.hardware.input.VirtualKeyEvent;
|
import android.hardware.input.VirtualKeyEvent;
|
||||||
@@ -29,11 +30,13 @@ import android.hardware.input.VirtualMouseButtonEvent;
|
|||||||
import android.hardware.input.VirtualMouseRelativeEvent;
|
import android.hardware.input.VirtualMouseRelativeEvent;
|
||||||
import android.hardware.input.VirtualMouseScrollEvent;
|
import android.hardware.input.VirtualMouseScrollEvent;
|
||||||
import android.hardware.input.VirtualTouchEvent;
|
import android.hardware.input.VirtualTouchEvent;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.ArrayMap;
|
import android.util.ArrayMap;
|
||||||
import android.util.Slog;
|
import android.util.Slog;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
|
||||||
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.GuardedBy;
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
@@ -44,7 +47,11 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/** Controls virtual input devices, including device lifecycle and event dispatch. */
|
/** Controls virtual input devices, including device lifecycle and event dispatch. */
|
||||||
class InputController {
|
class InputController {
|
||||||
@@ -72,20 +79,27 @@ class InputController {
|
|||||||
@GuardedBy("mLock")
|
@GuardedBy("mLock")
|
||||||
final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
|
final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
|
||||||
|
|
||||||
|
private final Handler mHandler;
|
||||||
private final NativeWrapper mNativeWrapper;
|
private final NativeWrapper mNativeWrapper;
|
||||||
private final DisplayManagerInternal mDisplayManagerInternal;
|
private final DisplayManagerInternal mDisplayManagerInternal;
|
||||||
private final InputManagerInternal mInputManagerInternal;
|
private final InputManagerInternal mInputManagerInternal;
|
||||||
|
private final DeviceCreationThreadVerifier mThreadVerifier;
|
||||||
|
|
||||||
InputController(@NonNull Object lock) {
|
InputController(@NonNull Object lock, @NonNull Handler handler) {
|
||||||
this(lock, new NativeWrapper());
|
this(lock, new NativeWrapper(), handler,
|
||||||
|
// Verify that virtual devices are not created on the handler thread.
|
||||||
|
() -> !handler.getLooper().isCurrentThread());
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper) {
|
InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper,
|
||||||
|
@NonNull Handler handler, @NonNull DeviceCreationThreadVerifier threadVerifier) {
|
||||||
mLock = lock;
|
mLock = lock;
|
||||||
|
mHandler = handler;
|
||||||
mNativeWrapper = nativeWrapper;
|
mNativeWrapper = nativeWrapper;
|
||||||
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
|
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
|
||||||
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
|
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
|
||||||
|
mThreadVerifier = threadVerifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
void close() {
|
void close() {
|
||||||
@@ -108,23 +122,13 @@ class InputController {
|
|||||||
@NonNull IBinder deviceToken,
|
@NonNull IBinder deviceToken,
|
||||||
int displayId) {
|
int displayId) {
|
||||||
final String phys = createPhys(PHYS_TYPE_KEYBOARD);
|
final String phys = createPhys(PHYS_TYPE_KEYBOARD);
|
||||||
setUniqueIdAssociation(displayId, phys);
|
|
||||||
final int fd = mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys);
|
|
||||||
if (fd < 0) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"A native error occurred when creating keyboard: " + -fd);
|
|
||||||
}
|
|
||||||
final BinderDeathRecipient binderDeathRecipient = new BinderDeathRecipient(deviceToken);
|
|
||||||
synchronized (mLock) {
|
|
||||||
mInputDeviceDescriptors.put(deviceToken,
|
|
||||||
new InputDeviceDescriptor(fd, binderDeathRecipient,
|
|
||||||
InputDeviceDescriptor.TYPE_KEYBOARD, displayId, phys));
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
|
createDeviceInternal(InputDeviceDescriptor.TYPE_KEYBOARD, deviceName, vendorId,
|
||||||
} catch (RemoteException e) {
|
productId, deviceToken, displayId, phys,
|
||||||
// TODO(b/215608394): remove and close InputDeviceDescriptor
|
() -> mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys));
|
||||||
throw new RuntimeException("Could not create virtual keyboard", e);
|
} catch (DeviceCreationException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to create virtual keyboard device '" + deviceName + "'.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,25 +138,15 @@ class InputController {
|
|||||||
@NonNull IBinder deviceToken,
|
@NonNull IBinder deviceToken,
|
||||||
int displayId) {
|
int displayId) {
|
||||||
final String phys = createPhys(PHYS_TYPE_MOUSE);
|
final String phys = createPhys(PHYS_TYPE_MOUSE);
|
||||||
setUniqueIdAssociation(displayId, phys);
|
|
||||||
final int fd = mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys);
|
|
||||||
if (fd < 0) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"A native error occurred when creating mouse: " + -fd);
|
|
||||||
}
|
|
||||||
final BinderDeathRecipient binderDeathRecipient = new BinderDeathRecipient(deviceToken);
|
|
||||||
synchronized (mLock) {
|
|
||||||
mInputDeviceDescriptors.put(deviceToken,
|
|
||||||
new InputDeviceDescriptor(fd, binderDeathRecipient,
|
|
||||||
InputDeviceDescriptor.TYPE_MOUSE, displayId, phys));
|
|
||||||
mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
|
createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
|
||||||
} catch (RemoteException e) {
|
deviceToken, displayId, phys,
|
||||||
// TODO(b/215608394): remove and close InputDeviceDescriptor
|
() -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
|
||||||
throw new RuntimeException("Could not create virtual mouse", e);
|
} catch (DeviceCreationException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to create virtual mouse device: '" + deviceName + "'.", e);
|
||||||
}
|
}
|
||||||
|
mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void createTouchscreen(@NonNull String deviceName,
|
void createTouchscreen(@NonNull String deviceName,
|
||||||
@@ -162,24 +156,14 @@ class InputController {
|
|||||||
int displayId,
|
int displayId,
|
||||||
@NonNull Point screenSize) {
|
@NonNull Point screenSize) {
|
||||||
final String phys = createPhys(PHYS_TYPE_TOUCHSCREEN);
|
final String phys = createPhys(PHYS_TYPE_TOUCHSCREEN);
|
||||||
setUniqueIdAssociation(displayId, phys);
|
|
||||||
final int fd = mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId, phys,
|
|
||||||
screenSize.y, screenSize.x);
|
|
||||||
if (fd < 0) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"A native error occurred when creating touchscreen: " + -fd);
|
|
||||||
}
|
|
||||||
final BinderDeathRecipient binderDeathRecipient = new BinderDeathRecipient(deviceToken);
|
|
||||||
synchronized (mLock) {
|
|
||||||
mInputDeviceDescriptors.put(deviceToken,
|
|
||||||
new InputDeviceDescriptor(fd, binderDeathRecipient,
|
|
||||||
InputDeviceDescriptor.TYPE_TOUCHSCREEN, displayId, phys));
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
|
createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
|
||||||
} catch (RemoteException e) {
|
productId, deviceToken, displayId, phys,
|
||||||
// TODO(b/215608394): remove and close InputDeviceDescriptor
|
() -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
|
||||||
throw new RuntimeException("Could not create virtual touchscreen", e);
|
phys, screenSize.y, screenSize.x));
|
||||||
|
} catch (DeviceCreationException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to create virtual touchscreen device '" + deviceName + "'.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,4 +494,133 @@ class InputController {
|
|||||||
unregisterInputDevice(mDeviceToken);
|
unregisterInputDevice(mDeviceToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A helper class used to wait for an input device to be registered. */
|
||||||
|
private class WaitForDevice implements AutoCloseable {
|
||||||
|
private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1);
|
||||||
|
private final InputManager.InputDeviceListener mListener;
|
||||||
|
|
||||||
|
WaitForDevice(String deviceName, int vendorId, int productId) {
|
||||||
|
mListener = new InputManager.InputDeviceListener() {
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceAdded(int deviceId) {
|
||||||
|
final InputDevice device = InputManager.getInstance().getInputDevice(
|
||||||
|
deviceId);
|
||||||
|
Objects.requireNonNull(device, "Newly added input device was null.");
|
||||||
|
if (!device.getName().equals(deviceName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final InputDeviceIdentifier id = device.getIdentifier();
|
||||||
|
if (id.getVendorId() != vendorId || id.getProductId() != productId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mDeviceAddedLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceRemoved(int deviceId) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceChanged(int deviceId) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
InputManager.getInstance().registerInputDeviceListener(mListener, mHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Note: This must not be called from {@link #mHandler}'s thread. */
|
||||||
|
void waitForDeviceCreation() throws DeviceCreationException {
|
||||||
|
try {
|
||||||
|
if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) {
|
||||||
|
throw new DeviceCreationException(
|
||||||
|
"Timed out waiting for virtual device to be created.");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new DeviceCreationException(
|
||||||
|
"Interrupted while waiting for virtual device to be created.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
InputManager.getInstance().unregisterInputDeviceListener(mListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An internal exception that is thrown to indicate an error when opening a virtual device. */
|
||||||
|
private static class DeviceCreationException extends Exception {
|
||||||
|
DeviceCreationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
DeviceCreationException(String message, Exception cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a virtual input device synchronously, and waits for the notification that the device
|
||||||
|
* was added.
|
||||||
|
*
|
||||||
|
* Note: Input device creation is expected to happen on a binder thread, and the calling thread
|
||||||
|
* will be blocked until the input device creation is successful. This should not be called on
|
||||||
|
* the handler's thread.
|
||||||
|
*
|
||||||
|
* @throws DeviceCreationException Throws this exception if anything unexpected happens in the
|
||||||
|
* process of creating the device. This method will take care
|
||||||
|
* to restore the state of the system in the event of any
|
||||||
|
* unexpected behavior.
|
||||||
|
*/
|
||||||
|
private void createDeviceInternal(@InputDeviceDescriptor.Type int type, String deviceName,
|
||||||
|
int vendorId, int productId, IBinder deviceToken, int displayId, String phys,
|
||||||
|
Supplier<Integer> deviceOpener)
|
||||||
|
throws DeviceCreationException {
|
||||||
|
if (!mThreadVerifier.isValidThread()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Virtual device creation should happen on an auxiliary thread (e.g. binder "
|
||||||
|
+ "thread) and not from the handler's thread.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int fd;
|
||||||
|
final BinderDeathRecipient binderDeathRecipient;
|
||||||
|
|
||||||
|
setUniqueIdAssociation(displayId, phys);
|
||||||
|
try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
|
||||||
|
fd = deviceOpener.get();
|
||||||
|
if (fd < 0) {
|
||||||
|
throw new DeviceCreationException(
|
||||||
|
"A native error occurred when creating touchscreen: " + -fd);
|
||||||
|
}
|
||||||
|
// The fd is valid from here, so ensure that all failures close the fd after this point.
|
||||||
|
try {
|
||||||
|
waiter.waitForDeviceCreation();
|
||||||
|
|
||||||
|
binderDeathRecipient = new BinderDeathRecipient(deviceToken);
|
||||||
|
try {
|
||||||
|
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new DeviceCreationException(
|
||||||
|
"Client died before virtual device could be created.", e);
|
||||||
|
}
|
||||||
|
} catch (DeviceCreationException e) {
|
||||||
|
mNativeWrapper.closeUinput(fd);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (DeviceCreationException e) {
|
||||||
|
InputManager.getInstance().removeUniqueIdAssociation(phys);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (mLock) {
|
||||||
|
mInputDeviceDescriptors.put(deviceToken,
|
||||||
|
new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
interface DeviceCreationThreadVerifier {
|
||||||
|
/** Returns true if the calling thread is a valid thread for device creation. */
|
||||||
|
boolean isValidThread();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
|
|||||||
mAppToken = token;
|
mAppToken = token;
|
||||||
mParams = params;
|
mParams = params;
|
||||||
if (inputController == null) {
|
if (inputController == null) {
|
||||||
mInputController = new InputController(mVirtualDeviceLock);
|
mInputController = new InputController(
|
||||||
|
mVirtualDeviceLock, context.getMainThreadHandler());
|
||||||
} else {
|
} else {
|
||||||
mInputController = inputController;
|
mInputController = inputController;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,20 +21,21 @@ import static org.mockito.ArgumentMatchers.anyString;
|
|||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
import android.hardware.display.DisplayManagerInternal;
|
import android.hardware.display.DisplayManagerInternal;
|
||||||
import android.hardware.input.IInputManager;
|
import android.hardware.input.IInputManager;
|
||||||
import android.hardware.input.InputManager;
|
|
||||||
import android.hardware.input.InputManagerInternal;
|
import android.hardware.input.InputManagerInternal;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.platform.test.annotations.Presubmit;
|
import android.platform.test.annotations.Presubmit;
|
||||||
|
import android.testing.AndroidTestingRunner;
|
||||||
|
import android.testing.TestableLooper;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.DisplayInfo;
|
import android.view.DisplayInfo;
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4;
|
|
||||||
|
|
||||||
import com.android.server.LocalServices;
|
import com.android.server.LocalServices;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -44,7 +45,8 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
@Presubmit
|
@Presubmit
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidTestingRunner.class)
|
||||||
|
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||||
public class InputControllerTest {
|
public class InputControllerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
@@ -56,11 +58,14 @@ public class InputControllerTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private IInputManager mIInputManagerMock;
|
private IInputManager mIInputManagerMock;
|
||||||
|
|
||||||
|
private InputManagerMockHelper mInputManagerMockHelper;
|
||||||
private InputController mInputController;
|
private InputController mInputController;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
|
mInputManagerMockHelper = new InputManagerMockHelper(
|
||||||
|
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
|
||||||
|
|
||||||
doNothing().when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
|
doNothing().when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
|
||||||
LocalServices.removeServiceForTest(InputManagerInternal.class);
|
LocalServices.removeServiceForTest(InputManagerInternal.class);
|
||||||
@@ -72,10 +77,10 @@ public class InputControllerTest {
|
|||||||
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
|
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
|
||||||
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
|
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
|
||||||
|
|
||||||
InputManager.resetInstance(mIInputManagerMock);
|
// Allow virtual devices to be created on the looper thread for testing.
|
||||||
doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
|
final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
|
||||||
doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
|
mInputController = new InputController(new Object(), mNativeWrapperMock,
|
||||||
mInputController = new InputController(new Object(), mNativeWrapperMock);
|
new Handler(TestableLooper.get(this).getLooper()), threadVerifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -83,6 +88,7 @@ public class InputControllerTest {
|
|||||||
final IBinder deviceToken = new Binder();
|
final IBinder deviceToken = new Binder();
|
||||||
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
|
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
|
||||||
/* displayId= */ 1);
|
/* displayId= */ 1);
|
||||||
|
verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
|
||||||
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
|
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
|
||||||
doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
|
doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
|
||||||
mInputController.unregisterInputDevice(deviceToken);
|
mInputController.unregisterInputDevice(deviceToken);
|
||||||
@@ -95,10 +101,12 @@ public class InputControllerTest {
|
|||||||
final IBinder deviceToken = new Binder();
|
final IBinder deviceToken = new Binder();
|
||||||
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
|
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
|
||||||
/* displayId= */ 1);
|
/* displayId= */ 1);
|
||||||
|
verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
|
||||||
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
|
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
|
||||||
final IBinder deviceToken2 = new Binder();
|
final IBinder deviceToken2 = new Binder();
|
||||||
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken2,
|
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken2,
|
||||||
/* displayId= */ 2);
|
/* displayId= */ 2);
|
||||||
|
verify(mNativeWrapperMock, times(2)).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
|
||||||
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(2));
|
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(2));
|
||||||
mInputController.unregisterInputDevice(deviceToken);
|
mInputController.unregisterInputDevice(deviceToken);
|
||||||
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
|
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.server.companion.virtual;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.notNull;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.hardware.input.IInputDevicesChangedListener;
|
||||||
|
import android.hardware.input.IInputManager;
|
||||||
|
import android.hardware.input.InputManager;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.testing.TestableLooper;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test utility class used to share the logic for setting up {@link InputManager}'s callback for
|
||||||
|
* when a virtual input device being added.
|
||||||
|
*/
|
||||||
|
class InputManagerMockHelper {
|
||||||
|
private final TestableLooper mTestableLooper;
|
||||||
|
private final InputController.NativeWrapper mNativeWrapperMock;
|
||||||
|
private final IInputManager mIInputManagerMock;
|
||||||
|
private final List<InputDevice> mDevices = new ArrayList<>();
|
||||||
|
private IInputDevicesChangedListener mDevicesChangedListener;
|
||||||
|
|
||||||
|
InputManagerMockHelper(TestableLooper testableLooper,
|
||||||
|
InputController.NativeWrapper nativeWrapperMock, IInputManager iInputManagerMock)
|
||||||
|
throws Exception {
|
||||||
|
mTestableLooper = testableLooper;
|
||||||
|
mNativeWrapperMock = nativeWrapperMock;
|
||||||
|
mIInputManagerMock = iInputManagerMock;
|
||||||
|
|
||||||
|
doAnswer(this::handleNativeOpenInputDevice).when(mNativeWrapperMock).openUinputMouse(
|
||||||
|
anyString(), anyInt(), anyInt(), anyString());
|
||||||
|
doAnswer(this::handleNativeOpenInputDevice).when(mNativeWrapperMock).openUinputKeyboard(
|
||||||
|
anyString(), anyInt(), anyInt(), anyString());
|
||||||
|
doAnswer(this::handleNativeOpenInputDevice).when(mNativeWrapperMock).openUinputTouchscreen(
|
||||||
|
anyString(), anyInt(), anyInt(), anyString(), anyInt(), anyInt());
|
||||||
|
|
||||||
|
doAnswer(inv -> {
|
||||||
|
mDevicesChangedListener = inv.getArgument(0);
|
||||||
|
return null;
|
||||||
|
}).when(mIInputManagerMock).registerInputDevicesChangedListener(notNull());
|
||||||
|
when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]);
|
||||||
|
doAnswer(inv -> mDevices.get(inv.getArgument(0)))
|
||||||
|
.when(mIInputManagerMock).getInputDevice(anyInt());
|
||||||
|
doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
|
||||||
|
doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
|
||||||
|
|
||||||
|
// Set a new instance of InputManager for testing that uses the IInputManager mock as the
|
||||||
|
// interface to the server.
|
||||||
|
InputManager.resetInstance(mIInputManagerMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Void handleNativeOpenInputDevice(InvocationOnMock inv) {
|
||||||
|
Objects.requireNonNull(mDevicesChangedListener,
|
||||||
|
"InputController did not register an InputDevicesChangedListener.");
|
||||||
|
// We only use a subset of the fields of InputDevice in InputController.
|
||||||
|
final InputDevice device = new InputDevice(mDevices.size() /*id*/, 1 /*generation*/, 0,
|
||||||
|
inv.getArgument(0) /*name*/, inv.getArgument(1) /*vendorId*/,
|
||||||
|
inv.getArgument(2) /*productId*/, inv.getArgument(3) /*descriptor*/,
|
||||||
|
true /*isExternal*/, 0 /*sources*/, 0 /*keyboardType*/,
|
||||||
|
null /*keyCharacterMap*/, false /*hasVibrator*/, false /*hasMic*/,
|
||||||
|
false /*hasButtonUnderPad*/, false /*hasSensor*/, false /*hasBattery*/);
|
||||||
|
mDevices.add(device);
|
||||||
|
try {
|
||||||
|
mDevicesChangedListener.onInputDevicesChanged(
|
||||||
|
mDevices.stream().flatMapToInt(
|
||||||
|
d -> IntStream.of(d.getId(), d.getGeneration())).toArray());
|
||||||
|
} catch (RemoteException ignored) {
|
||||||
|
}
|
||||||
|
// Process the device added notification.
|
||||||
|
mTestableLooper.processAllMessages();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ import android.content.pm.ActivityInfo;
|
|||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.hardware.display.DisplayManagerInternal;
|
import android.hardware.display.DisplayManagerInternal;
|
||||||
|
import android.hardware.input.IInputManager;
|
||||||
import android.hardware.input.InputManagerInternal;
|
import android.hardware.input.InputManagerInternal;
|
||||||
import android.hardware.input.VirtualKeyEvent;
|
import android.hardware.input.VirtualKeyEvent;
|
||||||
import android.hardware.input.VirtualMouseButtonEvent;
|
import android.hardware.input.VirtualMouseButtonEvent;
|
||||||
@@ -118,6 +119,7 @@ public class VirtualDeviceManagerServiceTest {
|
|||||||
private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
|
private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
private InputManagerMockHelper mInputManagerMockHelper;
|
||||||
private VirtualDeviceImpl mDeviceImpl;
|
private VirtualDeviceImpl mDeviceImpl;
|
||||||
private InputController mInputController;
|
private InputController mInputController;
|
||||||
private AssociationInfo mAssociationInfo;
|
private AssociationInfo mAssociationInfo;
|
||||||
@@ -146,6 +148,8 @@ public class VirtualDeviceManagerServiceTest {
|
|||||||
private IAudioConfigChangedCallback mConfigChangedCallback;
|
private IAudioConfigChangedCallback mConfigChangedCallback;
|
||||||
@Mock
|
@Mock
|
||||||
private ApplicationInfo mApplicationInfoMock;
|
private ApplicationInfo mApplicationInfoMock;
|
||||||
|
@Mock
|
||||||
|
IInputManager mIInputManagerMock;
|
||||||
|
|
||||||
private ArraySet<ComponentName> getBlockedActivities() {
|
private ArraySet<ComponentName> getBlockedActivities() {
|
||||||
ArraySet<ComponentName> blockedActivities = new ArraySet<>();
|
ArraySet<ComponentName> blockedActivities = new ArraySet<>();
|
||||||
@@ -170,7 +174,7 @@ public class VirtualDeviceManagerServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() throws Exception {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
|
|
||||||
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
|
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
|
||||||
@@ -199,7 +203,13 @@ public class VirtualDeviceManagerServiceTest {
|
|||||||
new Handler(TestableLooper.get(this).getLooper()));
|
new Handler(TestableLooper.get(this).getLooper()));
|
||||||
when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
|
when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
|
||||||
|
|
||||||
mInputController = new InputController(new Object(), mNativeWrapperMock);
|
mInputManagerMockHelper = new InputManagerMockHelper(
|
||||||
|
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
|
||||||
|
// Allow virtual devices to be created on the looper thread for testing.
|
||||||
|
final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
|
||||||
|
mInputController = new InputController(new Object(), mNativeWrapperMock,
|
||||||
|
new Handler(TestableLooper.get(this).getLooper()), threadVerifier);
|
||||||
|
|
||||||
mAssociationInfo = new AssociationInfo(1, 0, null,
|
mAssociationInfo = new AssociationInfo(1, 0, null,
|
||||||
MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);
|
MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user