Merge "A bunch of public VDM APIs for exposing VD properties." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 2e81528..0e5a515 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9681,14 +9681,24 @@
   public final class VirtualDevice implements android.os.Parcelable {
     method public int describeContents();
     method public int getDeviceId();
+    method @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) @NonNull public int[] getDisplayIds();
     method @Nullable public String getName();
     method @Nullable public String getPersistentDeviceId();
+    method @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public boolean hasCustomSensorSupport();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.VirtualDevice> CREATOR;
   }
 
   public final class VirtualDeviceManager {
+    method @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) @Nullable public android.companion.virtual.VirtualDevice getVirtualDevice(int);
     method @NonNull public java.util.List<android.companion.virtual.VirtualDevice> getVirtualDevices();
+    method @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public void registerVirtualDeviceListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.VirtualDeviceListener);
+    method @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public void unregisterVirtualDeviceListener(@NonNull android.companion.virtual.VirtualDeviceManager.VirtualDeviceListener);
+  }
+
+  @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public static interface VirtualDeviceManager.VirtualDeviceListener {
+    method public default void onVirtualDeviceClosed(int);
+    method public default void onVirtualDeviceCreated(int);
   }
 
 }
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index be699f4..c58561d 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -64,6 +64,16 @@
     String getPersistentDeviceId();
 
     /**
+     * Returns the IDs of all virtual displays of this device.
+     */
+    int[] getDisplayIds();
+
+    /**
+     * Returns the device policy for the given policy type.
+     */
+    int getDevicePolicy(int policyType);
+
+    /**
      * Closes the virtual device and frees all associated resources.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/IVirtualDeviceListener.aidl b/core/java/android/companion/virtual/IVirtualDeviceListener.aidl
new file mode 100644
index 0000000..c6dd227
--- /dev/null
+++ b/core/java/android/companion/virtual/IVirtualDeviceListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual;
+
+/**
+ * Interface to listen for changes in the available virtual devices.
+ *
+ * @hide
+ */
+oneway interface IVirtualDeviceListener {
+
+    /**
+     * Called whenever a new virtual device has been added to the system.
+     */
+    void onVirtualDeviceCreated(int deviceId);
+
+    /**
+     * Called whenever a virtual device has been removed from the system.
+     */
+    void onVirtualDeviceClosed(int deviceId);
+}
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index ed8484f..b665036 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -18,6 +18,7 @@
 
 import android.companion.virtual.IVirtualDevice;
 import android.companion.virtual.IVirtualDeviceActivityListener;
+import android.companion.virtual.IVirtualDeviceListener;
 import android.companion.virtual.IVirtualDeviceSoundEffectListener;
 import android.companion.virtual.VirtualDevice;
 import android.companion.virtual.VirtualDeviceParams;
@@ -56,12 +57,27 @@
      */
     List<VirtualDevice> getVirtualDevices();
 
-   /**
+    /**
+     * Returns the details of the virtual device with the given ID, if any.
+     */
+    VirtualDevice getVirtualDevice(int deviceId);
+
+    /**
+     * Registers a virtual device listener to receive notifications for virtual device events.
+     */
+    void registerVirtualDeviceListener(in IVirtualDeviceListener listener);
+
+    /**
+     * Unregisters a previously registered virtual device listener.
+     */
+    void unregisterVirtualDeviceListener(in IVirtualDeviceListener listener);
+
+    /**
      * Returns the ID of the device which owns the display with the given ID.
      */
     int getDeviceIdForDisplayId(int displayId);
 
-   /**
+    /**
      * Checks whether the passed {@code deviceId} is a valid virtual device ID or not.
      * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default
      * device which is not a virtual device. {@code deviceId} must correspond to a virtual device
diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java
index ceaf7e4..4692f92 100644
--- a/core/java/android/companion/virtual/VirtualDevice.java
+++ b/core/java/android/companion/virtual/VirtualDevice.java
@@ -16,13 +16,17 @@
 
 package android.companion.virtual;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.companion.virtual.flags.Flags;
 import android.content.Context;
 import android.os.Parcel;
 import android.os.Parcelable;
-
-import java.util.Objects;
+import android.os.RemoteException;
 
 /**
  * Details of a particular virtual device.
@@ -31,9 +35,12 @@
  *
  * <p class="note">Not to be confused with {@link VirtualDeviceManager.VirtualDevice}, which is used
  * by the virtual device creator and allows them to manage the device.
+ *
+ * @see VirtualDeviceManager#registerVirtualDeviceListener
  */
 public final class VirtualDevice implements Parcelable {
 
+    private final @NonNull IVirtualDevice mVirtualDevice;
     private final int mId;
     private final @Nullable String mPersistentId;
     private final @Nullable String mName;
@@ -44,17 +51,20 @@
      *
      * @hide
      */
-    public VirtualDevice(int id, @Nullable String persistentId, @Nullable String name) {
+    public VirtualDevice(@NonNull IVirtualDevice virtualDevice, int id,
+            @Nullable String persistentId, @Nullable String name) {
         if (id <= Context.DEVICE_ID_DEFAULT) {
             throw new IllegalArgumentException("VirtualDevice ID must be greater than "
                     + Context.DEVICE_ID_DEFAULT);
         }
+        mVirtualDevice = virtualDevice;
         mId = id;
         mPersistentId = persistentId;
         mName = name;
     }
 
     private VirtualDevice(@NonNull Parcel parcel) {
+        mVirtualDevice = IVirtualDevice.Stub.asInterface(parcel.readStrongBinder());
         mId = parcel.readInt();
         mPersistentId = parcel.readString8();
         mName = parcel.readString8();
@@ -101,6 +111,40 @@
         return mName;
     }
 
+    /**
+     * Returns the IDs of all virtual displays that belong to this device, if any.
+     *
+     * <p>The actual {@link android.view.Display} objects can be obtained by passing the returned
+     * IDs to {@link android.hardware.display.DisplayManager#getDisplay(int)}.</p>
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public @NonNull int[] getDisplayIds() {
+        try {
+            return mVirtualDevice.getDisplayIds();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns whether this device may have custom sensors.
+     *
+     * <p>Returning {@code true} does not necessarily mean that this device has sensors, it only
+     * means that a {@link android.hardware.SensorManager} instance created from a {@link Context}
+     * associated with this device will return this device's sensors, if any.</p>
+     *
+     * @see Context#getDeviceId()
+     * @see Context#createDeviceContext(int)
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public boolean hasCustomSensorSupport() {
+        try {
+            return mVirtualDevice.getDevicePolicy(POLICY_TYPE_SENSORS) == DEVICE_POLICY_CUSTOM;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -108,31 +152,13 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mVirtualDevice.asBinder());
         dest.writeInt(mId);
         dest.writeString8(mPersistentId);
         dest.writeString8(mName);
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!(o instanceof VirtualDevice)) {
-            return false;
-        }
-        VirtualDevice that = (VirtualDevice) o;
-        return mId == that.mId
-                && Objects.equals(mPersistentId, that.mPersistentId)
-                && Objects.equals(mName, that.mName);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mId, mPersistentId, mName);
-    }
-
-    @Override
     @NonNull
     public String toString() {
         return "VirtualDevice("
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 923e689..29b0ff3 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -55,11 +55,13 @@
 import android.hardware.input.VirtualTouchscreen;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.media.AudioManager;
+import android.os.Binder;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.Surface;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.AnnotationValidations;
 
 import java.lang.annotation.ElementType;
@@ -67,6 +69,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -147,6 +150,9 @@
     private final IVirtualDeviceManager mService;
     private final Context mContext;
 
+    @GuardedBy("mVirtualDeviceListeners")
+    private final List<VirtualDeviceListenerDelegate> mVirtualDeviceListeners = new ArrayList<>();
+
     /** @hide */
     public VirtualDeviceManager(
             @Nullable IVirtualDeviceManager service, @NonNull Context context) {
@@ -207,6 +213,88 @@
     }
 
     /**
+     * Returns the details of the virtual device with the given ID, if any.
+     *
+     * <p>The returned object is a read-only representation of the virtual device that expose its
+     * properties.</p>
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    @Nullable
+    public android.companion.virtual.VirtualDevice getVirtualDevice(int deviceId) {
+        if (mService == null) {
+            Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service.");
+            return null;
+        }
+        if (deviceId == Context.DEVICE_ID_INVALID || deviceId == Context.DEVICE_ID_DEFAULT) {
+            return null;  // Don't even bother making a Binder call.
+        }
+        try {
+            return mService.getVirtualDevice(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers a virtual device listener to receive notifications when virtual devices are created
+     * or closed.
+     *
+     * @param executor The executor where the listener is executed on.
+     * @param listener The listener to add.
+     * @see #unregisterVirtualDeviceListener
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public void registerVirtualDeviceListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull VirtualDeviceListener listener) {
+        if (mService == null) {
+            Log.w(TAG, "Failed to register listener; no virtual device manager service.");
+            return;
+        }
+        final VirtualDeviceListenerDelegate delegate =
+                new VirtualDeviceListenerDelegate(Objects.requireNonNull(executor),
+                        Objects.requireNonNull(listener));
+        synchronized (mVirtualDeviceListeners) {
+            try {
+                mService.registerVirtualDeviceListener(delegate);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mVirtualDeviceListeners.add(delegate);
+        }
+    }
+
+    /**
+     * Unregisters a virtual device listener previously registered with
+     * {@link #registerVirtualDeviceListener}.
+     *
+     * @param listener The listener to unregister.
+     * @see #registerVirtualDeviceListener
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public void unregisterVirtualDeviceListener(@NonNull VirtualDeviceListener listener) {
+        if (mService == null) {
+            Log.w(TAG, "Failed to unregister listener; no virtual device manager service.");
+            return;
+        }
+        Objects.requireNonNull(listener);
+        synchronized (mVirtualDeviceListeners) {
+            final Iterator<VirtualDeviceListenerDelegate> it = mVirtualDeviceListeners.iterator();
+            while (it.hasNext()) {
+                final VirtualDeviceListenerDelegate delegate = it.next();
+                if (delegate.mListener == listener) {
+                    try {
+                        mService.unregisterVirtualDeviceListener(delegate);
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                    it.remove();
+                }
+            }
+        }
+    }
+
+    /**
      * Returns the device policy for the given virtual device and policy type.
      *
      * <p>In case the virtual device identifier is not valid, or there's no explicitly specified
@@ -748,7 +836,7 @@
          *
          * @param executor The executor where the listener is executed on.
          * @param soundEffectListener The listener to add.
-         * @see #removeActivityListener(ActivityListener)
+         * @see #removeSoundEffectListener(SoundEffectListener)
          */
         public void addSoundEffectListener(@CallbackExecutor @NonNull Executor executor,
                 @NonNull SoundEffectListener soundEffectListener) {
@@ -877,4 +965,59 @@
          */
         void onPlaySoundEffect(@AudioManager.SystemSoundEffect int effectType);
     }
+
+    /**
+     * Listener for changes in the available virtual devices.
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public interface VirtualDeviceListener {
+        /**
+         * Called whenever a new virtual device has been added to the system.
+         * Use {@link VirtualDeviceManager#getVirtualDevice(int)} to get more information about
+         * the device.
+         *
+         * @param deviceId The id of the virtual device that was added.
+         */
+        default void onVirtualDeviceCreated(int deviceId) {}
+
+        /**
+         * Called whenever a virtual device has been removed from the system.
+         *
+         * @param deviceId The id of the virtual device that was removed.
+         */
+        default void onVirtualDeviceClosed(int deviceId) {}
+    }
+
+    /**
+     * A wrapper for {@link VirtualDeviceListener} that executes callbacks on the given executor.
+     */
+    private static class VirtualDeviceListenerDelegate extends IVirtualDeviceListener.Stub {
+        private final VirtualDeviceListener mListener;
+        private final Executor mExecutor;
+
+        private VirtualDeviceListenerDelegate(Executor executor, VirtualDeviceListener listener) {
+            mExecutor = executor;
+            mListener = listener;
+        }
+
+        @Override
+        public void onVirtualDeviceCreated(int deviceId) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mListener.onVirtualDeviceCreated(deviceId));
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void onVirtualDeviceClosed(int deviceId) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mListener.onVirtualDeviceClosed(deviceId));
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
 }
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 057b856..9ab3be6 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -13,3 +13,10 @@
   description: "Enable dynamic policy API"
   bug: "298401780"
 }
+
+flag {
+  name: "vdm_public_apis"
+  namespace: "virtual_devices"
+  description: "Enable public VDM API for device capabilities"
+  bug: "297253526"
+}
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 2b97bbb1..8f765e4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -41,6 +41,7 @@
 import android.companion.virtual.IVirtualDeviceActivityListener;
 import android.companion.virtual.IVirtualDeviceIntentInterceptor;
 import android.companion.virtual.IVirtualDeviceSoundEffectListener;
+import android.companion.virtual.VirtualDevice;
 import android.companion.virtual.VirtualDeviceManager;
 import android.companion.virtual.VirtualDeviceManager.ActivityListener;
 import android.companion.virtual.VirtualDeviceParams;
@@ -170,6 +171,9 @@
     @Nullable
     private LocaleList mLocaleList = null;
 
+    @NonNull
+    private final VirtualDevice mPublicVirtualDeviceObject;
+
     private ActivityListener createListenerAdapter() {
         return new ActivityListener() {
 
@@ -288,6 +292,9 @@
             throw e.rethrowFromSystemServer();
         }
         mVirtualDeviceLog.logCreated(deviceId, mOwnerUid);
+
+        mPublicVirtualDeviceObject = new VirtualDevice(
+                this, getDeviceId(), getPersistentDeviceId(), mParams.getName());
     }
 
     @VisibleForTesting
@@ -317,9 +324,9 @@
         return mAssociationInfo.getDisplayName();
     }
 
-    /** Returns the optional name of the device. */
-    String getDeviceName() {
-        return mParams.getName();
+    /** Returns the public representation of the device. */
+    VirtualDevice getPublicVirtualDeviceObject() {
+        return mPublicVirtualDeviceObject;
     }
 
     /** Returns the locale of the device. */
@@ -329,7 +336,7 @@
         }
     }
 
-    /** Returns the policy specified for this policy type */
+    @Override  // Binder call
     public @VirtualDeviceParams.DevicePolicy int getDevicePolicy(
             @VirtualDeviceParams.PolicyType int policyType) {
         if (Flags.dynamicPolicy()) {
@@ -756,8 +763,10 @@
             synchronized (mVirtualDeviceLock) {
                 mDefaultShowPointerIcon = showPointerIcon;
             }
-            getDisplayIds().forEach(
-                    displayId -> mInputController.setShowPointerIcon(showPointerIcon, displayId));
+            final int[] displayIds = getDisplayIds();
+            for (int i = 0; i < displayIds.length; ++i) {
+                mInputController.setShowPointerIcon(showPointerIcon, displayIds[i]);
+            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -1029,14 +1038,15 @@
         return mOwnerUid;
     }
 
-    ArraySet<Integer> getDisplayIds() {
+    @Override  // Binder call
+    public int[] getDisplayIds() {
         synchronized (mVirtualDeviceLock) {
             final int size = mVirtualDisplays.size();
-            ArraySet<Integer> arraySet = new ArraySet<>(size);
+            int[] displayIds = new int[size];
             for (int i = 0; i < size; i++) {
-                arraySet.append(mVirtualDisplays.keyAt(i));
+                displayIds[i] = mVirtualDisplays.keyAt(i);
             }
-            return arraySet;
+            return displayIds;
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 4da9298..cfe56e9 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -30,6 +30,7 @@
 import android.companion.CompanionDeviceManager;
 import android.companion.virtual.IVirtualDevice;
 import android.companion.virtual.IVirtualDeviceActivityListener;
+import android.companion.virtual.IVirtualDeviceListener;
 import android.companion.virtual.IVirtualDeviceManager;
 import android.companion.virtual.IVirtualDeviceSoundEffectListener;
 import android.companion.virtual.VirtualDevice;
@@ -49,6 +50,7 @@
 import android.os.Looper;
 import android.os.Parcel;
 import android.os.Process;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArraySet;
@@ -70,6 +72,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -77,6 +80,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 
 @SuppressLint("LongLogTag")
@@ -103,6 +107,9 @@
                 }
             };
 
+    private final RemoteCallbackList<IVirtualDeviceListener> mVirtualDeviceListeners =
+            new RemoteCallbackList<>();
+
     /**
      * Mapping from device IDs to virtual devices.
      */
@@ -225,6 +232,17 @@
             mVirtualDevices.remove(deviceId);
         }
 
+        if (Flags.vdmPublicApis()) {
+            mVirtualDeviceListeners.broadcast(listener -> {
+                try {
+                    listener.onVirtualDeviceClosed(deviceId);
+                } catch (RemoteException e) {
+                    Slog.i(TAG, "Failed to invoke onVirtualDeviceClosed listener: "
+                            + e.getMessage());
+                }
+            });
+        }
+
         Intent i = new Intent(VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED);
         i.putExtra(VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, deviceId);
         i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -376,6 +394,17 @@
                 }
                 mVirtualDevices.put(deviceId, virtualDevice);
             }
+
+            if (Flags.vdmPublicApis()) {
+                mVirtualDeviceListeners.broadcast(listener -> {
+                    try {
+                        listener.onVirtualDeviceCreated(deviceId);
+                    } catch (RemoteException e) {
+                        Slog.i(TAG, "Failed to invoke onVirtualDeviceCreated listener: "
+                                + e.getMessage());
+                    }
+                });
+            }
             return virtualDevice;
         }
 
@@ -415,14 +444,31 @@
             synchronized (mVirtualDeviceManagerLock) {
                 for (int i = 0; i < mVirtualDevices.size(); i++) {
                     final VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
-                    virtualDevices.add(
-                            new VirtualDevice(device.getDeviceId(), device.getPersistentDeviceId(),
-                                    device.getDeviceName()));
+                    virtualDevices.add(device.getPublicVirtualDeviceObject());
                 }
             }
             return virtualDevices;
         }
 
+        @Override // Binder call
+        public VirtualDevice getVirtualDevice(int deviceId) {
+            VirtualDeviceImpl device;
+            synchronized (mVirtualDeviceManagerLock) {
+                device = mVirtualDevices.get(deviceId);
+            }
+            return device == null ? null : device.getPublicVirtualDeviceObject();
+        }
+
+        @Override // Binder call
+        public void registerVirtualDeviceListener(IVirtualDeviceListener listener) {
+            mVirtualDeviceListeners.register(listener);
+        }
+
+        @Override // Binder call
+        public void unregisterVirtualDeviceListener(IVirtualDeviceListener listener) {
+            mVirtualDeviceListeners.unregister(listener);
+        }
+
         @Override // BinderCall
         @VirtualDeviceParams.DevicePolicy
         public int getDevicePolicy(int deviceId, @VirtualDeviceParams.PolicyType int policyType) {
@@ -705,7 +751,9 @@
             synchronized (mVirtualDeviceManagerLock) {
                 virtualDevice = mVirtualDevices.get(deviceId);
             }
-            return virtualDevice == null ? new ArraySet<>() : virtualDevice.getDisplayIds();
+            return virtualDevice == null ? new ArraySet<>()
+                    : Arrays.stream(virtualDevice.getDisplayIds()).boxed()
+                            .collect(Collectors.toCollection(ArraySet::new));
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index 28df24c..c65452a 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -16,21 +16,31 @@
 
 package com.android.server.companion.virtual;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 import static android.content.Context.DEVICE_ID_DEFAULT;
 import static android.content.Context.DEVICE_ID_INVALID;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
 
+import android.companion.virtual.IVirtualDevice;
 import android.companion.virtual.VirtualDevice;
+import android.companion.virtual.flags.Flags;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 @Presubmit
 @RunWith(AndroidJUnit4.class)
@@ -40,24 +50,35 @@
     private static final String PERSISTENT_ID = "persistentId";
     private static final String DEVICE_NAME = "VirtualDeviceName";
 
+    @Mock
+    private IVirtualDevice mVirtualDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
     @Test
     public void build_invalidId_shouldThrowIllegalArgumentException() {
         assertThrows(
                 IllegalArgumentException.class,
-                () -> new VirtualDevice(DEVICE_ID_INVALID, PERSISTENT_ID, DEVICE_NAME));
+                () -> new VirtualDevice(
+                        mVirtualDevice, DEVICE_ID_INVALID, PERSISTENT_ID, DEVICE_NAME));
     }
 
     @Test
     public void build_defaultId_shouldThrowIllegalArgumentException() {
         assertThrows(
                 IllegalArgumentException.class,
-                () -> new VirtualDevice(DEVICE_ID_DEFAULT, PERSISTENT_ID, DEVICE_NAME));
+                () -> new VirtualDevice(
+                        mVirtualDevice, DEVICE_ID_DEFAULT, PERSISTENT_ID, DEVICE_NAME));
     }
 
     @Test
     public void build_onlyRequiredFields() {
         VirtualDevice virtualDevice =
-                new VirtualDevice(VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null);
+                new VirtualDevice(
+                        mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null);
         assertThat(virtualDevice.getDeviceId()).isEqualTo(VIRTUAL_DEVICE_ID);
         assertThat(virtualDevice.getPersistentDeviceId()).isNull();
         assertThat(virtualDevice.getName()).isNull();
@@ -66,15 +87,43 @@
     @Test
     public void parcelable_shouldRecreateSuccessfully() {
         VirtualDevice originalDevice =
-                new VirtualDevice(VIRTUAL_DEVICE_ID, PERSISTENT_ID, DEVICE_NAME);
+                new VirtualDevice(mVirtualDevice, VIRTUAL_DEVICE_ID, PERSISTENT_ID, DEVICE_NAME);
         Parcel parcel = Parcel.obtain();
         originalDevice.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
 
         VirtualDevice device = VirtualDevice.CREATOR.createFromParcel(parcel);
-        assertThat(device).isEqualTo(originalDevice);
         assertThat(device.getDeviceId()).isEqualTo(VIRTUAL_DEVICE_ID);
         assertThat(device.getPersistentDeviceId()).isEqualTo(PERSISTENT_ID);
         assertThat(device.getName()).isEqualTo(DEVICE_NAME);
     }
+
+    @RequiresFlagsEnabled(Flags.FLAG_VDM_PUBLIC_APIS)
+    @Test
+    public void virtualDevice_getDisplayIds() throws Exception {
+        VirtualDevice virtualDevice =
+                new VirtualDevice(
+                        mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null);
+
+        when(mVirtualDevice.getDisplayIds()).thenReturn(new int[0]);
+        assertThat(virtualDevice.getDisplayIds()).hasLength(0);
+
+        final int[] displayIds = new int[]{7, 18};
+        when(mVirtualDevice.getDisplayIds()).thenReturn(displayIds);
+        assertThat(virtualDevice.getDisplayIds()).isEqualTo(displayIds);
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_VDM_PUBLIC_APIS)
+    @Test
+    public void virtualDevice_hasCustomSensorSupport() throws Exception {
+        VirtualDevice virtualDevice =
+                new VirtualDevice(
+                        mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null);
+
+        when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_SENSORS)).thenReturn(DEVICE_POLICY_DEFAULT);
+        assertThat(virtualDevice.hasCustomSensorSupport()).isFalse();
+
+        when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_SENSORS)).thenReturn(DEVICE_POLICY_CUSTOM);
+        assertThat(virtualDevice.hasCustomSensorSupport()).isTrue();
+    }
 }