Virtual sensors lifecycle management.

Add a SensorController between virtual devices and the JNI layer of the sensor service.

The virtual sensors are registered with the sensor framework as runtime sensors when the virtual device is created, and unregistered when the device is closed.

Bug: 237278244
Test: atest SensorControllerTest
Test: atest VirtualDeviceManagerServiceTest
Change-Id: I474b64381ccaf6fd6b0055f7344ebe6d8d5d6e3f
diff --git a/services/companion/java/com/android/server/companion/virtual/SensorController.java b/services/companion/java/com/android/server/companion/virtual/SensorController.java
new file mode 100644
index 0000000..ec7e993
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/SensorController.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual;
+
+import android.annotation.NonNull;
+import android.companion.virtual.sensor.IVirtualSensorStateChangeCallback;
+import android.companion.virtual.sensor.VirtualSensorConfig;
+import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.sensors.SensorManagerInternal;
+
+import java.io.PrintWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+
+/** Controls virtual sensors, including their lifecycle and sensor event dispatch. */
+public class SensorController {
+
+    private static final String TAG = "SensorController";
+
+    private final Object mLock;
+    private final int mVirtualDeviceId;
+    @GuardedBy("mLock")
+    private final Map<IBinder, SensorDescriptor> mSensorDescriptors = new ArrayMap<>();
+
+    private final SensorManagerInternal mSensorManagerInternal;
+
+    public SensorController(@NonNull Object lock, int virtualDeviceId) {
+        mLock = lock;
+        mVirtualDeviceId = virtualDeviceId;
+        mSensorManagerInternal = LocalServices.getService(SensorManagerInternal.class);
+    }
+
+    void close() {
+        synchronized (mLock) {
+            final Iterator<Map.Entry<IBinder, SensorDescriptor>> iterator =
+                    mSensorDescriptors.entrySet().iterator();
+            if (iterator.hasNext()) {
+                final Map.Entry<IBinder, SensorDescriptor> entry = iterator.next();
+                final IBinder token = entry.getKey();
+                final SensorDescriptor sensorDescriptor = entry.getValue();
+                iterator.remove();
+                closeSensorDescriptorLocked(token, sensorDescriptor);
+            }
+        }
+    }
+
+    void createSensor(@NonNull IBinder deviceToken, @NonNull VirtualSensorConfig config) {
+        Objects.requireNonNull(deviceToken);
+        Objects.requireNonNull(config);
+        try {
+            createSensorInternal(deviceToken, config);
+        } catch (SensorCreationException e) {
+            throw new RuntimeException(
+                    "Failed to create virtual sensor '" + config.getName() + "'.", e);
+        }
+    }
+
+    private void createSensorInternal(IBinder deviceToken, VirtualSensorConfig config)
+            throws SensorCreationException {
+        final SensorManagerInternal.RuntimeSensorStateChangeCallback runtimeSensorCallback =
+                (enabled, samplingPeriodMicros, batchReportLatencyMicros) -> {
+                    IVirtualSensorStateChangeCallback callback = config.getStateChangeCallback();
+                    if (callback != null) {
+                        try {
+                            callback.onStateChanged(
+                                    enabled, samplingPeriodMicros, batchReportLatencyMicros);
+                        } catch (RemoteException e) {
+                            throw new RuntimeException("Failed to call sensor callback.", e);
+                        }
+                    }
+                };
+
+        final int handle = mSensorManagerInternal.createRuntimeSensor(mVirtualDeviceId,
+                config.getType(), config.getName(),
+                config.getVendor() == null ? "" : config.getVendor(),
+                runtimeSensorCallback);
+        if (handle <= 0) {
+            throw new SensorCreationException("Received an invalid virtual sensor handle.");
+        }
+
+        // The handle is valid from here, so ensure that all failures clean it up.
+        final BinderDeathRecipient binderDeathRecipient;
+        try {
+            binderDeathRecipient = new BinderDeathRecipient(deviceToken);
+            deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
+        } catch (RemoteException e) {
+            mSensorManagerInternal.removeRuntimeSensor(handle);
+            throw new SensorCreationException("Client died before sensor could be created.", e);
+        }
+
+        synchronized (mLock) {
+            SensorDescriptor sensorDescriptor = new SensorDescriptor(
+                    handle, config.getType(), config.getName(), binderDeathRecipient);
+            mSensorDescriptors.put(deviceToken, sensorDescriptor);
+        }
+    }
+
+    boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
+        Objects.requireNonNull(token);
+        Objects.requireNonNull(event);
+        synchronized (mLock) {
+            final SensorDescriptor sensorDescriptor = mSensorDescriptors.get(token);
+            if (sensorDescriptor == null) {
+                throw new IllegalArgumentException("Could not send sensor event for given token");
+            }
+            return mSensorManagerInternal.sendSensorEvent(
+                    sensorDescriptor.getHandle(), sensorDescriptor.getType(),
+                    event.getTimestampNanos(), event.getValues());
+        }
+    }
+
+    void unregisterSensor(@NonNull IBinder token) {
+        Objects.requireNonNull(token);
+        synchronized (mLock) {
+            final SensorDescriptor sensorDescriptor = mSensorDescriptors.remove(token);
+            if (sensorDescriptor == null) {
+                throw new IllegalArgumentException("Could not unregister sensor for given token");
+            }
+            closeSensorDescriptorLocked(token, sensorDescriptor);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void closeSensorDescriptorLocked(IBinder token, SensorDescriptor sensorDescriptor) {
+        token.unlinkToDeath(sensorDescriptor.getDeathRecipient(), /* flags= */ 0);
+        final int handle = sensorDescriptor.getHandle();
+        mSensorManagerInternal.removeRuntimeSensor(handle);
+    }
+
+
+    void dump(@NonNull PrintWriter fout) {
+        fout.println("    SensorController: ");
+        synchronized (mLock) {
+            fout.println("      Active descriptors: ");
+            for (SensorDescriptor sensorDescriptor : mSensorDescriptors.values()) {
+                fout.println("        handle: " + sensorDescriptor.getHandle());
+                fout.println("          type: " + sensorDescriptor.getType());
+                fout.println("          name: " + sensorDescriptor.getName());
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void addSensorForTesting(IBinder deviceToken, int handle, int type, String name) {
+        synchronized (mLock) {
+            mSensorDescriptors.put(deviceToken,
+                    new SensorDescriptor(handle, type, name, () -> {}));
+        }
+    }
+
+    @VisibleForTesting
+    Map<IBinder, SensorDescriptor> getSensorDescriptors() {
+        synchronized (mLock) {
+            return mSensorDescriptors;
+        }
+    }
+
+    @VisibleForTesting
+    static final class SensorDescriptor {
+
+        private final int mHandle;
+        private final IBinder.DeathRecipient mDeathRecipient;
+        private final int mType;
+        private final String mName;
+
+        SensorDescriptor(int handle, int type, String name, IBinder.DeathRecipient deathRecipient) {
+            mHandle = handle;
+            mDeathRecipient = deathRecipient;
+            mType = type;
+            mName = name;
+        }
+        public int getHandle() {
+            return mHandle;
+        }
+        public int getType() {
+            return mType;
+        }
+        public String getName() {
+            return mName;
+        }
+        public IBinder.DeathRecipient getDeathRecipient() {
+            return mDeathRecipient;
+        }
+    }
+
+    private final class BinderDeathRecipient implements IBinder.DeathRecipient {
+        private final IBinder mDeviceToken;
+
+        BinderDeathRecipient(IBinder deviceToken) {
+            mDeviceToken = deviceToken;
+        }
+
+        @Override
+        public void binderDied() {
+            // All callers are expected to call {@link VirtualDevice#unregisterSensor} before
+            // quitting, which removes this death recipient. If this is invoked, the remote end
+            // died, or they disposed of the object without properly unregistering.
+            Slog.e(TAG, "Virtual sensor controller binder died");
+            unregisterSensor(mDeviceToken);
+        }
+    }
+
+    /** An internal exception that is thrown to indicate an error when opening a virtual sensor. */
+    private static class SensorCreationException extends Exception {
+        SensorCreationException(String message) {
+            super(message);
+        }
+        SensorCreationException(String message, Exception cause) {
+            super(message, cause);
+        }
+    }
+}
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 019f582..798ae0e 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -77,6 +77,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Consumer;
 
@@ -99,6 +100,7 @@
     private final int mOwnerUid;
     private final int mDeviceId;
     private final InputController mInputController;
+    private final SensorController mSensorController;
     private VirtualAudioController mVirtualAudioController;
     @VisibleForTesting
     final Set<Integer> mVirtualDisplayIds = new ArraySet<>();
@@ -161,6 +163,7 @@
                 ownerUid,
                 deviceId,
                 /* inputController= */ null,
+                /* sensorController= */ null,
                 listener,
                 pendingTrampolineCallback,
                 activityListener,
@@ -176,6 +179,7 @@
             int ownerUid,
             int deviceId,
             InputController inputController,
+            SensorController sensorController,
             OnDeviceCloseListener listener,
             PendingTrampolineCallback pendingTrampolineCallback,
             IVirtualDeviceActivityListener activityListener,
@@ -199,6 +203,11 @@
         } else {
             mInputController = inputController;
         }
+        if (sensorController == null) {
+            mSensorController = new SensorController(mVirtualDeviceLock, mDeviceId);
+        } else {
+            mSensorController = sensorController;
+        }
         mListener = listener;
         try {
             token.linkToDeath(this, 0);
@@ -320,11 +329,12 @@
         mListener.onClose(mAssociationInfo.getId());
         mAppToken.unlinkToDeath(this, 0);
 
-        final long token = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.close();
+            mSensorController.close();
         } finally {
-            Binder.restoreCallingIdentity(token);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -404,12 +414,12 @@
                                 + "this virtual device");
             }
         }
-        final long token = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createDpad(deviceName, vendorId, productId, deviceToken,
                     displayId);
         } finally {
-            Binder.restoreCallingIdentity(token);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -431,12 +441,12 @@
                                 + "this virtual device");
             }
         }
-        final long token = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createKeyboard(deviceName, vendorId, productId, deviceToken,
                     displayId);
         } finally {
-            Binder.restoreCallingIdentity(token);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -458,11 +468,11 @@
                                 + "virtual device");
             }
         }
-        final long token = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createMouse(deviceName, vendorId, productId, deviceToken, displayId);
         } finally {
-            Binder.restoreCallingIdentity(token);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -492,12 +502,12 @@
                             + screenSize);
         }
 
-        final long token = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createTouchscreen(deviceName, vendorId, productId,
                     deviceToken, displayId, screenSize);
         } finally {
-            Binder.restoreCallingIdentity(token);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -507,92 +517,92 @@
                 android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
                 "Permission required to unregister this input device");
 
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.unregisterInputDevice(token);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public int getInputDeviceId(IBinder token) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.getInputDeviceId(token);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
 
     @Override // Binder call
     public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendDpadKeyEvent(token, event);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendKeyEvent(token, event);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendButtonEvent(token, event);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendTouchEvent(token, event);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendRelativeEvent(token, event);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendScrollEvent(token, event);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
     public PointF getCursorPosition(IBinder token) {
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.getCursorPosition(token);
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -602,7 +612,7 @@
                 android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
                 "Permission required to unregister this input device");
 
-        final long binderToken = Binder.clearCallingIdentity();
+        final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mVirtualDeviceLock) {
                 mDefaultShowPointerIcon = showPointerIcon;
@@ -611,7 +621,7 @@
                 }
             }
         } finally {
-            Binder.restoreCallingIdentity(binderToken);
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -619,15 +629,43 @@
     public void createVirtualSensor(
             @NonNull IBinder deviceToken,
             @NonNull VirtualSensorConfig config) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to create a virtual sensor");
+        Objects.requireNonNull(config);
+        Objects.requireNonNull(deviceToken);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mSensorController.createSensor(deviceToken, config);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
     }
 
     @Override // Binder call
-    public void unregisterSensor(IBinder token) {
+    public void unregisterSensor(@NonNull IBinder token) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to unregister a virtual sensor");
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mSensorController.unregisterSensor(token);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
     }
 
     @Override // Binder call
-    public boolean sendSensorEvent(IBinder token, VirtualSensorEvent event) {
-        return true;
+    public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to send a virtual sensor event");
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return mSensorController.sendSensorEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
     }
 
     @Override
@@ -643,6 +681,7 @@
             fout.println("    mDefaultShowPointerIcon: " + mDefaultShowPointerIcon);
         }
         mInputController.dump(fout);
+        mSensorController.dump(fout);
     }
 
     GenericWindowPolicyController createWindowPolicyController() {
diff --git a/services/core/java/com/android/server/sensors/SensorManagerInternal.java b/services/core/java/com/android/server/sensors/SensorManagerInternal.java
index fbb6644..f17e5e7 100644
--- a/services/core/java/com/android/server/sensors/SensorManagerInternal.java
+++ b/services/core/java/com/android/server/sensors/SensorManagerInternal.java
@@ -43,6 +43,43 @@
     public abstract void removeProximityActiveListener(@NonNull ProximityActiveListener listener);
 
     /**
+     * Creates a sensor that is registered at runtime by the system with the sensor service.
+     *
+     * The runtime sensors created here are different from the
+     * <a href="https://source.android.com/docs/core/interaction/sensors/sensors-hal2#dynamic-sensors">
+     * dynamic sensor support in the HAL</a>. These sensors have no HAL dependency and correspond to
+     * sensors that belong to an external (virtual) device.
+     *
+     * @param deviceId The identifier of the device this sensor is associated with.
+     * @param type The generic type of the sensor.
+     * @param name The name of the sensor.
+     * @param vendor The vendor string of the sensor.
+     * @param callback The callback to get notified when the sensor listeners have changed.
+     * @return The sensor handle.
+     */
+    public abstract int createRuntimeSensor(int deviceId, int type, @NonNull String name,
+            @NonNull String vendor, @NonNull RuntimeSensorStateChangeCallback callback);
+
+    /**
+     * Unregisters the sensor with the given handle from the framework.
+     */
+    public abstract void removeRuntimeSensor(int handle);
+
+    /**
+     * Sends an event for the runtime sensor with the given handle to the framework.
+     *
+     * Only relevant for sending runtime sensor events. @see #createRuntimeSensor.
+     *
+     * @param handle The sensor handle.
+     * @param type The type of the sensor.
+     * @param timestampNanos When the event occurred.
+     * @param values The values of the event.
+     * @return Whether the event injection was successful.
+     */
+    public abstract boolean sendSensorEvent(int handle, int type, long timestampNanos,
+            @NonNull float[] values);
+
+    /**
      * Listener for proximity sensor state changes.
      */
     public interface ProximityActiveListener {
@@ -52,4 +89,17 @@
          */
         void onProximityActive(boolean isActive);
     }
+
+    /**
+     * Callback for runtime sensor state changes. Only relevant to sensors created via
+     * {@link #createRuntimeSensor}, i.e. the dynamic sensors created via the dynamic sensor HAL are
+     * not covered.
+     */
+    public interface RuntimeSensorStateChangeCallback {
+        /**
+         * Invoked when the listeners of the runtime sensor have changed.
+         */
+        void onStateChanged(boolean enabled, int samplingPeriodMicros,
+                int batchReportLatencyMicros);
+    }
 }
diff --git a/services/core/java/com/android/server/sensors/SensorService.java b/services/core/java/com/android/server/sensors/SensorService.java
index 8fe2d52..d8e3bdd 100644
--- a/services/core/java/com/android/server/sensors/SensorService.java
+++ b/services/core/java/com/android/server/sensors/SensorService.java
@@ -29,7 +29,9 @@
 import com.android.server.SystemService;
 import com.android.server.utils.TimingsTraceAndSlog;
 
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Future;
 
@@ -40,6 +42,8 @@
     private final ArrayMap<ProximityActiveListener, ProximityListenerProxy> mProximityListeners =
             new ArrayMap<>();
     @GuardedBy("mLock")
+    private final Set<Integer> mRuntimeSensorHandles = new HashSet<>();
+    @GuardedBy("mLock")
     private Future<?> mSensorServiceStart;
     @GuardedBy("mLock")
     private long mPtr;
@@ -51,6 +55,12 @@
     private static native void registerProximityActiveListenerNative(long ptr);
     private static native void unregisterProximityActiveListenerNative(long ptr);
 
+    private static native int registerRuntimeSensorNative(long ptr, int deviceId, int type,
+            String name, String vendor,
+            SensorManagerInternal.RuntimeSensorStateChangeCallback callback);
+    private static native void unregisterRuntimeSensorNative(long ptr, int handle);
+    private static native boolean sendRuntimeSensorEventNative(long ptr, int handle, int type,
+            long timestampNanos, float[] values);
 
     public SensorService(Context ctx) {
         super(ctx);
@@ -85,6 +95,38 @@
 
     class LocalService extends SensorManagerInternal {
         @Override
+        public int createRuntimeSensor(int deviceId, int type, @NonNull String name,
+                @NonNull String vendor, @NonNull RuntimeSensorStateChangeCallback callback) {
+            synchronized (mLock) {
+                int handle = registerRuntimeSensorNative(mPtr, deviceId, type, name, vendor,
+                        callback);
+                mRuntimeSensorHandles.add(handle);
+                return handle;
+            }
+        }
+
+        @Override
+        public void removeRuntimeSensor(int handle) {
+            synchronized (mLock) {
+                if (mRuntimeSensorHandles.contains(handle)) {
+                    mRuntimeSensorHandles.remove(handle);
+                    unregisterRuntimeSensorNative(mPtr, handle);
+                }
+            }
+        }
+
+        @Override
+        public boolean sendSensorEvent(int handle, int type, long timestampNanos,
+                @NonNull float[] values) {
+            synchronized (mLock) {
+                if (!mRuntimeSensorHandles.contains(handle)) {
+                    return false;
+                }
+                return sendRuntimeSensorEventNative(mPtr, handle, type, timestampNanos, values);
+            }
+        }
+
+        @Override
         public void addProximityActiveListener(@NonNull Executor executor,
                 @NonNull ProximityActiveListener listener) {
             Objects.requireNonNull(executor, "executor must not be null");
diff --git a/services/core/jni/com_android_server_sensor_SensorService.cpp b/services/core/jni/com_android_server_sensor_SensorService.cpp
index 63b7dfb..10d8b42 100644
--- a/services/core/jni/com_android_server_sensor_SensorService.cpp
+++ b/services/core/jni/com_android_server_sensor_SensorService.cpp
@@ -22,6 +22,7 @@
 #include <cutils/properties.h>
 #include <jni.h>
 #include <sensorservice/SensorService.h>
+#include <string.h>
 #include <utils/Log.h>
 #include <utils/misc.h>
 
@@ -30,10 +31,14 @@
 #define PROXIMITY_ACTIVE_CLASS \
     "com/android/server/sensors/SensorManagerInternal$ProximityActiveListener"
 
+#define RUNTIME_SENSOR_CALLBACK_CLASS \
+    "com/android/server/sensors/SensorManagerInternal$RuntimeSensorStateChangeCallback"
+
 namespace android {
 
 static JavaVM* sJvm = nullptr;
 static jmethodID sMethodIdOnProximityActive;
+static jmethodID sMethodIdOnStateChanged;
 
 class NativeSensorService {
 public:
@@ -41,6 +46,11 @@
 
     void registerProximityActiveListener();
     void unregisterProximityActiveListener();
+    jint registerRuntimeSensor(JNIEnv* env, jint deviceId, jint type, jstring name, jstring vendor,
+                               jobject callback);
+    void unregisterRuntimeSensor(jint handle);
+    jboolean sendRuntimeSensorEvent(JNIEnv* env, jint handle, jint type, jlong timestamp,
+                                    jfloatArray values);
 
 private:
     sp<SensorService> mService;
@@ -56,6 +66,18 @@
         jobject mListener;
     };
     sp<ProximityActiveListenerDelegate> mProximityActiveListenerDelegate;
+
+    class RuntimeSensorCallbackDelegate : public SensorService::RuntimeSensorStateChangeCallback {
+    public:
+        RuntimeSensorCallbackDelegate(JNIEnv* env, jobject callback);
+        ~RuntimeSensorCallbackDelegate();
+
+        void onStateChanged(bool enabled, int64_t samplingPeriodNs,
+                            int64_t batchReportLatencyNs) override;
+
+    private:
+        jobject mCallback;
+    };
 };
 
 NativeSensorService::NativeSensorService(JNIEnv* env, jobject listener)
@@ -85,6 +107,109 @@
     mService->removeProximityActiveListener(mProximityActiveListenerDelegate);
 }
 
+jint NativeSensorService::registerRuntimeSensor(JNIEnv* env, jint deviceId, jint type, jstring name,
+                                                jstring vendor, jobject callback) {
+    if (mService == nullptr) {
+        ALOGD("Dropping registerRuntimeSensor, sensor service not available.");
+        return -1;
+    }
+
+    sensor_t sensor{
+            .name = env->GetStringUTFChars(name, 0),
+            .vendor = env->GetStringUTFChars(vendor, 0),
+            .version = sizeof(sensor_t),
+            .type = type,
+    };
+
+    sp<RuntimeSensorCallbackDelegate> callbackDelegate(
+            new RuntimeSensorCallbackDelegate(env, callback));
+    return mService->registerRuntimeSensor(sensor, deviceId, callbackDelegate);
+}
+
+void NativeSensorService::unregisterRuntimeSensor(jint handle) {
+    if (mService == nullptr) {
+        ALOGD("Dropping unregisterProximityActiveListener, sensor service not available.");
+        return;
+    }
+
+    mService->unregisterRuntimeSensor(handle);
+}
+
+jboolean NativeSensorService::sendRuntimeSensorEvent(JNIEnv* env, jint handle, jint type,
+                                                     jlong timestamp, jfloatArray values) {
+    if (mService == nullptr) {
+        ALOGD("Dropping sendRuntimeSensorEvent, sensor service not available.");
+        return false;
+    }
+    if (values == nullptr) {
+        ALOGD("Dropping sendRuntimeSensorEvent, no values.");
+        return false;
+    }
+
+    sensors_event_t event{
+            .version = sizeof(sensors_event_t),
+            .timestamp = timestamp,
+            .sensor = handle,
+            .type = type,
+    };
+
+    int valuesLength = env->GetArrayLength(values);
+    jfloat* sensorValues = env->GetFloatArrayElements(values, nullptr);
+
+    switch (type) {
+        case SENSOR_TYPE_ACCELEROMETER:
+        case SENSOR_TYPE_MAGNETIC_FIELD:
+        case SENSOR_TYPE_ORIENTATION:
+        case SENSOR_TYPE_GYROSCOPE:
+        case SENSOR_TYPE_GRAVITY:
+        case SENSOR_TYPE_LINEAR_ACCELERATION: {
+            if (valuesLength != 3) {
+                ALOGD("Dropping sendRuntimeSensorEvent, wrong number of values.");
+                return false;
+            }
+            event.acceleration.x = sensorValues[0];
+            event.acceleration.y = sensorValues[1];
+            event.acceleration.z = sensorValues[2];
+            break;
+        }
+        case SENSOR_TYPE_DEVICE_ORIENTATION:
+        case SENSOR_TYPE_LIGHT:
+        case SENSOR_TYPE_PRESSURE:
+        case SENSOR_TYPE_TEMPERATURE:
+        case SENSOR_TYPE_PROXIMITY:
+        case SENSOR_TYPE_RELATIVE_HUMIDITY:
+        case SENSOR_TYPE_AMBIENT_TEMPERATURE:
+        case SENSOR_TYPE_SIGNIFICANT_MOTION:
+        case SENSOR_TYPE_STEP_DETECTOR:
+        case SENSOR_TYPE_TILT_DETECTOR:
+        case SENSOR_TYPE_WAKE_GESTURE:
+        case SENSOR_TYPE_GLANCE_GESTURE:
+        case SENSOR_TYPE_PICK_UP_GESTURE:
+        case SENSOR_TYPE_WRIST_TILT_GESTURE:
+        case SENSOR_TYPE_STATIONARY_DETECT:
+        case SENSOR_TYPE_MOTION_DETECT:
+        case SENSOR_TYPE_HEART_BEAT:
+        case SENSOR_TYPE_LOW_LATENCY_OFFBODY_DETECT: {
+            if (valuesLength != 1) {
+                ALOGD("Dropping sendRuntimeSensorEvent, wrong number of values.");
+                return false;
+            }
+            event.data[0] = sensorValues[0];
+            break;
+        }
+        default: {
+            if (valuesLength > 16) {
+                ALOGD("Dropping sendRuntimeSensorEvent, number of values exceeds the maximum.");
+                return false;
+            }
+            memcpy(event.data, sensorValues, valuesLength * sizeof(float));
+        }
+    }
+
+    status_t err = mService->sendRuntimeSensorEvent(event);
+    return err == OK;
+}
+
 NativeSensorService::ProximityActiveListenerDelegate::ProximityActiveListenerDelegate(
         JNIEnv* env, jobject listener)
       : mListener(env->NewGlobalRef(listener)) {}
@@ -98,6 +223,22 @@
     jniEnv->CallVoidMethod(mListener, sMethodIdOnProximityActive, static_cast<jboolean>(isActive));
 }
 
+NativeSensorService::RuntimeSensorCallbackDelegate::RuntimeSensorCallbackDelegate(JNIEnv* env,
+                                                                                  jobject callback)
+      : mCallback(env->NewGlobalRef(callback)) {}
+
+NativeSensorService::RuntimeSensorCallbackDelegate::~RuntimeSensorCallbackDelegate() {
+    AndroidRuntime::getJNIEnv()->DeleteGlobalRef(mCallback);
+}
+
+void NativeSensorService::RuntimeSensorCallbackDelegate::onStateChanged(
+        bool enabled, int64_t samplingPeriodNs, int64_t batchReportLatencyNs) {
+    auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
+    jniEnv->CallVoidMethod(mCallback, sMethodIdOnStateChanged, static_cast<jboolean>(enabled),
+                           static_cast<jint>(ns2us(samplingPeriodNs)),
+                           static_cast<jint>(ns2us(batchReportLatencyNs)));
+}
+
 static jlong startSensorServiceNative(JNIEnv* env, jclass, jobject listener) {
     NativeSensorService* service = new NativeSensorService(env, listener);
     return reinterpret_cast<jlong>(service);
@@ -113,26 +254,46 @@
     service->unregisterProximityActiveListener();
 }
 
-static const JNINativeMethod methods[] = {
-        {
-                "startSensorServiceNative", "(L" PROXIMITY_ACTIVE_CLASS ";)J",
-                reinterpret_cast<void*>(startSensorServiceNative)
-        },
-        {
-                "registerProximityActiveListenerNative", "(J)V",
-                reinterpret_cast<void*>(registerProximityActiveListenerNative)
-        },
-        {
-                "unregisterProximityActiveListenerNative", "(J)V",
-                reinterpret_cast<void*>(unregisterProximityActiveListenerNative)
-         },
+static jint registerRuntimeSensorNative(JNIEnv* env, jclass, jlong ptr, jint deviceId, jint type,
+                                        jstring name, jstring vendor, jobject callback) {
+    auto* service = reinterpret_cast<NativeSensorService*>(ptr);
+    return service->registerRuntimeSensor(env, deviceId, type, name, vendor, callback);
+}
 
+static void unregisterRuntimeSensorNative(JNIEnv* env, jclass, jlong ptr, jint handle) {
+    auto* service = reinterpret_cast<NativeSensorService*>(ptr);
+    service->unregisterRuntimeSensor(handle);
+}
+
+static jboolean sendRuntimeSensorEventNative(JNIEnv* env, jclass, jlong ptr, jint handle, jint type,
+                                             jlong timestamp, jfloatArray values) {
+    auto* service = reinterpret_cast<NativeSensorService*>(ptr);
+    return service->sendRuntimeSensorEvent(env, handle, type, timestamp, values);
+}
+
+static const JNINativeMethod methods[] = {
+        {"startSensorServiceNative", "(L" PROXIMITY_ACTIVE_CLASS ";)J",
+         reinterpret_cast<void*>(startSensorServiceNative)},
+        {"registerProximityActiveListenerNative", "(J)V",
+         reinterpret_cast<void*>(registerProximityActiveListenerNative)},
+        {"unregisterProximityActiveListenerNative", "(J)V",
+         reinterpret_cast<void*>(unregisterProximityActiveListenerNative)},
+        {"registerRuntimeSensorNative",
+         "(JIILjava/lang/String;Ljava/lang/String;L" RUNTIME_SENSOR_CALLBACK_CLASS ";)I",
+         reinterpret_cast<void*>(registerRuntimeSensorNative)},
+        {"unregisterRuntimeSensorNative", "(JI)V",
+         reinterpret_cast<void*>(unregisterRuntimeSensorNative)},
+        {"sendRuntimeSensorEventNative", "(JIIJ[F)Z",
+         reinterpret_cast<void*>(sendRuntimeSensorEventNative)},
 };
 
 int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env) {
     sJvm = vm;
     jclass listenerClass = FindClassOrDie(env, PROXIMITY_ACTIVE_CLASS);
     sMethodIdOnProximityActive = GetMethodIDOrDie(env, listenerClass, "onProximityActive", "(Z)V");
+    jclass runtimeSensorCallbackClass = FindClassOrDie(env, RUNTIME_SENSOR_CALLBACK_CLASS);
+    sMethodIdOnStateChanged =
+            GetMethodIDOrDie(env, runtimeSensorCallbackClass, "onStateChanged", "(ZII)V");
     return jniRegisterNativeMethods(env, "com/android/server/sensors/SensorService", methods,
                                     NELEM(methods));
 }
diff --git a/services/proguard.flags b/services/proguard.flags
index 27fe505..6cdf11c 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -88,6 +88,7 @@
 -keep,allowoptimization,allowaccessmodification class com.android.server.location.gnss.GnssPowerStats { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.location.gnss.hal.GnssNative { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.pm.PackageManagerShellCommandDataLoader { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.sensors.SensorManagerInternal$RuntimeSensorStateChangeCallback { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.sensors.SensorManagerInternal$ProximityActiveListener { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.sensors.SensorService { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareImpl$AudioSessionProvider$AudioSession { *; }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.java
new file mode 100644
index 0000000..ef8a49f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.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 com.android.server.companion.virtual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.companion.virtual.sensor.VirtualSensorConfig;
+import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.hardware.Sensor;
+import android.os.Binder;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.server.LocalServices;
+import com.android.server.sensors.SensorManagerInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class SensorControllerTest {
+
+    private static final int VIRTUAL_DEVICE_ID = 42;
+    private static final String VIRTUAL_SENSOR_NAME = "VirtualAccelerometer";
+    private static final int SENSOR_HANDLE = 7;
+
+    @Mock
+    private SensorManagerInternal mSensorManagerInternalMock;
+    private SensorController mSensorController;
+    private VirtualSensorEvent mSensorEvent;
+    private VirtualSensorConfig mVirtualSensorConfig;
+    private IBinder mSensorToken;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        LocalServices.removeServiceForTest(SensorManagerInternal.class);
+        LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock);
+
+        mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID);
+        mSensorEvent = new VirtualSensorEvent.Builder(new float[] { 1f, 2f, 3f}).build();
+        mVirtualSensorConfig =
+                new VirtualSensorConfig.Builder(Sensor.TYPE_ACCELEROMETER, VIRTUAL_SENSOR_NAME)
+                        .build();
+        mSensorToken = new Binder("sensorToken");
+    }
+
+    @Test
+    public void createSensor_invalidHandle_throwsException() {
+        doReturn(/* handle= */0).when(mSensorManagerInternalMock).createRuntimeSensor(
+                anyInt(), anyInt(), anyString(), anyString(), any());
+
+        Throwable thrown = assertThrows(
+                RuntimeException.class,
+                () -> mSensorController.createSensor(mSensorToken, mVirtualSensorConfig));
+
+        assertThat(thrown.getCause().getMessage())
+                .contains("Received an invalid virtual sensor handle");
+    }
+
+    @Test
+    public void createSensor_success() {
+        doCreateSensorSuccessfully();
+
+        assertThat(mSensorController.getSensorDescriptors()).isNotEmpty();
+    }
+
+    @Test
+    public void sendSensorEvent_invalidToken_throwsException() {
+        doCreateSensorSuccessfully();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mSensorController.sendSensorEvent(
+                        new Binder("invalidSensorToken"), mSensorEvent));
+    }
+
+    @Test
+    public void sendSensorEvent_success() {
+        doCreateSensorSuccessfully();
+
+        mSensorController.sendSensorEvent(mSensorToken, mSensorEvent);
+        verify(mSensorManagerInternalMock).sendSensorEvent(
+                SENSOR_HANDLE, Sensor.TYPE_ACCELEROMETER, mSensorEvent.getTimestampNanos(),
+                mSensorEvent.getValues());
+    }
+
+    @Test
+    public void unregisterSensor_invalidToken_throwsException() {
+        doCreateSensorSuccessfully();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mSensorController.unregisterSensor(new Binder("invalidSensorToken")));
+    }
+
+    @Test
+    public void unregisterSensor_success() {
+        doCreateSensorSuccessfully();
+
+        mSensorController.unregisterSensor(mSensorToken);
+        verify(mSensorManagerInternalMock).removeRuntimeSensor(SENSOR_HANDLE);
+        assertThat(mSensorController.getSensorDescriptors()).isEmpty();
+    }
+
+    private void doCreateSensorSuccessfully() {
+        doReturn(SENSOR_HANDLE).when(mSensorManagerInternalMock).createRuntimeSensor(
+                anyInt(), anyInt(), anyString(), anyString(), any());
+        mSensorController.createSensor(mSensorToken, mVirtualSensorConfig);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 09dc367..41700ab 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -51,6 +51,7 @@
 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.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -58,6 +59,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.graphics.Point;
+import android.hardware.Sensor;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.IInputManager;
 import android.hardware.input.VirtualKeyEvent;
@@ -88,6 +90,7 @@
 import com.android.internal.app.BlockedAppStreamingActivity;
 import com.android.server.LocalServices;
 import com.android.server.input.InputManagerInternal;
+import com.android.server.sensors.SensorManagerInternal;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -125,16 +128,19 @@
     private static final int VENDOR_ID = 5;
     private static final String UNIQUE_ID = "uniqueid";
     private static final String PHYS = "phys";
-    private static final int DEVICE_ID = 42;
+    private static final int DEVICE_ID = 53;
     private static final int HEIGHT = 1800;
     private static final int WIDTH = 900;
+    private static final int SENSOR_HANDLE = 64;
     private static final Binder BINDER = new Binder("binder");
     private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
+    private static final int VIRTUAL_DEVICE_ID =  42;
 
     private Context mContext;
     private InputManagerMockHelper mInputManagerMockHelper;
     private VirtualDeviceImpl mDeviceImpl;
     private InputController mInputController;
+    private SensorController mSensorController;
     private AssociationInfo mAssociationInfo;
     private VirtualDeviceManagerService mVdms;
     private VirtualDeviceManagerInternal mLocalService;
@@ -149,6 +155,8 @@
     @Mock
     private InputManagerInternal mInputManagerInternalMock;
     @Mock
+    private SensorManagerInternal mSensorManagerInternalMock;
+    @Mock
     private IVirtualDeviceActivityListener mActivityListener;
     @Mock
     private Consumer<ArraySet<Integer>> mRunningAppsChangedCallback;
@@ -205,6 +213,9 @@
         LocalServices.removeServiceForTest(InputManagerInternal.class);
         LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
 
+        LocalServices.removeServiceForTest(SensorManagerInternal.class);
+        LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock);
+
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.uniqueId = UNIQUE_ID;
         doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt());
@@ -229,6 +240,7 @@
         mInputController = new InputController(new Object(), mNativeWrapperMock,
                 new Handler(TestableLooper.get(this).getLooper()),
                 mContext.getSystemService(WindowManager.class), threadVerifier);
+        mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID);
 
         mAssociationInfo = new AssociationInfo(1, 0, null,
                 MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0);
@@ -241,9 +253,9 @@
                 .setBlockedActivities(getBlockedActivities())
                 .build();
         mDeviceImpl = new VirtualDeviceImpl(mContext,
-                mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
-                mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
-                mActivityListener, mRunningAppsChangedCallback, params);
+                mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID,
+                mInputController, mSensorController, (int associationId) -> {},
+                mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params);
         mVdms.addVirtualDevice(mDeviceImpl);
     }
 
@@ -285,9 +297,9 @@
                 .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM)
                 .build();
         mDeviceImpl = new VirtualDeviceImpl(mContext,
-                mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
-                mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
-                mActivityListener, mRunningAppsChangedCallback, params);
+                mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID,
+                mInputController, mSensorController, (int associationId) -> {},
+                mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params);
         mVdms.addVirtualDevice(mDeviceImpl);
 
         assertThat(
@@ -552,6 +564,18 @@
     }
 
     @Test
+    public void createVirtualSensor_noPermission_failsSecurityException() {
+        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualSensor(
+                        BINDER,
+                        new VirtualSensorConfig.Builder(
+                                Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+    }
+
+    @Test
     public void onAudioSessionStarting_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
         doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
@@ -655,6 +679,17 @@
     }
 
     @Test
+    public void close_cleanSensorController() {
+        mSensorController.addSensorForTesting(
+                BINDER, SENSOR_HANDLE, Sensor.TYPE_ACCELEROMETER, DEVICE_NAME);
+
+        mDeviceImpl.close();
+
+        assertThat(mSensorController.getSensorDescriptors()).isEmpty();
+        verify(mSensorManagerInternalMock).removeRuntimeSensor(SENSOR_HANDLE);
+    }
+
+    @Test
     public void sendKeyEvent_noFd() {
         assertThrows(
                 IllegalArgumentException.class,