blob: 307f7bfc1fd576270bea6b22bd61902ad3072115 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.StringDef;
import android.graphics.PointF;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
import android.hardware.input.VirtualKeyEvent;
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.IInputConstants;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.Display;
import android.view.InputDevice;
import android.view.WindowManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
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 {
private static final String TAG = "VirtualInputController";
private static final AtomicLong sNextPhysId = new AtomicLong(1);
static final String NAVIGATION_TOUCHPAD_DEVICE_TYPE = "touchNavigation";
static final String PHYS_TYPE_DPAD = "Dpad";
static final String PHYS_TYPE_KEYBOARD = "Keyboard";
static final String PHYS_TYPE_MOUSE = "Mouse";
static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen";
static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad";
@StringDef(prefix = { "PHYS_TYPE_" }, value = {
PHYS_TYPE_DPAD,
PHYS_TYPE_KEYBOARD,
PHYS_TYPE_MOUSE,
PHYS_TYPE_TOUCHSCREEN,
PHYS_TYPE_NAVIGATION_TOUCHPAD,
})
@Retention(RetentionPolicy.SOURCE)
@interface PhysType {
}
/**
* The maximum length of a device name (in bytes in UTF-8 encoding).
*
* This limitation comes directly from uinput.
* See also UINPUT_MAX_NAME_SIZE in linux/uinput.h
*/
private static final int DEVICE_NAME_MAX_LENGTH = 80;
final Object mLock = new Object();
/* Token -> file descriptor associations. */
@GuardedBy("mLock")
private final ArrayMap<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors =
new ArrayMap<>();
private final Handler mHandler;
private final NativeWrapper mNativeWrapper;
private final DisplayManagerInternal mDisplayManagerInternal;
private final InputManagerInternal mInputManagerInternal;
private final WindowManager mWindowManager;
private final DeviceCreationThreadVerifier mThreadVerifier;
InputController(@NonNull Handler handler,
@NonNull WindowManager windowManager) {
this(new NativeWrapper(), handler, windowManager,
// Verify that virtual devices are not created on the handler thread.
() -> !handler.getLooper().isCurrentThread());
}
@VisibleForTesting
InputController(@NonNull NativeWrapper nativeWrapper,
@NonNull Handler handler, @NonNull WindowManager windowManager,
@NonNull DeviceCreationThreadVerifier threadVerifier) {
mHandler = handler;
mNativeWrapper = nativeWrapper;
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
mWindowManager = windowManager;
mThreadVerifier = threadVerifier;
}
void close() {
synchronized (mLock) {
final Iterator<Map.Entry<IBinder, InputDeviceDescriptor>> iterator =
mInputDeviceDescriptors.entrySet().iterator();
if (iterator.hasNext()) {
final Map.Entry<IBinder, InputDeviceDescriptor> entry = iterator.next();
final IBinder token = entry.getKey();
final InputDeviceDescriptor inputDeviceDescriptor = entry.getValue();
iterator.remove();
closeInputDeviceDescriptorLocked(token, inputDeviceDescriptor);
}
}
}
void createDpad(@NonNull String deviceName,
int vendorId,
int productId,
@NonNull IBinder deviceToken,
int displayId) {
final String phys = createPhys(PHYS_TYPE_DPAD);
try {
createDeviceInternal(InputDeviceDescriptor.TYPE_DPAD, deviceName, vendorId,
productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputDpad(deviceName, vendorId, productId, phys));
} catch (DeviceCreationException e) {
throw new RuntimeException(
"Failed to create virtual dpad device '" + deviceName + "'.", e);
}
}
void createKeyboard(@NonNull String deviceName, int vendorId, int productId,
@NonNull IBinder deviceToken, int displayId, @NonNull String languageTag,
@NonNull String layoutType) {
final String phys = createPhys(PHYS_TYPE_KEYBOARD);
mInputManagerInternal.addKeyboardLayoutAssociation(phys, languageTag,
layoutType);
try {
createDeviceInternal(InputDeviceDescriptor.TYPE_KEYBOARD, deviceName, vendorId,
productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys));
} catch (DeviceCreationException e) {
mInputManagerInternal.removeKeyboardLayoutAssociation(phys);
throw new RuntimeException(
"Failed to create virtual keyboard device '" + deviceName + "'.", e);
}
}
void createMouse(@NonNull String deviceName,
int vendorId,
int productId,
@NonNull IBinder deviceToken,
int displayId) {
final String phys = createPhys(PHYS_TYPE_MOUSE);
try {
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,
int vendorId,
int productId,
@NonNull IBinder deviceToken,
int displayId,
int height,
int width) {
final String phys = createPhys(PHYS_TYPE_TOUCHSCREEN);
try {
createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
phys, height, width));
} catch (DeviceCreationException e) {
throw new RuntimeException(
"Failed to create virtual touchscreen device '" + deviceName + "'.", e);
}
}
void createNavigationTouchpad(
@NonNull String deviceName,
int vendorId,
int productId,
@NonNull IBinder deviceToken,
int displayId,
int touchpadHeight,
int touchpadWidth) {
final String phys = createPhys(PHYS_TYPE_NAVIGATION_TOUCHPAD);
mInputManagerInternal.setTypeAssociation(phys, NAVIGATION_TOUCHPAD_DEVICE_TYPE);
try {
createDeviceInternal(InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD, deviceName,
vendorId, productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
phys, touchpadHeight, touchpadWidth));
} catch (DeviceCreationException e) {
mInputManagerInternal.unsetTypeAssociation(phys);
throw new RuntimeException(
"Failed to create virtual navigation touchpad device '" + deviceName + "'.", e);
}
}
void unregisterInputDevice(@NonNull IBinder token) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not unregister input device for given token");
}
closeInputDeviceDescriptorLocked(token, inputDeviceDescriptor);
}
}
@GuardedBy("mLock")
private void closeInputDeviceDescriptorLocked(IBinder token,
InputDeviceDescriptor inputDeviceDescriptor) {
token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0);
mNativeWrapper.closeUinput(inputDeviceDescriptor.getNativePointer());
String phys = inputDeviceDescriptor.getPhys();
InputManagerGlobal.getInstance().removeUniqueIdAssociation(phys);
// Type associations are added in the case of navigation touchpads. Those should be removed
// once the input device gets closed.
if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) {
mInputManagerInternal.unsetTypeAssociation(phys);
}
if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_KEYBOARD) {
mInputManagerInternal.removeKeyboardLayoutAssociation(phys);
}
// Reset values to the default if all virtual mice are unregistered, or set display
// id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be
// removed from the mInputDeviceDescriptors instance variable prior to this point.
if (inputDeviceDescriptor.isMouse()) {
if (mInputManagerInternal.getVirtualMousePointerDisplayId()
== inputDeviceDescriptor.getDisplayId()) {
updateActivePointerDisplayIdLocked();
}
}
}
/**
* @return the device id for a given token (identifiying a device)
*/
int getInputDeviceId(IBinder token) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException("Could not get device id for given token");
}
return inputDeviceDescriptor.getInputDeviceId();
}
}
void setShowPointerIcon(boolean visible, int displayId) {
mInputManagerInternal.setPointerIconVisible(visible, displayId);
}
void setPointerAcceleration(float pointerAcceleration, int displayId) {
mInputManagerInternal.setPointerAcceleration(pointerAcceleration, displayId);
}
void setDisplayEligibilityForPointerCapture(boolean isEligible, int displayId) {
mInputManagerInternal.setDisplayEligibilityForPointerCapture(displayId, isEligible);
}
void setLocalIme(int displayId) {
// WM throws a SecurityException if the display is untrusted.
if ((mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
== Display.FLAG_TRUSTED) {
mWindowManager.setDisplayImePolicy(displayId,
WindowManager.DISPLAY_IME_POLICY_LOCAL);
}
}
@GuardedBy("mLock")
private void updateActivePointerDisplayIdLocked() {
InputDeviceDescriptor mostRecentlyCreatedMouse = null;
for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
InputDeviceDescriptor otherInputDeviceDescriptor = mInputDeviceDescriptors.valueAt(i);
if (otherInputDeviceDescriptor.isMouse()) {
if (mostRecentlyCreatedMouse == null
|| (otherInputDeviceDescriptor.getCreationOrderNumber()
> mostRecentlyCreatedMouse.getCreationOrderNumber())) {
mostRecentlyCreatedMouse = otherInputDeviceDescriptor;
}
}
}
if (mostRecentlyCreatedMouse != null) {
mInputManagerInternal.setVirtualMousePointerDisplayId(
mostRecentlyCreatedMouse.getDisplayId());
} else {
// All mice have been unregistered
mInputManagerInternal.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY);
}
}
/**
* Validates a device name by checking length and whether a device with the same name
* already exists. Throws exceptions if the validation fails.
* @param deviceName The name of the device to be validated
* @throws DeviceCreationException if {@code deviceName} is not valid.
*/
private void validateDeviceName(String deviceName) throws DeviceCreationException {
// Comparison is greater or equal because the device name must fit into a const char*
// including the \0-terminator. Therefore the actual number of bytes that can be used
// for device name is DEVICE_NAME_MAX_LENGTH - 1
if (deviceName.getBytes(StandardCharsets.UTF_8).length >= DEVICE_NAME_MAX_LENGTH) {
throw new DeviceCreationException(
"Input device name exceeds maximum length of " + DEVICE_NAME_MAX_LENGTH
+ "bytes: " + deviceName);
}
synchronized (mLock) {
for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
if (mInputDeviceDescriptors.valueAt(i).mName.equals(deviceName)) {
throw new DeviceCreationException(
"Input device name already in use: " + deviceName);
}
}
}
}
private static String createPhys(@PhysType String type) {
return String.format("virtual%s:%d", type, sNextPhysId.getAndIncrement());
}
private void setUniqueIdAssociation(int displayId, String phys) {
final String displayUniqueId = mDisplayManagerInternal.getDisplayInfo(displayId).uniqueId;
InputManagerGlobal.getInstance().addUniqueIdAssociation(phys, displayUniqueId);
}
boolean sendDpadKeyEvent(@NonNull IBinder token, @NonNull VirtualKeyEvent event) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not send key event to input device for given token");
}
return mNativeWrapper.writeDpadKeyEvent(inputDeviceDescriptor.getNativePointer(),
event.getKeyCode(), event.getAction(), event.getEventTimeNanos());
}
}
boolean sendKeyEvent(@NonNull IBinder token, @NonNull VirtualKeyEvent event) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not send key event to input device for given token");
}
return mNativeWrapper.writeKeyEvent(inputDeviceDescriptor.getNativePointer(),
event.getKeyCode(), event.getAction(), event.getEventTimeNanos());
}
}
boolean sendButtonEvent(@NonNull IBinder token, @NonNull VirtualMouseButtonEvent event) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not send button event to input device for given token");
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
throw new IllegalStateException(
"Display id associated with this mouse is not currently targetable");
}
return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(),
event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
}
}
boolean sendTouchEvent(@NonNull IBinder token, @NonNull VirtualTouchEvent event) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not send touch event to input device for given token");
}
return mNativeWrapper.writeTouchEvent(inputDeviceDescriptor.getNativePointer(),
event.getPointerId(), event.getToolType(), event.getAction(), event.getX(),
event.getY(), event.getPressure(), event.getMajorAxisSize(),
event.getEventTimeNanos());
}
}
boolean sendRelativeEvent(@NonNull IBinder token, @NonNull VirtualMouseRelativeEvent event) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not send relative event to input device for given token");
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
throw new IllegalStateException(
"Display id associated with this mouse is not currently targetable");
}
return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(),
event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos());
}
}
boolean sendScrollEvent(@NonNull IBinder token, @NonNull VirtualMouseScrollEvent event) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not send scroll event to input device for given token");
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
throw new IllegalStateException(
"Display id associated with this mouse is not currently targetable");
}
return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(),
event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos());
}
}
public PointF getCursorPosition(@NonNull IBinder token) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
throw new IllegalArgumentException(
"Could not get cursor position for input device for given token");
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
throw new IllegalStateException(
"Display id associated with this mouse is not currently targetable");
}
return LocalServices.getService(InputManagerInternal.class).getCursorPosition();
}
}
public void dump(@NonNull PrintWriter fout) {
fout.println(" InputController: ");
synchronized (mLock) {
fout.println(" Active descriptors: ");
for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.valueAt(i);
fout.println(" ptr: " + inputDeviceDescriptor.getNativePointer());
fout.println(" displayId: " + inputDeviceDescriptor.getDisplayId());
fout.println(" creationOrder: "
+ inputDeviceDescriptor.getCreationOrderNumber());
fout.println(" type: " + inputDeviceDescriptor.getType());
fout.println(" phys: " + inputDeviceDescriptor.getPhys());
fout.println(
" inputDeviceId: " + inputDeviceDescriptor.getInputDeviceId());
}
}
}
@VisibleForTesting
void addDeviceForTesting(IBinder deviceToken, long ptr, int type, int displayId, String phys,
String deviceName, int inputDeviceId) {
synchronized (mLock) {
mInputDeviceDescriptors.put(deviceToken, new InputDeviceDescriptor(ptr, () -> {
}, type, displayId, phys, deviceName, inputDeviceId));
}
}
@VisibleForTesting
Map<IBinder, InputDeviceDescriptor> getInputDeviceDescriptors() {
final Map<IBinder, InputDeviceDescriptor> inputDeviceDescriptors = new ArrayMap<>();
synchronized (mLock) {
inputDeviceDescriptors.putAll(mInputDeviceDescriptors);
}
return inputDeviceDescriptors;
}
private static native long nativeOpenUinputDpad(String deviceName, int vendorId, int productId,
String phys);
private static native long nativeOpenUinputKeyboard(String deviceName, int vendorId,
int productId, String phys);
private static native long nativeOpenUinputMouse(String deviceName, int vendorId, int productId,
String phys);
private static native long nativeOpenUinputTouchscreen(String deviceName, int vendorId,
int productId, String phys, int height, int width);
private static native void nativeCloseUinput(long ptr);
private static native boolean nativeWriteDpadKeyEvent(long ptr, int androidKeyCode, int action,
long eventTimeNanos);
private static native boolean nativeWriteKeyEvent(long ptr, int androidKeyCode, int action,
long eventTimeNanos);
private static native boolean nativeWriteButtonEvent(long ptr, int buttonCode, int action,
long eventTimeNanos);
private static native boolean nativeWriteTouchEvent(long ptr, int pointerId, int toolType,
int action, float locationX, float locationY, float pressure, float majorAxisSize,
long eventTimeNanos);
private static native boolean nativeWriteRelativeEvent(long ptr, float relativeX,
float relativeY, long eventTimeNanos);
private static native boolean nativeWriteScrollEvent(long ptr, float xAxisMovement,
float yAxisMovement, long eventTimeNanos);
/** Wrapper around the static native methods for tests. */
@VisibleForTesting
protected static class NativeWrapper {
public long openUinputDpad(String deviceName, int vendorId, int productId, String phys) {
return nativeOpenUinputDpad(deviceName, vendorId, productId, phys);
}
public long openUinputKeyboard(String deviceName, int vendorId, int productId,
String phys) {
return nativeOpenUinputKeyboard(deviceName, vendorId, productId, phys);
}
public long openUinputMouse(String deviceName, int vendorId, int productId, String phys) {
return nativeOpenUinputMouse(deviceName, vendorId, productId, phys);
}
public long openUinputTouchscreen(String deviceName, int vendorId,
int productId, String phys, int height, int width) {
return nativeOpenUinputTouchscreen(deviceName, vendorId, productId, phys, height,
width);
}
public void closeUinput(long ptr) {
nativeCloseUinput(ptr);
}
public boolean writeDpadKeyEvent(long ptr, int androidKeyCode, int action,
long eventTimeNanos) {
return nativeWriteDpadKeyEvent(ptr, androidKeyCode, action, eventTimeNanos);
}
public boolean writeKeyEvent(long ptr, int androidKeyCode, int action,
long eventTimeNanos) {
return nativeWriteKeyEvent(ptr, androidKeyCode, action, eventTimeNanos);
}
public boolean writeButtonEvent(long ptr, int buttonCode, int action,
long eventTimeNanos) {
return nativeWriteButtonEvent(ptr, buttonCode, action, eventTimeNanos);
}
public boolean writeTouchEvent(long ptr, int pointerId, int toolType, int action,
float locationX, float locationY, float pressure, float majorAxisSize,
long eventTimeNanos) {
return nativeWriteTouchEvent(ptr, pointerId, toolType,
action, locationX, locationY,
pressure, majorAxisSize, eventTimeNanos);
}
public boolean writeRelativeEvent(long ptr, float relativeX, float relativeY,
long eventTimeNanos) {
return nativeWriteRelativeEvent(ptr, relativeX, relativeY, eventTimeNanos);
}
public boolean writeScrollEvent(long ptr, float xAxisMovement, float yAxisMovement,
long eventTimeNanos) {
return nativeWriteScrollEvent(ptr, xAxisMovement, yAxisMovement, eventTimeNanos);
}
}
@VisibleForTesting static final class InputDeviceDescriptor {
static final int TYPE_KEYBOARD = 1;
static final int TYPE_MOUSE = 2;
static final int TYPE_TOUCHSCREEN = 3;
static final int TYPE_DPAD = 4;
static final int TYPE_NAVIGATION_TOUCHPAD = 5;
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_KEYBOARD,
TYPE_MOUSE,
TYPE_TOUCHSCREEN,
TYPE_DPAD,
TYPE_NAVIGATION_TOUCHPAD,
})
@Retention(RetentionPolicy.SOURCE)
@interface Type {
}
private static final AtomicLong sNextCreationOrderNumber = new AtomicLong(1);
// Pointer to the native input device object.
private final long mPtr;
private final IBinder.DeathRecipient mDeathRecipient;
private final @Type int mType;
private final int mDisplayId;
private final String mPhys;
// The name given to this device by the client. Enforced to be unique within
// InputController.
private final String mName;
// The input device id that was associated to the device by the InputReader on device
// creation.
private final int mInputDeviceId;
// Monotonically increasing number; devices with lower numbers were created earlier.
private final long mCreationOrderNumber;
InputDeviceDescriptor(long ptr, IBinder.DeathRecipient deathRecipient, @Type int type,
int displayId, String phys, String name, int inputDeviceId) {
mPtr = ptr;
mDeathRecipient = deathRecipient;
mType = type;
mDisplayId = displayId;
mPhys = phys;
mName = name;
mInputDeviceId = inputDeviceId;
mCreationOrderNumber = sNextCreationOrderNumber.getAndIncrement();
}
public long getNativePointer() {
return mPtr;
}
public int getType() {
return mType;
}
public boolean isMouse() {
return mType == TYPE_MOUSE;
}
public IBinder.DeathRecipient getDeathRecipient() {
return mDeathRecipient;
}
public int getDisplayId() {
return mDisplayId;
}
public long getCreationOrderNumber() {
return mCreationOrderNumber;
}
public String getPhys() {
return mPhys;
}
public int getInputDeviceId() {
return mInputDeviceId;
}
}
private final class BinderDeathRecipient implements IBinder.DeathRecipient {
private final IBinder mDeviceToken;
BinderDeathRecipient(IBinder deviceToken) {
mDeviceToken = deviceToken;
}
@Override
public void binderDied() {
// All callers are expected to call {@link VirtualDevice#unregisterInputDevice} before
// quitting, which removes this death recipient. If this is invoked, the remote end
// died, or they disposed of the object without properly unregistering.
Slog.e(TAG, "Virtual input controller binder died");
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;
private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
WaitForDevice(String deviceName, int vendorId, int productId) {
mListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
final InputDevice device = InputManagerGlobal.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;
}
mInputDeviceId = deviceId;
mDeviceAddedLatch.countDown();
}
@Override
public void onInputDeviceRemoved(int deviceId) {
}
@Override
public void onInputDeviceChanged(int deviceId) {
}
};
InputManagerGlobal.getInstance().registerInputDeviceListener(mListener, mHandler);
}
/**
* Note: This must not be called from {@link #mHandler}'s thread.
* @throws DeviceCreationException if the device was not created successfully within the
* timeout.
* @return The id of the created input device.
*/
int 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);
}
if (mInputDeviceId == IInputConstants.INVALID_INPUT_DEVICE_ID) {
throw new IllegalStateException(
"Virtual input device was created with an invalid "
+ "id=" + mInputDeviceId);
}
return mInputDeviceId;
}
@Override
public void close() {
InputManagerGlobal.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<Long> 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.");
}
validateDeviceName(deviceName);
final long ptr;
final BinderDeathRecipient binderDeathRecipient;
final int inputDeviceId;
setUniqueIdAssociation(displayId, phys);
try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
ptr = deviceOpener.get();
// See INVALID_PTR in libs/input/VirtualInputDevice.cpp.
if (ptr == 0) {
throw new DeviceCreationException(
"A native error occurred when creating virtual input device: "
+ deviceName);
}
// The pointer to the native input device is valid from here, so ensure that all
// failures close the device after this point.
try {
inputDeviceId = 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(ptr);
throw e;
}
} catch (DeviceCreationException e) {
InputManagerGlobal.getInstance().removeUniqueIdAssociation(phys);
throw e;
}
synchronized (mLock) {
mInputDeviceDescriptors.put(deviceToken,
new InputDeviceDescriptor(ptr, binderDeathRecipient, type, displayId, phys,
deviceName, inputDeviceId));
}
}
@VisibleForTesting
interface DeviceCreationThreadVerifier {
/** Returns true if the calling thread is a valid thread for device creation. */
boolean isValidThread();
}
}