Merge "Virtual stylus API" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index bd4ecf2..c7e314c 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3229,6 +3229,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
+    method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method public int getDeviceId();
@@ -5304,6 +5305,78 @@
     method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build();
   }
 
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public int getButtonCode();
+    method public long getEventTimeNanos();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_BUTTON_PRESS = 11; // 0xb
+    field public static final int ACTION_BUTTON_RELEASE = 12; // 0xc
+    field public static final int BUTTON_PRIMARY = 32; // 0x20
+    field public static final int BUTTON_SECONDARY = 64; // 0x40
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusButtonEvent> CREATOR;
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusButtonEvent.Builder {
+    ctor public VirtualStylusButtonEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent build();
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setButtonCode(int);
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setEventTimeNanos(long);
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getHeight();
+    method public int getWidth();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusConfig> CREATOR;
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualStylusConfig.Builder> {
+    ctor public VirtualStylusConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
+    method @NonNull public android.hardware.input.VirtualStylusConfig build();
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusMotionEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public long getEventTimeNanos();
+    method public int getPressure();
+    method public int getTiltX();
+    method public int getTiltY();
+    method public int getToolType();
+    method public int getX();
+    method public int getY();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_DOWN = 0; // 0x0
+    field public static final int ACTION_MOVE = 2; // 0x2
+    field public static final int ACTION_UP = 1; // 0x1
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusMotionEvent> CREATOR;
+    field public static final int TOOL_TYPE_ERASER = 4; // 0x4
+    field public static final int TOOL_TYPE_STYLUS = 2; // 0x2
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusMotionEvent.Builder {
+    ctor public VirtualStylusMotionEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent build();
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setEventTimeNanos(long);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setPressure(@IntRange(from=0x0, to=0xff) int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltX(@IntRange(from=0xffffffa6, to=0x5a) int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltY(@IntRange(from=0xffffffa6, to=0x5a) int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setToolType(int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setX(int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setY(int);
+  }
+
   public final class VirtualTouchEvent implements android.os.Parcelable {
     method public int describeContents();
     method public int getAction();
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 12229b1..6eab363 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -35,6 +35,9 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusConfig;
+import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
@@ -144,6 +147,12 @@
     void createVirtualNavigationTouchpad(in VirtualNavigationTouchpadConfig config, IBinder token);
 
     /**
+     * Creates a new stylus and registers it with the input framework with the given token.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void createVirtualStylus(in VirtualStylusConfig config, IBinder token);
+
+    /**
      * Removes the input device corresponding to the given token from the framework.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
@@ -156,32 +165,32 @@
     int getInputDeviceId(IBinder token);
 
     /**
-    * Injects a key event to the virtual dpad corresponding to the given token.
-    */
+     * Injects a key event to the virtual dpad corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
 
     /**
-    * Injects a key event to the virtual keyboard corresponding to the given token.
-    */
+     * Injects a key event to the virtual keyboard corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
 
     /**
-    * Injects a button event to the virtual mouse corresponding to the given token.
-    */
+     * Injects a button event to the virtual mouse corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
 
     /**
-    * Injects a relative event to the virtual mouse corresponding to the given token.
-    */
+     * Injects a relative event to the virtual mouse corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
 
     /**
-    * Injects a scroll event to the virtual mouse corresponding to the given token.
-    */
+     * Injects a scroll event to the virtual mouse corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
 
@@ -192,6 +201,18 @@
     boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
 
     /**
+     * Injects a motion event from the virtual stylus input device corresponding to the given token.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    boolean sendStylusMotionEvent(IBinder token, in VirtualStylusMotionEvent event);
+
+    /**
+     * Injects a button event from the virtual stylus input device corresponding to the given token.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    boolean sendStylusButtonEvent(IBinder token, in VirtualStylusButtonEvent event);
+
+    /**
      * Returns all virtual sensors created for this device.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 2abeeee..c1e443d 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -42,6 +42,8 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualNavigationTouchpad;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylus;
+import android.hardware.input.VirtualStylusConfig;
 import android.hardware.input.VirtualTouchscreen;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.media.AudioManager;
@@ -316,6 +318,19 @@
         }
     }
 
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    @NonNull
+    VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) {
+        try {
+            final IBinder token = new Binder(
+                    "android.hardware.input.VirtualStylus:" + config.getInputDeviceName());
+            mVirtualDevice.createVirtualStylus(config, token);
+            return new VirtualStylus(config, mVirtualDevice, token);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @NonNull
     VirtualNavigationTouchpad createVirtualNavigationTouchpad(
             @NonNull VirtualNavigationTouchpadConfig config) {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index eef60f1..10b652a 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -55,6 +55,8 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualNavigationTouchpad;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylus;
+import android.hardware.input.VirtualStylusConfig;
 import android.hardware.input.VirtualTouchscreen;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.media.AudioManager;
@@ -859,6 +861,19 @@
         }
 
         /**
+         * Creates a virtual stylus.
+         *
+         * @param config the touchscreen configurations for the virtual stylus.
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+        public VirtualStylus createVirtualStylus(
+                @NonNull VirtualStylusConfig config) {
+            return mVirtualDeviceInternal.createVirtualStylus(config);
+        }
+
+        /**
          * Creates a VirtualAudioDevice, capable of recording audio emanating from this device,
          * or injecting audio from another device.
          *
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index ce2490b..588e4fc 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -100,3 +100,10 @@
   description: "Enable interactive screen mirroring using Virtual Devices"
   bug: "292212199"
 }
+
+flag {
+  name: "virtual_stylus"
+  namespace: "virtual_devices"
+  description: "Enable virtual stylus input"
+  bug: "304829446"
+}
diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java
new file mode 100644
index 0000000..c763f740
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylus.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.flags.Flags;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A virtual stylus which can be used to inject input into the framework that represents a stylus
+ * on a remote device.
+ *
+ * This registers an {@link android.view.InputDevice} that is interpreted like a
+ * physically-connected device and dispatches received events to it.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public class VirtualStylus extends VirtualInputDevice {
+    /** @hide */
+    public VirtualStylus(VirtualStylusConfig config, IVirtualDevice virtualDevice,
+            IBinder token) {
+        super(config, virtualDevice, token);
+    }
+
+    /**
+     * Sends a motion event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendMotionEvent(@NonNull VirtualStylusMotionEvent event) {
+        try {
+            if (!mVirtualDevice.sendStylusMotionEvent(mToken, event)) {
+                Log.w(TAG, "Failed to send motion event from virtual stylus "
+                        + mConfig.getInputDeviceName());
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sends a button event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendButtonEvent(@NonNull VirtualStylusButtonEvent event) {
+        try {
+            if (!mVirtualDevice.sendStylusButtonEvent(mToken, event)) {
+                Log.w(TAG, "Failed to send button event from virtual stylus "
+                        + mConfig.getInputDeviceName());
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
new file mode 100644
index 0000000..7de32cc
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+parcelable VirtualStylusButtonEvent;
diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.java b/core/java/android/hardware/input/VirtualStylusButtonEvent.java
new file mode 100644
index 0000000..97a4cd0
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a stylus button click interaction originating from a remote device.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusButtonEvent implements Parcelable {
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /** Action indicating the stylus button has been pressed. */
+    public static final int ACTION_BUTTON_PRESS = MotionEvent.ACTION_BUTTON_PRESS;
+    /** Action indicating the stylus button has been released. */
+    public static final int ACTION_BUTTON_RELEASE = MotionEvent.ACTION_BUTTON_RELEASE;
+    /** @hide */
+    @IntDef(prefix = {"ACTION_"}, value = {
+            ACTION_UNKNOWN,
+            ACTION_BUTTON_PRESS,
+            ACTION_BUTTON_RELEASE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    /** @hide */
+    public static final int BUTTON_UNKNOWN = -1;
+    /** Action indicating the stylus button involved in this event is primary. */
+    public static final int BUTTON_PRIMARY = MotionEvent.BUTTON_STYLUS_PRIMARY;
+    /** Action indicating the stylus button involved in this event is secondary. */
+    public static final int BUTTON_SECONDARY = MotionEvent.BUTTON_STYLUS_SECONDARY;
+    /** @hide */
+    @IntDef(prefix = {"BUTTON_"}, value = {
+            BUTTON_UNKNOWN,
+            BUTTON_PRIMARY,
+            BUTTON_SECONDARY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Button {}
+
+    @Action
+    private final int mAction;
+    @Button
+    private final int mButtonCode;
+    private final long mEventTimeNanos;
+
+    private VirtualStylusButtonEvent(@Action int action, @Button int buttonCode,
+            long eventTimeNanos) {
+        mAction = action;
+        mButtonCode = buttonCode;
+        mEventTimeNanos = eventTimeNanos;
+    }
+
+    private VirtualStylusButtonEvent(@NonNull Parcel parcel) {
+        mAction = parcel.readInt();
+        mButtonCode = parcel.readInt();
+        mEventTimeNanos = parcel.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeInt(mAction);
+        parcel.writeInt(mButtonCode);
+        parcel.writeLong(mEventTimeNanos);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the button code associated with this event.
+     */
+    @Button
+    public int getButtonCode() {
+        return mButtonCode;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    @Action
+    public int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
+     * with nanosecond (instead of millisecond) precision.
+     *
+     * @see InputEvent#getEventTime()
+     */
+    public long getEventTimeNanos() {
+        return mEventTimeNanos;
+    }
+
+    /**
+     * Builder for {@link VirtualStylusButtonEvent}.
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+    public static final class Builder {
+
+        @Action
+        private int mAction = ACTION_UNKNOWN;
+        @Button
+        private int mButtonCode = BUTTON_UNKNOWN;
+        private long mEventTimeNanos = 0L;
+
+        /**
+         * Creates a {@link VirtualStylusButtonEvent} object with the current builder configuration.
+         */
+        @NonNull
+        public VirtualStylusButtonEvent build() {
+            if (mAction == ACTION_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus button event with unset action");
+            }
+            if (mButtonCode == BUTTON_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus button event with unset button code");
+            }
+            return new VirtualStylusButtonEvent(mAction, mButtonCode, mEventTimeNanos);
+        }
+
+        /**
+         * Sets the button code of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setButtonCode(@Button int buttonCode) {
+            if (buttonCode != BUTTON_PRIMARY && buttonCode != BUTTON_SECONDARY) {
+                throw new IllegalArgumentException(
+                        "Unsupported stylus button code : " + buttonCode);
+            }
+            mButtonCode = buttonCode;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setAction(@Action int action) {
+            if (action != ACTION_BUTTON_PRESS && action != ACTION_BUTTON_RELEASE) {
+                throw new IllegalArgumentException("Unsupported stylus button action : " + action);
+            }
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the time (in nanoseconds) when this specific event was generated. This may be
+         * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
+         * millisecond), but can be different depending on the use case.
+         * This field is optional and can be omitted.
+         *
+         * @return this builder, to allow for chaining of calls
+         * @see InputEvent#getEventTime()
+         */
+        @NonNull
+        public Builder setEventTimeNanos(long eventTimeNanos) {
+            if (eventTimeNanos < 0L) {
+                throw new IllegalArgumentException("Event time cannot be negative");
+            }
+            this.mEventTimeNanos = eventTimeNanos;
+            return this;
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<VirtualStylusButtonEvent> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualStylusButtonEvent createFromParcel(Parcel source) {
+                    return new VirtualStylusButtonEvent(source);
+                }
+
+                public VirtualStylusButtonEvent[] newArray(int size) {
+                    return new VirtualStylusButtonEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/input/VirtualStylusConfig.aidl b/core/java/android/hardware/input/VirtualStylusConfig.aidl
new file mode 100644
index 0000000..a13eec2
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+parcelable VirtualStylusConfig;
diff --git a/core/java/android/hardware/input/VirtualStylusConfig.java b/core/java/android/hardware/input/VirtualStylusConfig.java
new file mode 100644
index 0000000..64cf1f5
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create a virtual stylus.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusConfig extends VirtualTouchDeviceConfig implements Parcelable {
+
+    private VirtualStylusConfig(@NonNull Builder builder) {
+        super(builder);
+    }
+
+    private VirtualStylusConfig(@NonNull Parcel in) {
+        super(in);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+    }
+
+    @NonNull
+    public static final Creator<VirtualStylusConfig> CREATOR =
+            new Creator<>() {
+                @Override
+                public VirtualStylusConfig createFromParcel(Parcel in) {
+                    return new VirtualStylusConfig(in);
+                }
+
+                @Override
+                public VirtualStylusConfig[] newArray(int size) {
+                    return new VirtualStylusConfig[size];
+                }
+            };
+
+    /**
+     * Builder for creating a {@link VirtualStylusConfig}.
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+    public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> {
+
+        /**
+         * Creates a new instance for the given dimensions of the screen targeted by the
+         * {@link VirtualStylus}.
+         *
+         * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do
+         * not necessarily have to correspond to the display size or aspect ratio. In this case the
+         * framework will handle the scaling appropriately.
+         *
+         * @param screenWidth The width of the targeted screen.
+         * @param screenHeight The height of the targeted screen.
+         */
+        public Builder(@IntRange(from = 1) int screenWidth,
+                @IntRange(from = 1) int screenHeight) {
+            super(screenWidth, screenHeight);
+        }
+
+        /**
+         * Builds the {@link VirtualStylusConfig} instance.
+         */
+        @NonNull
+        public VirtualStylusConfig build() {
+            return new VirtualStylusConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
new file mode 100644
index 0000000..42d14ab
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+parcelable VirtualStylusMotionEvent;
diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.java b/core/java/android/hardware/input/VirtualStylusMotionEvent.java
new file mode 100644
index 0000000..2ab76ae
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a stylus interaction originating from a remote device.
+ *
+ * The tool type, location and action are required; tilts and pressure are optional.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusMotionEvent implements Parcelable {
+    private static final int TILT_MIN = -90;
+    private static final int TILT_MAX = 90;
+    private static final int PRESSURE_MIN = 0;
+    private static final int PRESSURE_MAX = 255;
+
+    /** @hide */
+    public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN;
+    /** Tool type indicating that a stylus is the origin of the event. */
+    public static final int TOOL_TYPE_STYLUS = MotionEvent.TOOL_TYPE_STYLUS;
+    /** Tool type indicating that an eraser is the origin of the event. */
+    public static final int TOOL_TYPE_ERASER = MotionEvent.TOOL_TYPE_ERASER;
+    /** @hide */
+    @IntDef(prefix = { "TOOL_TYPE_" }, value = {
+            TOOL_TYPE_UNKNOWN,
+            TOOL_TYPE_STYLUS,
+            TOOL_TYPE_ERASER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ToolType {}
+
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /**
+     * Action indicating the stylus has been pressed down to the screen. ACTION_DOWN with pressure
+     * {@code 0} indicates that the stylus is hovering over the screen, and non-zero pressure
+     * indicates that the stylus is touching the screen.
+     */
+    public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN;
+    /** Action indicating the stylus has been lifted from the screen. */
+    public static final int ACTION_UP = MotionEvent.ACTION_UP;
+    /** Action indicating the stylus has been moved along the screen. */
+    public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE;
+    /** @hide */
+    @IntDef(prefix = { "ACTION_" }, value = {
+            ACTION_UNKNOWN,
+            ACTION_DOWN,
+            ACTION_UP,
+            ACTION_MOVE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    @ToolType
+    private final int mToolType;
+    @Action
+    private final int mAction;
+    private final int mX;
+    private final int mY;
+    private final int mPressure;
+    private final int mTiltX;
+    private final int mTiltY;
+    private final long mEventTimeNanos;
+
+    private VirtualStylusMotionEvent(@ToolType int toolType, @Action int action, int x, int y,
+            int pressure, int tiltX, int tiltY, long eventTimeNanos) {
+        mToolType = toolType;
+        mAction = action;
+        mX = x;
+        mY = y;
+        mPressure = pressure;
+        mTiltX = tiltX;
+        mTiltY = tiltY;
+        mEventTimeNanos = eventTimeNanos;
+    }
+
+    private VirtualStylusMotionEvent(@NonNull Parcel parcel) {
+        mToolType = parcel.readInt();
+        mAction = parcel.readInt();
+        mX = parcel.readInt();
+        mY = parcel.readInt();
+        mPressure = parcel.readInt();
+        mTiltX = parcel.readInt();
+        mTiltY = parcel.readInt();
+        mEventTimeNanos = parcel.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mToolType);
+        dest.writeInt(mAction);
+        dest.writeInt(mX);
+        dest.writeInt(mY);
+        dest.writeInt(mPressure);
+        dest.writeInt(mTiltX);
+        dest.writeInt(mTiltY);
+        dest.writeLong(mEventTimeNanos);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the tool type associated with this event.
+     */
+    @ToolType
+    public int getToolType() {
+        return mToolType;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    @Action
+    public int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Returns the x-axis location associated with this event.
+     */
+    public int getX() {
+        return mX;
+    }
+
+    /**
+     * Returns the y-axis location associated with this event.
+     */
+    public int getY() {
+        return mY;
+    }
+
+    /**
+     * Returns the pressure associated with this event. {@code 0} pressure indicates that the stylus
+     * is hovering, otherwise the stylus is touching the screen. Returns {@code 255} if omitted.
+     */
+    public int getPressure() {
+        return mPressure;
+    }
+
+    /**
+     * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the
+     * y-z plane and the plane containing both the stylus axis and the y axis. A positive tiltX is
+     * to the right, in the direction of increasing x values. {@code 0} tilt indicates that the
+     * stylus is perpendicular to the x-axis. Returns {@code 0} if omitted.
+     *
+     * @see Builder#setTiltX
+     */
+    public int getTiltX() {
+        return mTiltX;
+    }
+
+    /**
+     * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the
+     * x-z plane and the plane containing both the stylus axis and the x axis. A positive tiltY is
+     * towards the user, in the direction of increasing y values. {@code 0} tilt indicates that the
+     * stylus is perpendicular to the y-axis. Returns {@code 0} if omitted.
+     *
+     * @see Builder#setTiltY
+     */
+    public int getTiltY() {
+        return mTiltY;
+    }
+
+    /**
+     * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
+     * with nanosecond (instead of millisecond) precision.
+     *
+     * @see InputEvent#getEventTime()
+     */
+    public long getEventTimeNanos() {
+        return mEventTimeNanos;
+    }
+
+    /**
+     * Builder for {@link VirtualStylusMotionEvent}.
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+    public static final class Builder {
+
+        @ToolType
+        private int mToolType = TOOL_TYPE_UNKNOWN;
+        @Action
+        private int mAction = ACTION_UNKNOWN;
+        private int mX = 0;
+        private int mY = 0;
+        private boolean mIsXSet = false;
+        private boolean mIsYSet = false;
+        private int mPressure = PRESSURE_MAX;
+        private int mTiltX = 0;
+        private int mTiltY = 0;
+        private long mEventTimeNanos = 0L;
+
+        /**
+         * Creates a {@link VirtualStylusMotionEvent} object with the current builder configuration.
+         *
+         * @throws IllegalArgumentException if one of the required arguments (action, tool type,
+         * x-axis location and y-axis location) is missing.
+         * {@link VirtualStylusMotionEvent} for a detailed explanation.
+         */
+        @NonNull
+        public VirtualStylusMotionEvent build() {
+            if (mToolType == TOOL_TYPE_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset tool type");
+            }
+            if (mAction == ACTION_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset action");
+            }
+            if (!mIsXSet) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset x-axis location");
+            }
+            if (!mIsYSet) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset y-axis location");
+            }
+            return new VirtualStylusMotionEvent(mToolType, mAction, mX, mY, mPressure, mTiltX,
+                    mTiltY, mEventTimeNanos);
+        }
+
+        /**
+         * Sets the tool type of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setToolType(@ToolType int toolType) {
+            if (toolType != TOOL_TYPE_STYLUS && toolType != TOOL_TYPE_ERASER) {
+                throw new IllegalArgumentException("Unsupported stylus tool type: " + toolType);
+            }
+            mToolType = toolType;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setAction(@Action int action) {
+            if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE) {
+                throw new IllegalArgumentException("Unsupported stylus action : " + action);
+            }
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the x-axis location of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setX(int absX) {
+            mX = absX;
+            mIsXSet = true;
+            return this;
+        }
+
+        /**
+         * Sets the y-axis location of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setY(int absY) {
+            mY = absY;
+            mIsYSet = true;
+            return this;
+        }
+
+        /**
+         * Sets the pressure of the event. {@code 0} pressure indicates that the stylus is hovering,
+         * otherwise the stylus is touching the screen. This field is optional and can be omitted
+         * (defaults to {@code 255}).
+         *
+         * @param pressure The pressure of the stylus.
+         *
+         * @throws IllegalArgumentException if the pressure is smaller than 0 or greater than 255.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setPressure(
+                @IntRange(from = PRESSURE_MIN, to = PRESSURE_MAX) int pressure) {
+            if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) {
+                throw new IllegalArgumentException(
+                        "Pressure should be between " + PRESSURE_MIN + " and " + PRESSURE_MAX);
+            }
+            mPressure = pressure;
+            return this;
+        }
+
+        /**
+         * Sets the x-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is
+         * perpendicular to the x-axis. This field is optional and can be omitted (defaults to
+         * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation
+         * of the stylus, given by {@link MotionEvent#AXIS_TILT} and
+         * {@link MotionEvent#AXIS_ORIENTATION} respectively.
+         *
+         * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90.
+         *
+         * @return this builder, to allow for chaining of calls
+         *
+         * @see VirtualStylusMotionEvent#getTiltX
+         * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields">
+         *     Stylus tilt and orientation</a>
+         */
+        @NonNull
+        public Builder setTiltX(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltX) {
+            validateTilt(tiltX);
+            mTiltX = tiltX;
+            return this;
+        }
+
+        /**
+         * Sets the y-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is
+         * perpendicular to the y-axis. This field is optional and can be omitted (defaults to
+         * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation
+         * of the stylus, given by {@link MotionEvent#AXIS_TILT} and
+         * {@link MotionEvent#AXIS_ORIENTATION} respectively.
+         *
+         * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90.
+         *
+         * @return this builder, to allow for chaining of calls
+         *
+         * @see VirtualStylusMotionEvent#getTiltY
+         * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields">
+         *     Stylus tilt and orientation</a>
+         */
+        @NonNull
+        public Builder setTiltY(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltY) {
+            validateTilt(tiltY);
+            mTiltY = tiltY;
+            return this;
+        }
+
+        /**
+         * Sets the time (in nanoseconds) when this specific event was generated. This may be
+         * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
+         * millisecond), but can be different depending on the use case.
+         * This field is optional and can be omitted.
+         *
+         * @return this builder, to allow for chaining of calls
+         * @see InputEvent#getEventTime()
+         */
+        @NonNull
+        public Builder setEventTimeNanos(long eventTimeNanos) {
+            if (eventTimeNanos < 0L) {
+                throw new IllegalArgumentException("Event time cannot be negative");
+            }
+            mEventTimeNanos = eventTimeNanos;
+            return this;
+        }
+
+        private void validateTilt(int tilt) {
+            if (tilt < TILT_MIN || tilt > TILT_MAX) {
+                throw new IllegalArgumentException(
+                        "Tilt must be between " + TILT_MIN + " and " + TILT_MAX);
+            }
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<VirtualStylusMotionEvent> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualStylusMotionEvent createFromParcel(Parcel source) {
+                    return new VirtualStylusMotionEvent(source);
+                }
+                public VirtualStylusMotionEvent[] newArray(int size) {
+                    return new VirtualStylusMotionEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/input/VirtualTouchDeviceConfig.java b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java
new file mode 100644
index 0000000..2e2cfab
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 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 android.hardware.input;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.Parcel;
+
+/**
+ * Configurations to create a virtual touch-based device.
+ *
+ * @hide
+ */
+abstract class VirtualTouchDeviceConfig extends VirtualInputDeviceConfig {
+
+    /** The touch device width. */
+    private final int mWidth;
+    /** The touch device height. */
+    private final int mHeight;
+
+    VirtualTouchDeviceConfig(@NonNull Builder<? extends Builder<?>> builder) {
+        super(builder);
+        mWidth = builder.mWidth;
+        mHeight = builder.mHeight;
+    }
+
+    VirtualTouchDeviceConfig(@NonNull Parcel in) {
+        super(in);
+        mWidth = in.readInt();
+        mHeight = in.readInt();
+    }
+
+    /** Returns the touch device width. */
+    public int getWidth() {
+        return mWidth;
+    }
+
+    /** Returns the touch device height. */
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mWidth);
+        dest.writeInt(mHeight);
+    }
+
+    @Override
+    @NonNull
+    String additionalFieldsToString() {
+        return " width=" + mWidth + " height=" + mHeight;
+    }
+
+    /**
+     * Builder for creating a {@link VirtualTouchDeviceConfig}.
+     *
+     * @param <T> The subclass to be built.
+     */
+    abstract static class Builder<T extends Builder<T>>
+            extends VirtualInputDeviceConfig.Builder<T> {
+
+        private final int mWidth;
+        private final int mHeight;
+
+        /**
+         * Creates a new instance for the given dimensions of the touch device.
+         *
+         * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do
+         * not necessarily have to correspond to the display size or aspect ratio. In this case the
+         * framework will handle the scaling appropriately.
+         *
+         * @param touchDeviceWidth The width of the touch device.
+         * @param touchDeviceHeight The height of the touch device.
+         */
+        Builder(@IntRange(from = 1) int touchDeviceWidth,
+                @IntRange(from = 1) int touchDeviceHeight) {
+            if (touchDeviceHeight <= 0 || touchDeviceWidth <= 0) {
+                throw new IllegalArgumentException(
+                        "Cannot create a virtual touch-based device, dimensions must be "
+                                + "positive. Got: (" + touchDeviceHeight + ", "
+                                + touchDeviceWidth + ")");
+            }
+            mHeight = touchDeviceHeight;
+            mWidth = touchDeviceWidth;
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index 6308459..851cee6 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -23,38 +23,19 @@
 import android.os.Parcelable;
 
 /**
- * Configurations to create virtual touchscreen.
+ * Configurations to create a virtual touchscreen.
  *
  * @hide
  */
 @SystemApi
-public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
-
-    /** The touchscreen width. */
-    private final int mWidth;
-    /** The touchscreen height. */
-    private final int mHeight;
+public final class VirtualTouchscreenConfig extends VirtualTouchDeviceConfig implements Parcelable {
 
     private VirtualTouchscreenConfig(@NonNull Builder builder) {
         super(builder);
-        mWidth = builder.mWidth;
-        mHeight = builder.mHeight;
     }
 
     private VirtualTouchscreenConfig(@NonNull Parcel in) {
         super(in);
-        mWidth = in.readInt();
-        mHeight = in.readInt();
-    }
-
-    /** Returns the touchscreen width. */
-    public int getWidth() {
-        return mWidth;
-    }
-
-    /** Returns the touchscreen height. */
-    public int getHeight() {
-        return mHeight;
     }
 
     @Override
@@ -65,19 +46,11 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
-        dest.writeInt(mWidth);
-        dest.writeInt(mHeight);
-    }
-
-    @Override
-    @NonNull
-    String additionalFieldsToString() {
-        return " width=" + mWidth + " height=" + mHeight;
     }
 
     @NonNull
     public static final Creator<VirtualTouchscreenConfig> CREATOR =
-            new Creator<VirtualTouchscreenConfig>() {
+            new Creator<>() {
                 @Override
                 public VirtualTouchscreenConfig createFromParcel(Parcel in) {
                     return new VirtualTouchscreenConfig(in);
@@ -92,9 +65,7 @@
     /**
      * Builder for creating a {@link VirtualTouchscreenConfig}.
      */
-    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
-        private int mWidth;
-        private int mHeight;
+    public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> {
 
         /**
          * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}.
@@ -108,14 +79,7 @@
          */
         public Builder(@IntRange(from = 1) int touchscreenWidth,
                 @IntRange(from = 1) int touchscreenHeight) {
-            if (touchscreenHeight <= 0 || touchscreenWidth <= 0) {
-                throw new IllegalArgumentException(
-                        "Cannot create a virtual touchscreen, touchscreen dimensions must be "
-                                + "positive. Got: (" + touchscreenHeight + ", "
-                                + touchscreenWidth + ")");
-            }
-            mHeight = touchscreenHeight;
-            mWidth = touchscreenWidth;
+            super(touchscreenWidth, touchscreenHeight);
         }
 
         /**
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 980ec02..3b9d92d 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -30,6 +30,8 @@
 import android.hardware.input.VirtualMouseButtonEvent;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Handler;
 import android.os.IBinder;
@@ -71,12 +73,14 @@
     static final String PHYS_TYPE_MOUSE = "Mouse";
     static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen";
     static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad";
+    static final String PHYS_TYPE_STYLUS = "Stylus";
     @StringDef(prefix = { "PHYS_TYPE_" }, value = {
             PHYS_TYPE_DPAD,
             PHYS_TYPE_KEYBOARD,
             PHYS_TYPE_MOUSE,
             PHYS_TYPE_TOUCHSCREEN,
             PHYS_TYPE_NAVIGATION_TOUCHPAD,
+            PHYS_TYPE_STYLUS,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface PhysType {
@@ -188,6 +192,16 @@
         }
     }
 
+    void createStylus(@NonNull String deviceName, int vendorId, int productId,
+            @NonNull IBinder deviceToken, int displayId, int height, int width)
+            throws DeviceCreationException {
+        final String phys = createPhys(PHYS_TYPE_STYLUS);
+        createDeviceInternal(InputDeviceDescriptor.TYPE_STYLUS, deviceName, vendorId,
+                productId, deviceToken, displayId, phys,
+                () -> mNativeWrapper.openUinputStylus(deviceName, vendorId, productId, phys,
+                        height, width));
+    }
+
     void unregisterInputDevice(@NonNull IBinder token) {
         synchronized (mLock) {
             final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
@@ -410,6 +424,32 @@
         }
     }
 
+    boolean sendStylusMotionEvent(@NonNull IBinder token, @NonNull VirtualStylusMotionEvent event) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
+                    token);
+            if (inputDeviceDescriptor == null) {
+                return false;
+            }
+            return mNativeWrapper.writeStylusMotionEvent(inputDeviceDescriptor.getNativePointer(),
+                    event.getToolType(), event.getAction(), event.getX(), event.getY(),
+                    event.getPressure(), event.getTiltX(), event.getTiltY(),
+                    event.getEventTimeNanos());
+        }
+    }
+
+    boolean sendStylusButtonEvent(@NonNull IBinder token, @NonNull VirtualStylusButtonEvent event) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
+                    token);
+            if (inputDeviceDescriptor == null) {
+                return false;
+            }
+            return mNativeWrapper.writeStylusButtonEvent(inputDeviceDescriptor.getNativePointer(),
+                    event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
+        }
+    }
+
     public void dump(@NonNull PrintWriter fout) {
         fout.println("    InputController: ");
         synchronized (mLock) {
@@ -454,6 +494,8 @@
             String phys);
     private static native long nativeOpenUinputTouchscreen(String deviceName, int vendorId,
             int productId, String phys, int height, int width);
+    private static native long nativeOpenUinputStylus(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);
@@ -468,6 +510,10 @@
             float relativeY, long eventTimeNanos);
     private static native boolean nativeWriteScrollEvent(long ptr, float xAxisMovement,
             float yAxisMovement, long eventTimeNanos);
+    private static native boolean nativeWriteStylusMotionEvent(long ptr, int toolType, int action,
+            int locationX, int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos);
+    private static native boolean nativeWriteStylusButtonEvent(long ptr, int buttonCode, int action,
+            long eventTimeNanos);
 
     /** Wrapper around the static native methods for tests. */
     @VisibleForTesting
@@ -491,6 +537,11 @@
                     width);
         }
 
+        public long openUinputStylus(String deviceName, int vendorId, int productId, String phys,
+                int height, int width) {
+            return nativeOpenUinputStylus(deviceName, vendorId, productId, phys, height, width);
+        }
+
         public void closeUinput(long ptr) {
             nativeCloseUinput(ptr);
         }
@@ -527,6 +578,17 @@
                 long eventTimeNanos) {
             return nativeWriteScrollEvent(ptr, xAxisMovement, yAxisMovement, eventTimeNanos);
         }
+
+        public boolean writeStylusMotionEvent(long ptr, int toolType, int action, int locationX,
+                int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos) {
+            return nativeWriteStylusMotionEvent(ptr, toolType, action, locationX, locationY,
+                    pressure, tiltX, tiltY, eventTimeNanos);
+        }
+
+        public boolean writeStylusButtonEvent(long ptr, int buttonCode, int action,
+                long eventTimeNanos) {
+            return nativeWriteStylusButtonEvent(ptr, buttonCode, action, eventTimeNanos);
+        }
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -537,12 +599,14 @@
         static final int TYPE_TOUCHSCREEN = 3;
         static final int TYPE_DPAD = 4;
         static final int TYPE_NAVIGATION_TOUCHPAD = 5;
+        static final int TYPE_STYLUS = 6;
         @IntDef(prefix = { "TYPE_" }, value = {
                 TYPE_KEYBOARD,
                 TYPE_MOUSE,
                 TYPE_TOUCHSCREEN,
                 TYPE_DPAD,
                 TYPE_NAVIGATION_TOUCHPAD,
+                TYPE_STYLUS,
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface Type {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 50eab25b..c8be6b5 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -75,6 +75,9 @@
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusConfig;
+import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.Binder;
@@ -776,6 +779,26 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void createVirtualStylus(@NonNull VirtualStylusConfig config,
+            @NonNull IBinder deviceToken) {
+        super.createVirtualStylus_enforcePermission();
+        Objects.requireNonNull(config);
+        Objects.requireNonNull(deviceToken);
+        checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mInputController.createStylus(config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+                    config.getHeight(), config.getWidth());
+        } catch (InputController.DeviceCreationException e) {
+            throw new IllegalArgumentException(e);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterInputDevice(IBinder token) {
         super.unregisterInputDevice_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
@@ -881,6 +904,36 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public boolean sendStylusMotionEvent(@NonNull IBinder token,
+            @NonNull VirtualStylusMotionEvent event) {
+        super.sendStylusMotionEvent_enforcePermission();
+        Objects.requireNonNull(token);
+        Objects.requireNonNull(event);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendStylusMotionEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public boolean sendStylusButtonEvent(@NonNull IBinder token,
+            @NonNull VirtualStylusButtonEvent event) {
+        super.sendStylusButtonEvent_enforcePermission();
+        Objects.requireNonNull(token);
+        Objects.requireNonNull(event);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendStylusButtonEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setShowPointerIcon(boolean showPointerIcon) {
         super.setShowPointerIcon_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 4cd018b..50d48b7 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -44,6 +44,7 @@
     MOUSE,
     TOUCHSCREEN,
     DPAD,
+    STYLUS,
 };
 
 static unique_fd invalidFd() {
@@ -98,6 +99,24 @@
             ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR);
             ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
             ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+            break;
+        case DeviceType::STYLUS:
+            ioctl(fd, UI_SET_EVBIT, EV_ABS);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH);
+            ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS);
+            ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_PEN);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_RUBBER);
+            ioctl(fd, UI_SET_ABSBIT, ABS_X);
+            ioctl(fd, UI_SET_ABSBIT, ABS_Y);
+            ioctl(fd, UI_SET_ABSBIT, ABS_TILT_X);
+            ioctl(fd, UI_SET_ABSBIT, ABS_TILT_Y);
+            ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE);
+            ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+            break;
+        default:
+            ALOGE("Invalid input device type %d", static_cast<int32_t>(deviceType));
+            return invalidFd();
     }
 
     int version;
@@ -158,6 +177,47 @@
                 ALOGE("Error creating touchscreen uinput tracking ids: %s", strerror(errno));
                 return invalidFd();
             }
+        } else if (deviceType == DeviceType::STYLUS) {
+            uinput_abs_setup xAbsSetup;
+            xAbsSetup.code = ABS_X;
+            xAbsSetup.absinfo.maximum = screenWidth - 1;
+            xAbsSetup.absinfo.minimum = 0;
+            if (ioctl(fd, UI_ABS_SETUP, &xAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput x axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup yAbsSetup;
+            yAbsSetup.code = ABS_Y;
+            yAbsSetup.absinfo.maximum = screenHeight - 1;
+            yAbsSetup.absinfo.minimum = 0;
+            if (ioctl(fd, UI_ABS_SETUP, &yAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput y axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup tiltXAbsSetup;
+            tiltXAbsSetup.code = ABS_TILT_X;
+            tiltXAbsSetup.absinfo.maximum = 90;
+            tiltXAbsSetup.absinfo.minimum = -90;
+            if (ioctl(fd, UI_ABS_SETUP, &tiltXAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput tilt x axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup tiltYAbsSetup;
+            tiltYAbsSetup.code = ABS_TILT_Y;
+            tiltYAbsSetup.absinfo.maximum = 90;
+            tiltYAbsSetup.absinfo.minimum = -90;
+            if (ioctl(fd, UI_ABS_SETUP, &tiltYAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput tilt y axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup pressureAbsSetup;
+            pressureAbsSetup.code = ABS_PRESSURE;
+            pressureAbsSetup.absinfo.maximum = 255;
+            pressureAbsSetup.absinfo.minimum = 0;
+            if (ioctl(fd, UI_ABS_SETUP, &pressureAbsSetup) != 0) {
+                ALOGE("Error creating touchscreen uinput pressure axis: %s", strerror(errno));
+                return invalidFd();
+            }
         }
         if (ioctl(fd, UI_DEV_SETUP, &setup) != 0) {
             ALOGE("Error creating uinput device: %s", strerror(errno));
@@ -182,6 +242,17 @@
             fallback.absmax[ABS_MT_TOUCH_MAJOR] = screenWidth - 1;
             fallback.absmin[ABS_MT_PRESSURE] = 0;
             fallback.absmax[ABS_MT_PRESSURE] = 255;
+        } else if (deviceType == DeviceType::STYLUS) {
+            fallback.absmin[ABS_X] = 0;
+            fallback.absmax[ABS_X] = screenWidth - 1;
+            fallback.absmin[ABS_Y] = 0;
+            fallback.absmax[ABS_Y] = screenHeight - 1;
+            fallback.absmin[ABS_TILT_X] = -90;
+            fallback.absmax[ABS_TILT_X] = 90;
+            fallback.absmin[ABS_TILT_Y] = -90;
+            fallback.absmax[ABS_TILT_Y] = 90;
+            fallback.absmin[ABS_PRESSURE] = 0;
+            fallback.absmax[ABS_PRESSURE] = 255;
         }
         if (TEMP_FAILURE_RETRY(write(fd, &fallback, sizeof(fallback))) != sizeof(fallback)) {
             ALOGE("Error creating uinput device: %s", strerror(errno));
@@ -234,6 +305,13 @@
     return fd.ok() ? reinterpret_cast<jlong>(new VirtualTouchscreen(std::move(fd))) : INVALID_PTR;
 }
 
+static jlong nativeOpenUinputStylus(JNIEnv* env, jobject thiz, jstring name, jint vendorId,
+                                    jint productId, jstring phys, jint height, jint width) {
+    auto fd =
+            openUinputJni(env, name, vendorId, productId, phys, DeviceType::STYLUS, height, width);
+    return fd.ok() ? reinterpret_cast<jlong>(new VirtualStylus(std::move(fd))) : INVALID_PTR;
+}
+
 static void nativeCloseUinput(JNIEnv* env, jobject thiz, jlong ptr) {
     VirtualInputDevice* virtualInputDevice = reinterpret_cast<VirtualInputDevice*>(ptr);
     delete virtualInputDevice;
@@ -287,6 +365,22 @@
                                           std::chrono::nanoseconds(eventTimeNanos));
 }
 
+// Native methods for VirtualStylus
+static bool nativeWriteStylusMotionEvent(JNIEnv* env, jobject thiz, jlong ptr, jint toolType,
+                                         jint action, jint locationX, jint locationY, jint pressure,
+                                         jint tiltX, jint tiltY, jlong eventTimeNanos) {
+    VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr);
+    return virtualStylus->writeMotionEvent(toolType, action, locationX, locationY, pressure, tiltX,
+                                           tiltY, std::chrono::nanoseconds(eventTimeNanos));
+}
+
+static bool nativeWriteStylusButtonEvent(JNIEnv* env, jobject thiz, jlong ptr, jint buttonCode,
+                                         jint action, jlong eventTimeNanos) {
+    VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr);
+    return virtualStylus->writeButtonEvent(buttonCode, action,
+                                           std::chrono::nanoseconds(eventTimeNanos));
+}
+
 static JNINativeMethod methods[] = {
         {"nativeOpenUinputDpad", "(Ljava/lang/String;IILjava/lang/String;)J",
          (void*)nativeOpenUinputDpad},
@@ -296,6 +390,8 @@
          (void*)nativeOpenUinputMouse},
         {"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IILjava/lang/String;II)J",
          (void*)nativeOpenUinputTouchscreen},
+        {"nativeOpenUinputStylus", "(Ljava/lang/String;IILjava/lang/String;II)J",
+         (void*)nativeOpenUinputStylus},
         {"nativeCloseUinput", "(J)V", (void*)nativeCloseUinput},
         {"nativeWriteDpadKeyEvent", "(JIIJ)Z", (void*)nativeWriteDpadKeyEvent},
         {"nativeWriteKeyEvent", "(JIIJ)Z", (void*)nativeWriteKeyEvent},
@@ -303,6 +399,8 @@
         {"nativeWriteTouchEvent", "(JIIIFFFFJ)Z", (void*)nativeWriteTouchEvent},
         {"nativeWriteRelativeEvent", "(JFFJ)Z", (void*)nativeWriteRelativeEvent},
         {"nativeWriteScrollEvent", "(JFFJ)Z", (void*)nativeWriteScrollEvent},
+        {"nativeWriteStylusMotionEvent", "(JIIIIIIIJ)Z", (void*)nativeWriteStylusMotionEvent},
+        {"nativeWriteStylusButtonEvent", "(JIIJ)Z", (void*)nativeWriteStylusButtonEvent},
 };
 
 int register_android_server_companion_virtual_InputController(JNIEnv* env) {