Virtual sensor API.

Add VirtualSensor, VirtualSensorEvent and a callback to notify the sensor owner for any changes in the registered sensor listeners.

The virtual device sensors will registered with the sensor framework as runtime sensors. They must remain valid for the entire lifetime of the virtual device, so they are created when it is created and only unregistered when it is closed. The virtual device owners can access them from the device itself after creation.

This CL is only adding the API, the sensor registration and lifetime management is added in a follow-up CL.

Bug: 237278244
Test: atest VirtualSensorConfigTest
Test: atest VirtualSensorEventTest
Test: atest VirtualDeviceParamsTest
Change-Id: I8bb458dacba1bb65b00e288734ce41186c4fe624
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8175d25..fbabeee 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2963,6 +2963,7 @@
     method @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.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method public int getDeviceId();
+    method @Nullable public android.companion.virtual.sensor.VirtualSensor getVirtualSensor(int, @NonNull String);
     method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
@@ -2980,6 +2981,7 @@
     method public int getLockState();
     method @Nullable public String getName();
     method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts();
+    method @NonNull public java.util.List<android.companion.virtual.sensor.VirtualSensorConfig> getVirtualSensorConfigs();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field public static final int ACTIVITY_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int ACTIVITY_POLICY_DEFAULT_BLOCKED = 1; // 0x1
@@ -2996,6 +2998,7 @@
   public static final class VirtualDeviceParams.Builder {
     ctor public VirtualDeviceParams.Builder();
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addDevicePolicy(int, int);
+    method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addVirtualSensorConfig(@NonNull android.companion.virtual.sensor.VirtualSensorConfig);
     method @NonNull public android.companion.virtual.VirtualDeviceParams build();
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
@@ -3053,6 +3056,50 @@
 
 }
 
+package android.companion.virtual.sensor {
+
+  public class VirtualSensor {
+    method @NonNull public String getName();
+    method public int getType();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendSensorEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
+  }
+
+  public static interface VirtualSensor.SensorStateChangeCallback {
+    method public void onStateChanged(boolean, @NonNull java.time.Duration, @NonNull java.time.Duration);
+  }
+
+  public final class VirtualSensorConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getName();
+    method public int getType();
+    method @Nullable public String getVendor();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.sensor.VirtualSensorConfig> CREATOR;
+  }
+
+  public static final class VirtualSensorConfig.Builder {
+    ctor public VirtualSensorConfig.Builder(int, @NonNull String);
+    method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig build();
+    method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setStateChangeCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.sensor.VirtualSensor.SensorStateChangeCallback);
+    method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setVendor(@Nullable String);
+  }
+
+  public final class VirtualSensorEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public long getTimestampNanos();
+    method @NonNull public float[] getValues();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.sensor.VirtualSensorEvent> CREATOR;
+  }
+
+  public static final class VirtualSensorEvent.Builder {
+    ctor public VirtualSensorEvent.Builder(@NonNull float[]);
+    method @NonNull public android.companion.virtual.sensor.VirtualSensorEvent build();
+    method @NonNull public android.companion.virtual.sensor.VirtualSensorEvent.Builder setTimestampNanos(long);
+  }
+
+}
+
 package android.content {
 
   public class ApexEnvironment {
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 295d69d..0837d85 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -19,6 +19,9 @@
 import android.app.PendingIntent;
 import android.companion.virtual.audio.IAudioConfigChangedCallback;
 import android.companion.virtual.audio.IAudioRoutingCallback;
+import android.companion.virtual.sensor.IVirtualSensorStateChangeCallback;
+import android.companion.virtual.sensor.VirtualSensorConfig;
+import android.companion.virtual.sensor.VirtualSensorEvent;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.hardware.input.VirtualKeyEvent;
@@ -97,6 +100,24 @@
     boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
 
     /**
+     * Creates a virtual sensor, capable of injecting sensor events into the system.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    void createVirtualSensor(IBinder tokenm, in VirtualSensorConfig config);
+
+    /**
+     * Removes the sensor corresponding to the given token from the system.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    void unregisterSensor(IBinder token);
+
+    /**
+     * Sends an event to the virtual sensor corresponding to the given token.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event);
+
+    /**
      * Launches a pending intent on the given display that is owned by this virtual device.
      */
     void launchPendingIntent(
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index c14bb1b..fbb4bd0 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -28,6 +28,8 @@
 import android.companion.AssociationInfo;
 import android.companion.virtual.audio.VirtualAudioDevice;
 import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback;
+import android.companion.virtual.sensor.VirtualSensor;
+import android.companion.virtual.sensor.VirtualSensorConfig;
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Point;
@@ -58,6 +60,7 @@
 import java.lang.annotation.Target;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.IntConsumer;
 
@@ -250,7 +253,10 @@
                 };
         @Nullable
         private VirtualAudioDevice mVirtualAudioDevice;
+        @NonNull
+        private List<VirtualSensor> mVirtualSensors = new ArrayList<>();
 
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         private VirtualDevice(
                 IVirtualDeviceManager service,
                 Context context,
@@ -264,6 +270,10 @@
                     associationId,
                     params,
                     mActivityListenerBinder);
+            final List<VirtualSensorConfig> virtualSensorConfigs = params.getVirtualSensorConfigs();
+            for (int i = 0; i < virtualSensorConfigs.size(); ++i) {
+                mVirtualSensors.add(createVirtualSensor(virtualSensorConfigs.get(i)));
+            }
         }
 
         /**
@@ -278,6 +288,23 @@
         }
 
         /**
+         * Returns this device's sensor with the given type and name, if any.
+         *
+         * @see VirtualDeviceParams.Builder#addVirtualSensorConfig
+         *
+         * @param type The type of the sensor.
+         * @param name The name of the sensor.
+         * @return The matching sensor if found, {@code null} otherwise.
+         */
+        @Nullable
+        public VirtualSensor getVirtualSensor(int type, @NonNull String name) {
+            return mVirtualSensors.stream()
+                    .filter(sensor -> sensor.getType() == type && sensor.getName().equals(name))
+                    .findAny()
+                    .orElse(null);
+        }
+
+        /**
          * Launches a given pending intent on the give display ID.
          *
          * @param displayId The display to launch the pending intent on. This display must be
@@ -379,6 +406,7 @@
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void close() {
             try {
+                // This also takes care of unregistering all virtual sensors.
                 mVirtualDevice.close();
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
@@ -564,6 +592,28 @@
         }
 
         /**
+         * Creates a virtual sensor, capable of injecting sensor events into the system. Only for
+         * internal use, since device sensors must remain valid for the entire lifetime of the
+         * device.
+         *
+         * @param config The configuration of the sensor.
+         * @hide
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualSensor createVirtualSensor(@NonNull VirtualSensorConfig config) {
+            Objects.requireNonNull(config);
+            try {
+                final IBinder token = new Binder(
+                        "android.hardware.sensor.VirtualSensor:" + config.getName());
+                mVirtualDevice.createVirtualSensor(token, config);
+                return new VirtualSensor(config.getType(), config.getName(), mVirtualDevice, token);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Adds an activity listener to listen for events such as top activity change or virtual
          * display task stack became empty.
          *
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index f8c2e34a..bad26c6 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -23,20 +23,22 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.companion.virtual.sensor.VirtualSensorConfig;
 import android.content.ComponentName;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.util.ArraySet;
+import android.util.SparseArray;
 import android.util.SparseIntArray;
 
-import com.android.internal.util.Preconditions;
-
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 
@@ -158,6 +160,7 @@
     @Nullable private final String mName;
     // Mapping of @PolicyType to @DevicePolicy
     @NonNull private final SparseIntArray mDevicePolicies;
+    @NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
 
     private VirtualDeviceParams(
             @LockState int lockState,
@@ -169,24 +172,22 @@
             @NonNull Set<ComponentName> blockedActivities,
             @ActivityPolicy int defaultActivityPolicy,
             @Nullable String name,
-            @NonNull SparseIntArray devicePolicies) {
-        Preconditions.checkNotNull(usersWithMatchingAccounts);
-        Preconditions.checkNotNull(allowedCrossTaskNavigations);
-        Preconditions.checkNotNull(blockedCrossTaskNavigations);
-        Preconditions.checkNotNull(allowedActivities);
-        Preconditions.checkNotNull(blockedActivities);
-        Preconditions.checkNotNull(devicePolicies);
-
+            @NonNull SparseIntArray devicePolicies,
+            @NonNull List<VirtualSensorConfig> virtualSensorConfigs) {
         mLockState = lockState;
-        mUsersWithMatchingAccounts = new ArraySet<>(usersWithMatchingAccounts);
-        mAllowedCrossTaskNavigations = new ArraySet<>(allowedCrossTaskNavigations);
-        mBlockedCrossTaskNavigations = new ArraySet<>(blockedCrossTaskNavigations);
+        mUsersWithMatchingAccounts =
+                new ArraySet<>(Objects.requireNonNull(usersWithMatchingAccounts));
+        mAllowedCrossTaskNavigations =
+                new ArraySet<>(Objects.requireNonNull(allowedCrossTaskNavigations));
+        mBlockedCrossTaskNavigations =
+                new ArraySet<>(Objects.requireNonNull(blockedCrossTaskNavigations));
         mDefaultNavigationPolicy = defaultNavigationPolicy;
-        mAllowedActivities = new ArraySet<>(allowedActivities);
-        mBlockedActivities = new ArraySet<>(blockedActivities);
+        mAllowedActivities = new ArraySet<>(Objects.requireNonNull(allowedActivities));
+        mBlockedActivities = new ArraySet<>(Objects.requireNonNull(blockedActivities));
         mDefaultActivityPolicy = defaultActivityPolicy;
         mName = name;
-        mDevicePolicies = devicePolicies;
+        mDevicePolicies = Objects.requireNonNull(devicePolicies);
+        mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
     }
 
     @SuppressWarnings("unchecked")
@@ -201,6 +202,8 @@
         mDefaultActivityPolicy = parcel.readInt();
         mName = parcel.readString8();
         mDevicePolicies = parcel.readSparseIntArray();
+        mVirtualSensorConfigs = new ArrayList<>();
+        parcel.readTypedList(mVirtualSensorConfigs, VirtualSensorConfig.CREATOR);
     }
 
     /**
@@ -316,6 +319,15 @@
         return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT);
     }
 
+    /**
+     * Returns the configurations for all sensors that should be created for this device.
+     *
+     * @see Builder#addVirtualSensorConfig
+     */
+    public @NonNull List<VirtualSensorConfig> getVirtualSensorConfigs() {
+        return mVirtualSensorConfigs;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -333,6 +345,7 @@
         dest.writeInt(mDefaultActivityPolicy);
         dest.writeString8(mName);
         dest.writeSparseIntArray(mDevicePolicies);
+        dest.writeTypedList(mVirtualSensorConfigs);
     }
 
     @Override
@@ -428,6 +441,7 @@
         private boolean mDefaultActivityPolicyConfigured = false;
         @Nullable private String mName;
         @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray();
+        @NonNull private List<VirtualSensorConfig> mVirtualSensorConfigs = new ArrayList<>();
 
         /**
          * Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY}
@@ -467,8 +481,7 @@
         @NonNull
         public Builder setUsersWithMatchingAccounts(
                 @NonNull Set<UserHandle> usersWithMatchingAccounts) {
-            Preconditions.checkNotNull(usersWithMatchingAccounts);
-            mUsersWithMatchingAccounts = usersWithMatchingAccounts;
+            mUsersWithMatchingAccounts = Objects.requireNonNull(usersWithMatchingAccounts);
             return this;
         }
 
@@ -491,7 +504,6 @@
         @NonNull
         public Builder setAllowedCrossTaskNavigations(
                 @NonNull Set<ComponentName> allowedCrossTaskNavigations) {
-            Preconditions.checkNotNull(allowedCrossTaskNavigations);
             if (mDefaultNavigationPolicyConfigured
                     && mDefaultNavigationPolicy != NAVIGATION_POLICY_DEFAULT_BLOCKED) {
                 throw new IllegalArgumentException(
@@ -500,7 +512,7 @@
             }
             mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_BLOCKED;
             mDefaultNavigationPolicyConfigured = true;
-            mAllowedCrossTaskNavigations = allowedCrossTaskNavigations;
+            mAllowedCrossTaskNavigations = Objects.requireNonNull(allowedCrossTaskNavigations);
             return this;
         }
 
@@ -523,7 +535,6 @@
         @NonNull
         public Builder setBlockedCrossTaskNavigations(
                 @NonNull Set<ComponentName> blockedCrossTaskNavigations) {
-            Preconditions.checkNotNull(blockedCrossTaskNavigations);
             if (mDefaultNavigationPolicyConfigured
                      && mDefaultNavigationPolicy != NAVIGATION_POLICY_DEFAULT_ALLOWED) {
                 throw new IllegalArgumentException(
@@ -532,7 +543,7 @@
             }
             mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_ALLOWED;
             mDefaultNavigationPolicyConfigured = true;
-            mBlockedCrossTaskNavigations = blockedCrossTaskNavigations;
+            mBlockedCrossTaskNavigations = Objects.requireNonNull(blockedCrossTaskNavigations);
             return this;
         }
 
@@ -551,7 +562,6 @@
          */
         @NonNull
         public Builder setAllowedActivities(@NonNull Set<ComponentName> allowedActivities) {
-            Preconditions.checkNotNull(allowedActivities);
             if (mDefaultActivityPolicyConfigured
                     && mDefaultActivityPolicy != ACTIVITY_POLICY_DEFAULT_BLOCKED) {
                 throw new IllegalArgumentException(
@@ -559,7 +569,7 @@
             }
             mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_BLOCKED;
             mDefaultActivityPolicyConfigured = true;
-            mAllowedActivities = allowedActivities;
+            mAllowedActivities = Objects.requireNonNull(allowedActivities);
             return this;
         }
 
@@ -578,7 +588,6 @@
          */
         @NonNull
         public Builder setBlockedActivities(@NonNull Set<ComponentName> blockedActivities) {
-            Preconditions.checkNotNull(blockedActivities);
             if (mDefaultActivityPolicyConfigured
                     && mDefaultActivityPolicy != ACTIVITY_POLICY_DEFAULT_ALLOWED) {
                 throw new IllegalArgumentException(
@@ -586,7 +595,7 @@
             }
             mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED;
             mDefaultActivityPolicyConfigured = true;
-            mBlockedActivities = blockedActivities;
+            mBlockedActivities = Objects.requireNonNull(blockedActivities);
             return this;
         }
 
@@ -621,10 +630,49 @@
         }
 
         /**
+         * Adds a configuration for a sensor that should be created for this virtual device.
+         *
+         * Device sensors must remain valid for the entire lifetime of the device, hence they are
+         * created together with the device itself, and removed when the device is removed.
+         *
+         * Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_SENSORS}.
+         *
+         * @see android.companion.virtual.sensor.VirtualSensor
+         * @see #addDevicePolicy
+         */
+        @NonNull
+        public Builder addVirtualSensorConfig(@NonNull VirtualSensorConfig virtualSensorConfig) {
+            mVirtualSensorConfigs.add(Objects.requireNonNull(virtualSensorConfig));
+            return this;
+        }
+
+        /**
          * Builds the {@link VirtualDeviceParams} instance.
+         *
+         * @throws IllegalArgumentException if there's mismatch between policy definition and
+         * the passed parameters or if there are sensor configs with the same type and name.
+         *
          */
         @NonNull
         public VirtualDeviceParams build() {
+            if (!mVirtualSensorConfigs.isEmpty()
+                    && (mDevicePolicies.get(POLICY_TYPE_SENSORS, DEVICE_POLICY_DEFAULT)
+                            != DEVICE_POLICY_CUSTOM)) {
+                throw new IllegalArgumentException(
+                        "DEVICE_POLICY_CUSTOM for POLICY_TYPE_SENSORS is required for creating "
+                                + "virtual sensors.");
+            }
+            SparseArray<Set<String>> sensorNameByType = new SparseArray();
+            for (int i = 0; i < mVirtualSensorConfigs.size(); ++i) {
+                VirtualSensorConfig config = mVirtualSensorConfigs.get(i);
+                Set<String> sensorNames = sensorNameByType.get(config.getType(), new ArraySet<>());
+                if (!sensorNames.add(config.getName())) {
+                    throw new IllegalArgumentException(
+                            "Sensor names must be unique for a particular sensor type.");
+                }
+                sensorNameByType.put(config.getType(), sensorNames);
+            }
+
             return new VirtualDeviceParams(
                     mLockState,
                     mUsersWithMatchingAccounts,
@@ -635,7 +683,8 @@
                     mBlockedActivities,
                     mDefaultActivityPolicy,
                     mName,
-                    mDevicePolicies);
+                    mDevicePolicies,
+                    mVirtualSensorConfigs);
         }
     }
 }
diff --git a/core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl b/core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl
new file mode 100644
index 0000000..b99cc7e
--- /dev/null
+++ b/core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+/**
+ * Interface for notification of listener registration changes for a virtual sensor.
+ *
+ * @hide
+ */
+oneway interface IVirtualSensorStateChangeCallback {
+
+    /**
+     * Called when the registered listeners to a virtual sensor have changed.
+     *
+     * @param enabled Whether the sensor is enabled.
+     * @param samplingPeriodMicros The requested sensor's sampling period in microseconds.
+     * @param batchReportingLatencyMicros The requested maximum time interval in microseconds
+     * between the delivery of two batches of sensor events.
+     */
+    void onStateChanged(boolean enabled, int samplingPeriodMicros, int batchReportLatencyMicros);
+}
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java
new file mode 100644
index 0000000..a184481
--- /dev/null
+++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.time.Duration;
+
+/**
+ * Representation of a sensor on a remote device, capable of sending events, such as an
+ * accelerometer or a gyroscope.
+ *
+ * This registers the sensor device with the sensor framework as a runtime sensor.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualSensor {
+
+    /**
+     * Interface for notification of listener registration changes for a virtual sensor.
+     */
+    public interface SensorStateChangeCallback {
+        /**
+         * Called when the registered listeners to a virtual sensor have changed.
+         *
+         * @param enabled Whether the sensor is enabled.
+         * @param samplingPeriod The requested sampling period of the sensor.
+         * @param batchReportLatency The requested maximum time interval between the delivery of two
+         * batches of sensor events.
+         */
+        void onStateChanged(boolean enabled, @NonNull Duration samplingPeriod,
+                @NonNull Duration batchReportLatency);
+    }
+
+    private final int mType;
+    private final String mName;
+    private final IVirtualDevice mVirtualDevice;
+    private final IBinder mToken;
+
+    /**
+     * @hide
+     */
+    public VirtualSensor(int type, String name, IVirtualDevice virtualDevice, IBinder token) {
+        mType = type;
+        mName = name;
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+    /**
+     * Returns the
+     * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+     */
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns the name of the sensor.
+     */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Send a sensor event to the system.
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendSensorEvent(@NonNull VirtualSensorEvent event) {
+        try {
+            mVirtualDevice.sendSensorEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl
new file mode 100644
index 0000000..48b463a
--- /dev/null
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+parcelable VirtualSensorConfig;
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
new file mode 100644
index 0000000..7982fa5
--- /dev/null
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Configuration for creation of a virtual sensor.
+ * @see VirtualSensor
+ * @hide
+ */
+@SystemApi
+public final class VirtualSensorConfig implements Parcelable {
+
+    private final int mType;
+    @NonNull
+    private final String mName;
+    @Nullable
+    private final String mVendor;
+    @Nullable
+    private final IVirtualSensorStateChangeCallback mStateChangeCallback;
+
+    private VirtualSensorConfig(int type, @NonNull String name, @Nullable String vendor,
+            @Nullable IVirtualSensorStateChangeCallback stateChangeCallback) {
+        mType = type;
+        mName = name;
+        mVendor = vendor;
+        mStateChangeCallback = stateChangeCallback;
+    }
+
+    private VirtualSensorConfig(@NonNull Parcel parcel) {
+        mType = parcel.readInt();
+        mName = parcel.readString8();
+        mVendor = parcel.readString8();
+        mStateChangeCallback =
+                IVirtualSensorStateChangeCallback.Stub.asInterface(parcel.readStrongBinder());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mType);
+        parcel.writeString8(mName);
+        parcel.writeString8(mVendor);
+        parcel.writeStrongBinder(
+                mStateChangeCallback != null ? mStateChangeCallback.asBinder() : null);
+    }
+
+    /**
+     * Returns the
+     * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+     */
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns the name of the sensor, which must be unique per sensor type for each virtual device.
+     */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Returns the vendor string of the sensor.
+     * @see Builder#setVendor
+     */
+    @Nullable
+    public String getVendor() {
+        return mVendor;
+    }
+
+    /**
+     * Returns the callback to get notified about changes in the sensor listeners.
+     * @hide
+     */
+    @Nullable
+    public IVirtualSensorStateChangeCallback getStateChangeCallback() {
+        return mStateChangeCallback;
+    }
+
+    /**
+     * Builder for {@link VirtualSensorConfig}.
+     */
+    public static final class Builder {
+
+        private final int mType;
+        @NonNull
+        private final String mName;
+        @Nullable
+        private String mVendor;
+        @Nullable
+        private IVirtualSensorStateChangeCallback mStateChangeCallback;
+
+        private static class SensorStateChangeCallbackDelegate
+                extends IVirtualSensorStateChangeCallback.Stub {
+            @NonNull
+            private final Executor mExecutor;
+            @NonNull
+            private final VirtualSensor.SensorStateChangeCallback mCallback;
+
+            SensorStateChangeCallbackDelegate(@NonNull @CallbackExecutor Executor executor,
+                    @NonNull VirtualSensor.SensorStateChangeCallback callback) {
+                mCallback = callback;
+                mExecutor = executor;
+            }
+            @Override
+            public void onStateChanged(boolean enabled, int samplingPeriodMicros,
+                    int batchReportLatencyMicros) {
+                final Duration samplingPeriod =
+                        Duration.ofNanos(MICROSECONDS.toNanos(samplingPeriodMicros));
+                final Duration batchReportingLatency =
+                        Duration.ofNanos(MICROSECONDS.toNanos(batchReportLatencyMicros));
+                mExecutor.execute(() -> mCallback.onStateChanged(
+                        enabled, samplingPeriod, batchReportingLatency));
+            }
+        }
+
+        /**
+         * Creates a new builder.
+         *
+         * @param type The
+         * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+         * @param name The name of the sensor. Must be unique among all sensors with the same type
+         * that belong to the same virtual device.
+         */
+        public Builder(int type, @NonNull String name) {
+            mType = type;
+            mName = Objects.requireNonNull(name);
+        }
+
+        /**
+         * Creates a new {@link VirtualSensorConfig}.
+         */
+        @NonNull
+        public VirtualSensorConfig build() {
+            return new VirtualSensorConfig(mType, mName, mVendor, mStateChangeCallback);
+        }
+
+        /**
+         * Sets the vendor string of the sensor.
+         */
+        @NonNull
+        public VirtualSensorConfig.Builder setVendor(@Nullable String vendor) {
+            mVendor = vendor;
+            return this;
+        }
+
+        /**
+         * Sets the callback to get notified about changes in the sensor listeners.
+         *
+         * @param executor The executor where the callback is executed on.
+         * @param callback The callback to get notified when the state of the sensor
+         * listeners has changed, see {@link VirtualSensor.SensorStateChangeCallback}
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public VirtualSensorConfig.Builder setStateChangeCallback(
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull VirtualSensor.SensorStateChangeCallback callback) {
+            mStateChangeCallback = new SensorStateChangeCallbackDelegate(
+                    Objects.requireNonNull(executor),
+                    Objects.requireNonNull(callback));
+            return this;
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<VirtualSensorConfig> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualSensorConfig createFromParcel(Parcel source) {
+                    return new VirtualSensorConfig(source);
+                }
+
+                public VirtualSensorConfig[] newArray(int size) {
+                    return new VirtualSensorConfig[size];
+                }
+            };
+}
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl
new file mode 100644
index 0000000..9943946
--- /dev/null
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+parcelable VirtualSensorEvent;
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
new file mode 100644
index 0000000..8f8860e
--- /dev/null
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+
+
+/**
+ * A sensor event that originated from a virtual device's sensor.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualSensorEvent implements Parcelable {
+
+    @NonNull
+    private float[] mValues;
+    private long mTimestampNanos;
+
+    private VirtualSensorEvent(@NonNull float[] values, long timestampNanos) {
+        mValues = values;
+        mTimestampNanos = timestampNanos;
+    }
+
+    private VirtualSensorEvent(@NonNull Parcel parcel) {
+        final int valuesLength = parcel.readInt();
+        mValues = new float[valuesLength];
+        parcel.readFloatArray(mValues);
+        mTimestampNanos = parcel.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeInt(mValues.length);
+        parcel.writeFloatArray(mValues);
+        parcel.writeLong(mTimestampNanos);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the values of this sensor event. The length and contents depend on the
+     * <a href="https://source.android.com/devices/sensors/sensor-types">sensor type</a>.
+     * @see android.hardware.SensorEvent#values
+     */
+    @NonNull
+    public float[] getValues() {
+        return mValues;
+    }
+
+    /**
+     * The time in nanoseconds at which the event happened. For a given sensor, each new sensor
+     * event should be monotonically increasing.
+     *
+     * @see Builder#setTimestampNanos(long)
+     */
+    public long getTimestampNanos() {
+        return mTimestampNanos;
+    }
+
+    /**
+     * Builder for {@link VirtualSensorEvent}.
+     */
+    public static final class Builder {
+
+        @NonNull
+        private float[] mValues;
+        private long mTimestampNanos = 0;
+
+        /**
+         * Creates a new builder.
+         * @param values the values of the sensor event. @see android.hardware.SensorEvent#values
+         */
+        public Builder(@NonNull float[] values) {
+            mValues = values;
+        }
+
+        /**
+         * Creates a new {@link VirtualSensorEvent}.
+         */
+        @NonNull
+        public VirtualSensorEvent build() {
+            if (mValues == null || mValues.length == 0) {
+                throw new IllegalArgumentException(
+                        "Cannot build virtual sensor event with no values.");
+            }
+            if (mTimestampNanos <= 0) {
+                mTimestampNanos = SystemClock.elapsedRealtimeNanos();
+            }
+            return new VirtualSensorEvent(mValues, mTimestampNanos);
+        }
+
+        /**
+         * Sets the timestamp of this event. For a given sensor, each new sensor event should be
+         * monotonically increasing using the same time base as
+         * {@link android.os.SystemClock#elapsedRealtimeNanos()}.
+         *
+         * If not explicitly set, the current timestamp is used for the sensor event.
+         *
+         * @see android.hardware.SensorEvent#timestamp
+         */
+        @NonNull
+        public Builder setTimestampNanos(long timestampNanos) {
+            mTimestampNanos = timestampNanos;
+            return this;
+        }
+    }
+
+    public static final @NonNull Parcelable.Creator<VirtualSensorEvent> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualSensorEvent createFromParcel(Parcel source) {
+                    return new VirtualSensorEvent(source);
+                }
+
+                public VirtualSensorEvent[] newArray(int size) {
+                    return new VirtualSensorEvent[size];
+                }
+            };
+}
diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
new file mode 100644
index 0000000..11afd04
--- /dev/null
+++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+import static android.hardware.Sensor.TYPE_ACCELEROMETER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.os.Parcel;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.BackgroundThread;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.time.Duration;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualSensorConfigTest {
+
+    private static final String SENSOR_NAME = "VirtualSensorName";
+    private static final String SENSOR_VENDOR = "VirtualSensorVendor";
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private VirtualSensor.SensorStateChangeCallback mSensorCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void parcelAndUnparcel_matches() {
+        final VirtualSensorConfig originalConfig =
+                new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
+                        .setVendor(SENSOR_VENDOR)
+                        .setStateChangeCallback(BackgroundThread.getExecutor(), mSensorCallback)
+                        .build();
+        final Parcel parcel = Parcel.obtain();
+        originalConfig.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+        final VirtualSensorConfig recreatedConfig =
+                VirtualSensorConfig.CREATOR.createFromParcel(parcel);
+        assertThat(recreatedConfig.getType()).isEqualTo(originalConfig.getType());
+        assertThat(recreatedConfig.getName()).isEqualTo(originalConfig.getName());
+        assertThat(recreatedConfig.getVendor()).isEqualTo(originalConfig.getVendor());
+        assertThat(recreatedConfig.getStateChangeCallback()).isNotNull();
+    }
+
+    @Test
+    public void sensorConfig_onlyRequiredFields() {
+        final VirtualSensorConfig config =
+                new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME).build();
+        assertThat(config.getVendor()).isNull();
+        assertThat(config.getStateChangeCallback()).isNull();
+    }
+
+    @Test
+    public void sensorConfig_sensorCallbackInvocation() throws Exception {
+        final VirtualSensorConfig config =
+                new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
+                        .setStateChangeCallback(BackgroundThread.getExecutor(), mSensorCallback)
+                        .build();
+
+        final Duration samplingPeriod = Duration.ofMillis(123);
+        final Duration batchLatency = Duration.ofMillis(456);
+
+        config.getStateChangeCallback().onStateChanged(true,
+                (int) MILLISECONDS.toMicros(samplingPeriod.toMillis()),
+                (int) MILLISECONDS.toMicros(batchLatency.toMillis()));
+
+        verify(mSensorCallback, timeout(1000)).onStateChanged(true, samplingPeriod, batchLatency);
+    }
+}
diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
new file mode 100644
index 0000000..a9583fd
--- /dev/null
+++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.sensor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Parcel;
+import android.os.SystemClock;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualSensorEventTest {
+
+    private static final long TIMESTAMP_NANOS = SystemClock.elapsedRealtimeNanos();
+    private static final float[] SENSOR_VALUES = new float[] {1.2f, 3.4f, 5.6f};
+
+    @Test
+    public void parcelAndUnparcel_matches() {
+        final VirtualSensorEvent originalEvent = new VirtualSensorEvent.Builder(SENSOR_VALUES)
+                .setTimestampNanos(TIMESTAMP_NANOS)
+                .build();
+        final Parcel parcel = Parcel.obtain();
+        originalEvent.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+        final VirtualSensorEvent recreatedEvent =
+                VirtualSensorEvent.CREATOR.createFromParcel(parcel);
+        assertThat(recreatedEvent.getValues()).isEqualTo(originalEvent.getValues());
+        assertThat(recreatedEvent.getTimestampNanos()).isEqualTo(originalEvent.getTimestampNanos());
+    }
+
+    @Test
+    public void sensorEvent_nullValues() {
+        assertThrows(
+                IllegalArgumentException.class, () -> new VirtualSensorEvent.Builder(null).build());
+    }
+
+    @Test
+    public void sensorEvent_noValues() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new VirtualSensorEvent.Builder(new float[0]).build());
+    }
+
+    @Test
+    public void sensorEvent_noTimestamp_usesCurrentTime() {
+        final VirtualSensorEvent event = new VirtualSensorEvent.Builder(SENSOR_VALUES).build();
+        assertThat(event.getValues()).isEqualTo(SENSOR_VALUES);
+        assertThat(TIMESTAMP_NANOS).isLessThan(event.getTimestampNanos());
+        assertThat(event.getTimestampNanos()).isLessThan(SystemClock.elapsedRealtimeNanos());
+    }
+
+    @Test
+    public void sensorEvent_created() {
+        final VirtualSensorEvent event = new VirtualSensorEvent.Builder(SENSOR_VALUES)
+                .setTimestampNanos(TIMESTAMP_NANOS)
+                .build();
+        assertThat(event.getTimestampNanos()).isEqualTo(TIMESTAMP_NANOS);
+        assertThat(event.getValues()).isEqualTo(SENSOR_VALUES);
+    }
+}
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 0def25d..019f582 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -38,6 +38,8 @@
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.audio.IAudioConfigChangedCallback;
 import android.companion.virtual.audio.IAudioRoutingCallback;
+import android.companion.virtual.sensor.VirtualSensorConfig;
+import android.companion.virtual.sensor.VirtualSensorEvent;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -613,6 +615,21 @@
         }
     }
 
+    @Override // Binder call
+    public void createVirtualSensor(
+            @NonNull IBinder deviceToken,
+            @NonNull VirtualSensorConfig config) {
+    }
+
+    @Override // Binder call
+    public void unregisterSensor(IBinder token) {
+    }
+
+    @Override // Binder call
+    public boolean sendSensorEvent(IBinder token, VirtualSensorEvent event) {
+        return true;
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
         fout.println("  VirtualDevice: ");
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
index 036b6df..a226ebc 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
@@ -16,9 +16,14 @@
 
 package com.android.server.companion.virtual;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
+import static android.hardware.Sensor.TYPE_ACCELEROMETER;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.companion.virtual.VirtualDeviceParams;
+import android.companion.virtual.sensor.VirtualSensorConfig;
 import android.os.Parcel;
 import android.os.UserHandle;
 
@@ -27,18 +32,25 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 public class VirtualDeviceParamsTest {
 
+    private static final String SENSOR_NAME = "VirtualSensorName";
+    private static final String SENSOR_VENDOR = "VirtualSensorVendor";
+
     @Test
     public void parcelable_shouldRecreateSuccessfully() {
         VirtualDeviceParams originalParams = new VirtualDeviceParams.Builder()
                 .setLockState(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED)
                 .setUsersWithMatchingAccounts(Set.of(UserHandle.of(123), UserHandle.of(456)))
-                .addDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS,
-                        VirtualDeviceParams.DEVICE_POLICY_CUSTOM)
+                .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM)
+                .addVirtualSensorConfig(
+                        new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
+                                .setVendor(SENSOR_VENDOR)
+                                .build())
                 .build();
         Parcel parcel = Parcel.obtain();
         originalParams.writeToParcel(parcel, 0);
@@ -49,7 +61,14 @@
         assertThat(params.getLockState()).isEqualTo(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED);
         assertThat(params.getUsersWithMatchingAccounts())
                 .containsExactly(UserHandle.of(123), UserHandle.of(456));
-        assertThat(params.getDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS))
-                .isEqualTo(VirtualDeviceParams.DEVICE_POLICY_CUSTOM);
+        assertThat(params.getDevicePolicy(POLICY_TYPE_SENSORS)).isEqualTo(DEVICE_POLICY_CUSTOM);
+
+        List<VirtualSensorConfig> sensorConfigs = params.getVirtualSensorConfigs();
+        assertThat(sensorConfigs).hasSize(1);
+        VirtualSensorConfig sensorConfig = sensorConfigs.get(0);
+        assertThat(sensorConfig.getType()).isEqualTo(TYPE_ACCELEROMETER);
+        assertThat(sensorConfig.getName()).isEqualTo(SENSOR_NAME);
+        assertThat(sensorConfig.getVendor()).isEqualTo(SENSOR_VENDOR);
+        assertThat(sensorConfig.getStateChangeCallback()).isNull();
     }
 }