Make virtual input device creation synchronous am: eb2d1e65b5

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17475784

Change-Id: I8485be33d6b51c0c9713456d237858e25189fbb9
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Prabir Pradhan
2022-05-05 19:38:09 +00:00
committed by Automerger Merge Worker
5 changed files with 297 additions and 64 deletions

View File

@@ -22,6 +22,7 @@ import android.annotation.StringDef;
import android.graphics.Point;
import android.graphics.PointF;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerInternal;
import android.hardware.input.VirtualKeyEvent;
@@ -29,11 +30,13 @@ import android.hardware.input.VirtualMouseButtonEvent;
import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
import android.hardware.input.VirtualTouchEvent;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.Display;
import android.view.InputDevice;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -44,7 +47,11 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Iterator;
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.function.Supplier;
/** Controls virtual input devices, including device lifecycle and event dispatch. */
class InputController {
@@ -72,20 +79,27 @@ class InputController {
@GuardedBy("mLock")
final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
private final Handler mHandler;
private final NativeWrapper mNativeWrapper;
private final DisplayManagerInternal mDisplayManagerInternal;
private final InputManagerInternal mInputManagerInternal;
private final DeviceCreationThreadVerifier mThreadVerifier;
InputController(@NonNull Object lock) {
this(lock, new NativeWrapper());
InputController(@NonNull Object lock, @NonNull Handler handler) {
this(lock, new NativeWrapper(), handler,
// Verify that virtual devices are not created on the handler thread.
() -> !handler.getLooper().isCurrentThread());
}
@VisibleForTesting
InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper) {
InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper,
@NonNull Handler handler, @NonNull DeviceCreationThreadVerifier threadVerifier) {
mLock = lock;
mHandler = handler;
mNativeWrapper = nativeWrapper;
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
mThreadVerifier = threadVerifier;
}
void close() {
@@ -108,23 +122,13 @@ class InputController {
@NonNull IBinder deviceToken,
int displayId) {
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 {
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
} catch (RemoteException e) {
// TODO(b/215608394): remove and close InputDeviceDescriptor
throw new RuntimeException("Could not create virtual keyboard", e);
createDeviceInternal(InputDeviceDescriptor.TYPE_KEYBOARD, deviceName, vendorId,
productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys));
} catch (DeviceCreationException e) {
throw new RuntimeException(
"Failed to create virtual keyboard device '" + deviceName + "'.", e);
}
}
@@ -134,25 +138,15 @@ class InputController {
@NonNull IBinder deviceToken,
int displayId) {
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 {
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
} catch (RemoteException e) {
// TODO(b/215608394): remove and close InputDeviceDescriptor
throw new RuntimeException("Could not create virtual mouse", e);
createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
} catch (DeviceCreationException e) {
throw new RuntimeException(
"Failed to create virtual mouse device: '" + deviceName + "'.", e);
}
mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
}
void createTouchscreen(@NonNull String deviceName,
@@ -162,24 +156,14 @@ class InputController {
int displayId,
@NonNull Point screenSize) {
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 {
deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
} catch (RemoteException e) {
// TODO(b/215608394): remove and close InputDeviceDescriptor
throw new RuntimeException("Could not create virtual touchscreen", e);
createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
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);
}
}
/** 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();
}
}

View File

@@ -166,7 +166,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
mAppToken = token;
mParams = params;
if (inputController == null) {
mInputController = new InputController(mVirtualDeviceLock);
mInputController = new InputController(
mVirtualDeviceLock, context.getMainThreadHandler());
} else {
mInputController = inputController;
}

View File

@@ -21,20 +21,21 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.IInputManager;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerInternal;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Display;
import android.view.DisplayInfo;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.LocalServices;
import org.junit.Before;
@@ -44,7 +45,8 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@Presubmit
@RunWith(AndroidJUnit4.class)
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class InputControllerTest {
@Mock
@@ -56,11 +58,14 @@ public class InputControllerTest {
@Mock
private IInputManager mIInputManagerMock;
private InputManagerMockHelper mInputManagerMockHelper;
private InputController mInputController;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mInputManagerMockHelper = new InputManagerMockHelper(
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
doNothing().when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
LocalServices.removeServiceForTest(InputManagerInternal.class);
@@ -72,10 +77,10 @@ public class InputControllerTest {
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
InputManager.resetInstance(mIInputManagerMock);
doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
mInputController = new InputController(new Object(), mNativeWrapperMock);
// 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);
}
@Test
@@ -83,6 +88,7 @@ public class InputControllerTest {
final IBinder deviceToken = new Binder();
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
/* displayId= */ 1);
verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
mInputController.unregisterInputDevice(deviceToken);
@@ -95,10 +101,12 @@ public class InputControllerTest {
final IBinder deviceToken = new Binder();
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
/* displayId= */ 1);
verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
final IBinder deviceToken2 = new Binder();
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken2,
/* displayId= */ 2);
verify(mNativeWrapperMock, times(2)).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(2));
mInputController.unregisterInputDevice(deviceToken);
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));

View File

@@ -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;
}
}

View File

@@ -54,6 +54,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.graphics.Point;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.IInputManager;
import android.hardware.input.InputManagerInternal;
import android.hardware.input.VirtualKeyEvent;
import android.hardware.input.VirtualMouseButtonEvent;
@@ -118,6 +119,7 @@ public class VirtualDeviceManagerServiceTest {
private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
private Context mContext;
private InputManagerMockHelper mInputManagerMockHelper;
private VirtualDeviceImpl mDeviceImpl;
private InputController mInputController;
private AssociationInfo mAssociationInfo;
@@ -146,6 +148,8 @@ public class VirtualDeviceManagerServiceTest {
private IAudioConfigChangedCallback mConfigChangedCallback;
@Mock
private ApplicationInfo mApplicationInfoMock;
@Mock
IInputManager mIInputManagerMock;
private ArraySet<ComponentName> getBlockedActivities() {
ArraySet<ComponentName> blockedActivities = new ArraySet<>();
@@ -170,7 +174,7 @@ public class VirtualDeviceManagerServiceTest {
}
@Before
public void setUp() {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
@@ -199,7 +203,13 @@ public class VirtualDeviceManagerServiceTest {
new Handler(TestableLooper.get(this).getLooper()));
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,
MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);