summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Biswarup Pal <biswarupp@google.com> 2023-11-14 13:47:13 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-11-14 13:47:13 +0000
commite7cf167fda9d9a21255ce845c32570ded04c47cc (patch)
treea4d55bbf7035c22980908394306c6045de0e9172
parent4831a63b231d71e9d99ae95facfcd5c8de1b6d52 (diff)
parent325b667ceb790e96e84157768cf7318c5a8491b7 (diff)
Merge "Add virtual camera API and the aidl interfaces." into main
-rw-r--r--core/api/system-current.txt47
-rw-r--r--core/java/android/companion/virtual/IVirtualDevice.aidl14
-rw-r--r--core/java/android/companion/virtual/camera/IVirtualCamera.aidl17
-rw-r--r--core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl70
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCamera.java57
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraCallback.java53
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl (renamed from core/java/android/companion/virtual/camera/VirtualCameraSession.java)15
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraConfig.java233
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl12
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl (renamed from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl)12
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraMetadata.java61
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java44
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl8
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java106
-rw-r--r--services/companion/Android.bp10
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java27
-rw-r--r--services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl39
-rw-r--r--services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java258
-rw-r--r--services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java94
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java224
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java222
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java158
23 files changed, 914 insertions, 869 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ee1659bbe84a..fbd2142ba625 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3331,6 +3331,53 @@ package android.companion.virtual.audio {
}
+package android.companion.virtual.camera {
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
+ method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
+ method public void onProcessCaptureRequest(int, long, @Nullable android.companion.virtual.camera.VirtualCameraMetadata);
+ method public void onStreamClosed(int);
+ method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
+ method public int describeContents();
+ method @StringRes public int getDisplayNameStringRes();
+ method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR;
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
+ ctor public VirtualCameraConfig.Builder();
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int);
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setDisplayNameStringRes(@StringRes int);
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraMetadata implements android.os.Parcelable {
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraMetadata> CREATOR;
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
+ ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public int describeContents();
+ method public int getFormat();
+ method @IntRange(from=1) public int getHeight();
+ method @IntRange(from=1) public int getWidth();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraStreamConfig> CREATOR;
+ }
+
+}
+
package android.companion.virtual.sensor {
public final class VirtualSensor implements android.os.Parcelable {
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 2f97080901f9..102cbf3a7e31 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -23,8 +23,7 @@ import android.companion.virtual.audio.IAudioRoutingCallback;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorConfig;
import android.companion.virtual.sensor.VirtualSensorEvent;
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
+import android.companion.virtual.camera.VirtualCameraConfig;
import android.content.ComponentName;
import android.content.IntentFilter;
import android.graphics.Point;
@@ -236,8 +235,15 @@ interface IVirtualDevice {
void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
/**
- * Creates a new VirtualCamera and registers it with the VirtualCameraProvider.
+ * Creates a new virtual camera and registers it with the virtual camera service.
*/
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
- void registerVirtualCamera(in IVirtualCamera camera);
+ void registerVirtualCamera(in VirtualCameraConfig camera);
+
+ /**
+ * Destroys the virtual camera with given config and unregisters it from the virtual camera
+ * service.
+ */
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+ void unregisterVirtualCamera(in VirtualCameraConfig camera);
}
diff --git a/core/java/android/companion/virtual/camera/IVirtualCamera.aidl b/core/java/android/companion/virtual/camera/IVirtualCamera.aidl
deleted file mode 100644
index 58b850dac41c..000000000000
--- a/core/java/android/companion/virtual/camera/IVirtualCamera.aidl
+++ /dev/null
@@ -1,17 +0,0 @@
-package android.companion.virtual.camera;
-
-import android.companion.virtual.camera.IVirtualCameraSession;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-
-/**
- * Counterpart of ICameraDevice for virtual camera.
- *
- * @hide
- */
-interface IVirtualCamera {
-
- IVirtualCameraSession open();
-
- VirtualCameraHalConfig getHalConfig();
-
-} \ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
new file mode 100644
index 000000000000..fac44b50024f
--- /dev/null
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -0,0 +1,70 @@
+/*
+ * 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.camera;
+
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtual.camera.VirtualCameraMetadata;
+import android.view.Surface;
+
+/**
+ * Interface for the virtual camera service and system server to talk back to the virtual camera owner.
+ *
+ * @hide
+ */
+interface IVirtualCameraCallback {
+
+ /**
+ * Called when one of the requested stream has been configured by the virtual camera service and
+ * is ready to receive data onto its {@link Surface}
+ *
+ * @param streamId The id of the configured stream
+ * @param surface The surface to write data into for this stream
+ * @param streamConfig The image data configuration for this stream
+ */
+ oneway void onStreamConfigured(
+ int streamId,
+ in Surface surface,
+ in VirtualCameraStreamConfig streamConfig);
+
+ /**
+ * The client application is requesting a camera frame for the given streamId with the provided
+ * metadata.
+ *
+ * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
+ * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
+ * VirtualCameraStreamConfig)} call.
+ *
+ * @param streamId The streamId for which the frame is requested. This corresponds to the
+ * streamId that was given in {@link #onStreamConfigured(int, Surface,
+ * VirtualCameraStreamConfig)}
+ * @param frameId The frameId that is being requested. Each request will have a different
+ * frameId, that will be increasing for each call with a particular streamId.
+ * @param metadata The metadata requested for the frame. The virtual camera should do its best
+ * to honor the requested metadata.
+ */
+ oneway void onProcessCaptureRequest(
+ int streamId, long frameId, in VirtualCameraMetadata metadata);
+
+ /**
+ * The stream previously configured when {@link #onStreamConfigured(int, Surface,
+ * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
+ * freed. The Surface was disposed on the client side and should not be used anymore by the virtual camera owner
+ *
+ * @param streamId The id of the stream that was closed.
+ */
+ oneway void onStreamClosed(int streamId);
+
+} \ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java
index 791bf0a1ff1f..beee86fcfac2 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -16,20 +16,44 @@
package android.companion.virtual.camera;
+import android.annotation.FlaggedApi;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.companion.virtual.flags.Flags;
+import android.hardware.camera2.CameraDevice;
import android.os.RemoteException;
import androidx.annotation.NonNull;
+import java.io.Closeable;
import java.util.Objects;
+import java.util.concurrent.Executor;
/**
- * Virtual camera that is used to send image data into system.
+ * A VirtualCamera is the representation of a remote or computer generated camera that will be
+ * exposed to applications using the Android Camera APIs.
*
+ * <p>A VirtualCamera is created using {@link
+ * VirtualDeviceManager.VirtualDevice#createVirtualCamera(VirtualCameraConfig)}.
+ *
+ * <p>Once a virtual camera is created, it will receive callbacks from the system when an
+ * application attempts to use it via the {@link VirtualCameraCallback} class set using {@link
+ * VirtualCameraConfig.Builder#setVirtualCameraCallback(Executor, VirtualCameraCallback)}
+ *
+ * @see VirtualDeviceManager.VirtualDevice#createVirtualDevice(int, VirtualDeviceParams)
+ * @see VirtualCameraConfig.Builder#setVirtualCameraCallback(Executor, VirtualCameraCallback)
+ * @see android.hardware.camera2.CameraManager#openCamera(String, CameraDevice.StateCallback,
+ * android.os.Handler)
* @hide
*/
-public final class VirtualCamera extends IVirtualCamera.Stub {
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCamera implements Closeable {
+ private final IVirtualDevice mVirtualDevice;
private final VirtualCameraConfig mConfig;
/**
@@ -37,40 +61,35 @@ public final class VirtualCamera extends IVirtualCamera.Stub {
*
* @param virtualDevice The Binder object representing this camera in the server.
* @param config Configuration for the new virtual camera
+ * @hide
*/
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public VirtualCamera(
@NonNull IVirtualDevice virtualDevice, @NonNull VirtualCameraConfig config) {
+ mVirtualDevice = virtualDevice;
mConfig = Objects.requireNonNull(config);
Objects.requireNonNull(virtualDevice);
+ // TODO(b/310857519): Avoid registration inside constructor.
try {
- virtualDevice.registerVirtualCamera(this);
+ mVirtualDevice.registerVirtualCamera(config);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
- /** Get the camera session associated with this device */
- @Override
- public IVirtualCameraSession open() {
- // TODO: b/302255544 - Make this async.
- VirtualCameraSession session = mConfig.getCallback().onOpenSession();
- return new VirtualCameraSessionInternal(session);
- }
-
/** Returns the configuration of this virtual camera instance. */
@NonNull
public VirtualCameraConfig getConfig() {
return mConfig;
}
- /**
- * Returns the configuration to be used by the virtual camera HAL.
- *
- * @hide
- */
@Override
- @NonNull
- public VirtualCameraHalConfig getHalConfig() {
- return mConfig.getHalConfig();
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public void close() {
+ try {
+ mVirtualDevice.unregisterVirtualCamera(mConfig);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
}
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index a7c3d4fac7dc..a18ae03555e9 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -16,7 +16,12 @@
package android.companion.virtual.camera;
-import android.hardware.camera2.params.SessionConfiguration;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.view.Surface;
import java.util.concurrent.Executor;
@@ -24,15 +29,53 @@ import java.util.concurrent.Executor;
* Interface to be provided when creating a new {@link VirtualCamera} in order to receive callbacks
* from the framework and the camera system.
*
- * @see VirtualCameraConfig.Builder#setCallback(Executor, VirtualCameraCallback)
+ * @see VirtualCameraConfig.Builder#setVirtualCameraCallback(Executor, VirtualCameraCallback)
* @hide
*/
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public interface VirtualCameraCallback {
/**
- * Called when a client opens a new camera session for the associated {@link VirtualCamera}
+ * Called when one of the requested stream has been configured by the virtual camera service and
+ * is ready to receive data onto its {@link Surface}
*
- * @see android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)
+ * @param streamId The id of the configured stream
+ * @param surface The surface to write data into for this stream
+ * @param streamConfig The image data configuration for this stream
*/
- VirtualCameraSession onOpenSession();
+ void onStreamConfigured(
+ int streamId,
+ @NonNull Surface surface,
+ @NonNull VirtualCameraStreamConfig streamConfig);
+
+ /**
+ * The client application is requesting a camera frame for the given streamId with the provided
+ * metadata.
+ *
+ * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
+ * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
+ * VirtualCameraStreamConfig)} call.
+ *
+ * @param streamId The streamId for which the frame is requested. This corresponds to the
+ * streamId that was given in {@link #onStreamConfigured(int, Surface,
+ * VirtualCameraStreamConfig)}
+ * @param frameId The frameId that is being requested. Each request will have a different
+ * frameId, that will be increasing for each call with a particular streamId.
+ * @param metadata The metadata requested for the frame. The virtual camera should do its best
+ * to honor the requested metadata but the consumer won't be informed about the metadata set
+ * for a particular frame. If null, the requested frame can be anything the producer sends.
+ */
+ void onProcessCaptureRequest(
+ int streamId, long frameId, @Nullable VirtualCameraMetadata metadata);
+
+ /**
+ * The stream previously configured when {@link #onStreamConfigured(int, Surface,
+ * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
+ * freed. The Surface corresponding to that streamId was disposed on the client side and should
+ * not be used anymore by the virtual camera owner
+ *
+ * @param streamId The id of the stream that was closed.
+ */
+ void onStreamClosed(int streamId);
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraSession.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
index c25d97711e75..88c27a52bb9d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraSession.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
@@ -13,18 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package android.companion.virtual.camera;
-/***
- * Counterpart of {@link android.hardware.camera2.CameraCaptureSession} for producing
- * images from a {@link VirtualCamera}.
- * @hide
- */
-// TODO: b/289881985 - This is just a POC implementation for now, this will be extended
-// to a full featured Camera Session
-public interface VirtualCameraSession {
-
- /** Close the session and release its resources. */
- default void close() {}
-}
+/** @hide */
+parcelable VirtualCameraConfig; \ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index fb464d53b9b5..f1eb240301e0 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -18,11 +18,19 @@ package android.companion.virtual.camera;
import static java.util.Objects.requireNonNull;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.StringRes;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.content.res.Resources;
import android.graphics.ImageFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArraySet;
+import android.view.Surface;
-import java.util.Collections;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -33,33 +41,115 @@ import java.util.concurrent.Executor;
*
* @hide
*/
-public final class VirtualCameraConfig {
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCameraConfig implements Parcelable {
- private final String mDisplayName;
+ private final @StringRes int mNameStringRes;
private final Set<VirtualCameraStreamConfig> mStreamConfigurations;
- private final VirtualCameraCallback mCallback;
- private final Executor mCallbackExecutor;
+ private final IVirtualCameraCallback mCallback;
+
+ private VirtualCameraConfig(
+ int displayNameStringRes,
+ @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
+ @NonNull Executor executor,
+ @NonNull VirtualCameraCallback callback) {
+ mNameStringRes = displayNameStringRes;
+ mStreamConfigurations =
+ Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations"));
+ if (mStreamConfigurations.isEmpty()) {
+ throw new IllegalArgumentException(
+ "At least one stream configuration is needed to create a virtual camera.");
+ }
+ mCallback =
+ new VirtualCameraCallbackInternal(
+ requireNonNull(callback, "Missing callback"),
+ requireNonNull(executor, "Missing callback executor"));
+ }
+
+ private VirtualCameraConfig(@NonNull Parcel in) {
+ mNameStringRes = in.readInt();
+ mCallback = IVirtualCameraCallback.Stub.asInterface(in.readStrongBinder());
+ mStreamConfigurations =
+ Set.of(
+ in.readParcelableArray(
+ VirtualCameraStreamConfig.class.getClassLoader(),
+ VirtualCameraStreamConfig.class));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mNameStringRes);
+ dest.writeStrongInterface(mCallback);
+ dest.writeParcelableArray(
+ mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags);
+ }
+
+ /**
+ * @return The display name of this VirtualCamera
+ */
+ @StringRes
+ public int getDisplayNameStringRes() {
+ return mNameStringRes;
+ }
+
+ /**
+ * Returns an unmodifiable set of the stream configurations added to this {@link
+ * VirtualCameraConfig}.
+ *
+ * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int)
+ */
+ @NonNull
+ public Set<VirtualCameraStreamConfig> getStreamConfigs() {
+ return mStreamConfigurations;
+ }
+
+ /**
+ * Returns the callback used to communicate from the server to the client.
+ *
+ * @hide
+ */
+ @NonNull
+ public IVirtualCameraCallback getCallback() {
+ return mCallback;
+ }
/**
* Builder for {@link VirtualCameraConfig}.
*
* <p>To build an instance of {@link VirtualCameraConfig} the following conditions must be met:
- * <li>At least one stream must be added wit {@link #addStreamConfiguration(int, int, int)}.
- * <li>A name must be set with {@link #setDisplayName(String)}
- * <li>A callback must be set wit {@link #setCallback(Executor, VirtualCameraCallback)}
+ * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}.
+ * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
+ * VirtualCameraCallback)}
+ * <li>A user readable name can be set with {@link #setDisplayNameStringRes(int)}
*/
+ @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public static final class Builder {
- private String mDisplayName;
- private final ArraySet<VirtualCameraStreamConfig> mStreamConfiguration = new ArraySet<>();
+ private @StringRes int mDisplayNameStringRes = Resources.ID_NULL;
+ private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>();
private Executor mCallbackExecutor;
private VirtualCameraCallback mCallback;
- /** Set the visible name of this camera for the user. */
- // TODO: b/290172356 - Take a resource id instead of displayName
+ /**
+ * Set the visible name of this camera for the user.
+ *
+ * <p>Sets the resource to a string representing a user readable name for this virtual
+ * camera.
+ *
+ * @throws IllegalArgumentException if an invalid resource id is passed.
+ */
@NonNull
- public Builder setDisplayName(@NonNull String displayName) {
- mDisplayName = requireNonNull(displayName);
+ public Builder setDisplayNameStringRes(@StringRes int displayNameStringRes) {
+ if (displayNameStringRes <= 0) {
+ throw new IllegalArgumentException("Invalid resource passed for display name");
+ }
+ mDisplayNameStringRes = displayNameStringRes;
return this;
}
@@ -68,18 +158,21 @@ public final class VirtualCameraConfig {
*
* <p>At least one {@link VirtualCameraStreamConfig} must be added.
*
- * @param width The width of the stream
- * @param height The height of the stream
- * @param format The {@link ImageFormat} of the stream
+ * @param width The width of the stream.
+ * @param height The height of the stream.
+ * @param format The {@link ImageFormat} of the stream.
+ *
+ * @throws IllegalArgumentException if invalid format or dimensions are passed.
*/
@NonNull
- public Builder addStreamConfiguration(
- int width, int height, @ImageFormat.Format int format) {
- VirtualCameraStreamConfig streamConfig = new VirtualCameraStreamConfig();
- streamConfig.width = width;
- streamConfig.height = height;
- streamConfig.format = format;
- mStreamConfiguration.add(streamConfig);
+ public Builder addStreamConfig(int width, int height, @ImageFormat.Format int format) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Invalid dimensions passed for stream config");
+ }
+ if (!ImageFormat.isPublicFormat(format)) {
+ throw new IllegalArgumentException("Invalid format passed for stream config");
+ }
+ mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format));
return this;
}
@@ -93,7 +186,9 @@ public final class VirtualCameraConfig {
* @param callback The instance of the callback to be added. Subsequent call to this method
* will replace the callback set.
*/
- public Builder setCallback(
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder") // The configuration is immutable
+ public Builder setVirtualCameraCallback(
@NonNull Executor executor, @NonNull VirtualCameraCallback callback) {
mCallbackExecutor = requireNonNull(executor);
mCallback = requireNonNull(callback);
@@ -108,67 +203,49 @@ public final class VirtualCameraConfig {
@NonNull
public VirtualCameraConfig build() {
return new VirtualCameraConfig(
- mDisplayName, mStreamConfiguration, mCallbackExecutor, mCallback);
+ mDisplayNameStringRes, mStreamConfigurations, mCallbackExecutor, mCallback);
}
}
- private VirtualCameraConfig(
- @NonNull String displayName,
- @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
- @NonNull Executor executor,
- @NonNull VirtualCameraCallback callback) {
- mDisplayName = requireNonNull(displayName, "Missing display name");
- mStreamConfigurations =
- Collections.unmodifiableSet(
- requireNonNull(streamConfigurations, "Missing stream configuration"));
- if (mStreamConfigurations.isEmpty()) {
- throw new IllegalArgumentException(
- "At least one StreamConfiguration is needed to create a virtual camera.");
- }
- mCallback = requireNonNull(callback, "Missing callback");
- mCallbackExecutor = requireNonNull(executor, "Missing callback executor");
- }
+ private static class VirtualCameraCallbackInternal extends IVirtualCameraCallback.Stub {
- /**
- * @return The display name of this VirtualCamera
- */
- @NonNull
- public String getDisplayName() {
- return mDisplayName;
- }
+ private final VirtualCameraCallback mCallback;
+ private final Executor mExecutor;
- /**
- * Returns an unmodifiable set of the stream configurations added to this {@link
- * VirtualCameraConfig}.
- *
- * @see VirtualCameraConfig.Builder#addStreamConfiguration(int, int, int)
- */
- @NonNull
- public Set<VirtualCameraStreamConfig> getStreamConfigs() {
- return mStreamConfigurations;
- }
+ private VirtualCameraCallbackInternal(VirtualCameraCallback callback, Executor executor) {
+ mCallback = callback;
+ mExecutor = executor;
+ }
- /** Returns the callback used to communicate from the server to the client. */
- @NonNull
- public VirtualCameraCallback getCallback() {
- return mCallback;
- }
+ @Override
+ public void onStreamConfigured(
+ int streamId, Surface surface, VirtualCameraStreamConfig streamConfig) {
+ mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, streamConfig));
+ }
- /** Returns the executor onto which the callback should be run. */
- @NonNull
- public Executor getCallbackExecutor() {
- return mCallbackExecutor;
+ @Override
+ public void onProcessCaptureRequest(
+ int streamId, long frameId, VirtualCameraMetadata metadata) {
+ mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId, metadata));
+ }
+
+ @Override
+ public void onStreamClosed(int streamId) {
+ mExecutor.execute(() -> mCallback.onStreamClosed(streamId));
+ }
}
- /**
- * Returns a new instance of {@link VirtualCameraHalConfig} initialized with data from this
- * {@link VirtualCameraConfig}
- */
@NonNull
- public VirtualCameraHalConfig getHalConfig() {
- VirtualCameraHalConfig halConfig = new VirtualCameraHalConfig();
- halConfig.displayName = mDisplayName;
- halConfig.streamConfigs = mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]);
- return halConfig;
- }
+ public static final Parcelable.Creator<VirtualCameraConfig> CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public VirtualCameraConfig createFromParcel(Parcel in) {
+ return new VirtualCameraConfig(in);
+ }
+
+ @Override
+ public VirtualCameraConfig[] newArray(int size) {
+ return new VirtualCameraConfig[size];
+ }
+ };
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl
deleted file mode 100644
index 7070a38b6414..000000000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl
+++ /dev/null
@@ -1,12 +0,0 @@
-package android.companion.virtual.camera;
-
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
-
-/**
- * Configuration for VirtualCamera to be passed to the server and HAL service.
- * @hide
- */
-parcelable VirtualCameraHalConfig {
- String displayName;
- VirtualCameraStreamConfig[] streamConfigs;
-}
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
index 252980773264..6c1f0fcd622a 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
@@ -13,16 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.companion.virtual.camera;
/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
+ * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
+ * VirtualCamera.
* @hide
*/
-interface IVirtualCameraSession {
-
- void configureStream(int width, int height, int format);
-
- void close();
-}
+parcelable VirtualCameraMetadata;
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
new file mode 100644
index 000000000000..1ba36d08cbeb
--- /dev/null
+++ b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
@@ -0,0 +1,61 @@
+/*
+ * 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.camera;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data structure used to store camera metadata compatible with VirtualCamera.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCameraMetadata implements Parcelable {
+
+ /** @hide */
+ public VirtualCameraMetadata(@NonNull Parcel in) {}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {}
+
+ @NonNull
+ public static final Creator<VirtualCameraMetadata> CREATOR =
+ new Creator<>() {
+ @Override
+ @NonNull
+ public VirtualCameraMetadata createFromParcel(Parcel in) {
+ return new VirtualCameraMetadata(in);
+ }
+
+ @Override
+ @NonNull
+ public VirtualCameraMetadata[] newArray(int size) {
+ return new VirtualCameraMetadata[size];
+ }
+ };
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java b/core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java
deleted file mode 100644
index da168de41cee..000000000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.camera;
-
-import android.graphics.ImageFormat;
-
-import androidx.annotation.NonNull;
-
-import java.util.Objects;
-
-/**
- * Wraps the client side {@link VirtualCameraSession} into an {@link IVirtualCameraSession}.
- *
- * @hide
- */
-final class VirtualCameraSessionInternal extends IVirtualCameraSession.Stub {
-
- @SuppressWarnings("FieldCanBeLocal")
- // TODO: b/289881985: Will be used once connected with the CameraService
- private final VirtualCameraSession mVirtualCameraSession;
-
- VirtualCameraSessionInternal(@NonNull VirtualCameraSession virtualCameraSession) {
- mVirtualCameraSession = Objects.requireNonNull(virtualCameraSession);
- }
-
- @Override
- public void configureStream(int width, int height, @ImageFormat.Format int format) {}
-
- public void close() {}
-}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
index 304d4553bd2a..ce92b6d48f0d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
@@ -16,11 +16,7 @@
package android.companion.virtual.camera;
/**
- * A stream configuration supported by a virtual camera
+ * The configuration of a single virtual camera stream.
* @hide
*/
-parcelable VirtualCameraStreamConfig {
- int width;
- int height;
- int format;
-}
+parcelable VirtualCameraStreamConfig; \ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
new file mode 100644
index 000000000000..e198821e860e
--- /dev/null
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -0,0 +1,106 @@
+/*
+ * 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.camera;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.graphics.ImageFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The configuration of a single virtual camera stream.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCameraStreamConfig implements Parcelable {
+
+ private final int mWidth;
+ private final int mHeight;
+ private final int mFormat;
+
+ /**
+ * Construct a new instance of {@link VirtualCameraStreamConfig} initialized with the provided
+ * width, height and {@link ImageFormat}
+ *
+ * @param width The width of the stream.
+ * @param height The height of the stream.
+ * @param format The {@link ImageFormat} of the stream.
+ */
+ public VirtualCameraStreamConfig(
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @ImageFormat.Format int format) {
+ this.mWidth = width;
+ this.mHeight = height;
+ this.mFormat = format;
+ }
+
+ private VirtualCameraStreamConfig(@NonNull Parcel in) {
+ mWidth = in.readInt();
+ mHeight = in.readInt();
+ mFormat = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
+ dest.writeInt(mFormat);
+ }
+
+ @NonNull
+ public static final Creator<VirtualCameraStreamConfig> CREATOR =
+ new Creator<>() {
+ @Override
+ public VirtualCameraStreamConfig createFromParcel(Parcel in) {
+ return new VirtualCameraStreamConfig(in);
+ }
+
+ @Override
+ public VirtualCameraStreamConfig[] newArray(int size) {
+ return new VirtualCameraStreamConfig[size];
+ }
+ };
+
+ /** Returns the width of this stream. */
+ @IntRange(from = 1)
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /** Returns the height of this stream. */
+ @IntRange(from = 1)
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /** Returns the {@link ImageFormat} of this stream. */
+ @ImageFormat.Format
+ public int getFormat() {
+ return mFormat;
+ }
+}
diff --git a/services/companion/Android.bp b/services/companion/Android.bp
index fb8db21c3090..550e17be276d 100644
--- a/services/companion/Android.bp
+++ b/services/companion/Android.bp
@@ -21,7 +21,6 @@ java_library_static {
defaults: ["platform_service_defaults"],
srcs: [
":services.companion-sources",
- ":VirtualCamera-aidl-sources",
],
libs: [
"app-compat-annotations",
@@ -30,13 +29,6 @@ java_library_static {
static_libs: [
"ukey2_jni",
"virtualdevice_flags_lib",
+ "virtual_camera_service_aidl-java",
],
}
-
-filegroup {
- name: "VirtualCamera-aidl-sources",
- srcs: [
- "java/com/android/server/companion/virtual/camera/*.aidl",
- ],
- path: "java",
-}
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 562fe3b17f9f..a392e78aa217 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -51,7 +51,7 @@ import android.companion.virtual.VirtualDeviceManager.ActivityListener;
import android.companion.virtual.VirtualDeviceParams;
import android.companion.virtual.audio.IAudioConfigChangedCallback;
import android.companion.virtual.audio.IAudioRoutingCallback;
-import android.companion.virtual.camera.IVirtualCamera;
+import android.companion.virtual.camera.VirtualCameraConfig;
import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorEvent;
@@ -277,7 +277,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
runningAppsChangedCallback,
params,
DisplayManagerGlobal.getInstance(),
- Flags.virtualCamera() ? new VirtualCameraController(context) : null);
+ Flags.virtualCamera() ? new VirtualCameraController() : null);
}
@VisibleForTesting
@@ -954,13 +954,28 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
}
+ @Override // Binder call
@EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
- public void registerVirtualCamera(@NonNull IVirtualCamera camera) {
+ public void registerVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
+ throws RemoteException {
super.registerVirtualCamera_enforcePermission();
+ Objects.requireNonNull(cameraConfig);
if (mVirtualCameraController == null) {
- return;
+ throw new UnsupportedOperationException("Virtual camera controller is not available");
+ }
+ mVirtualCameraController.registerCamera(Objects.requireNonNull(cameraConfig));
+ }
+
+ @Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public void unregisterVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
+ throws RemoteException {
+ super.unregisterVirtualCamera_enforcePermission();
+ Objects.requireNonNull(cameraConfig);
+ if (mVirtualCameraController == null) {
+ throw new UnsupportedOperationException("Virtual camera controller is not available");
}
- mVirtualCameraController.registerCamera(Objects.requireNonNull(camera));
+ mVirtualCameraController.unregisterCamera(Objects.requireNonNull(cameraConfig));
}
@Override
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl b/services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl
deleted file mode 100644
index a4c1c4249697..000000000000
--- a/services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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 com.android.server.companion.virtual.camera;
-
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-
-/**
- * AIDL Interface to communicate with the VirtualCamera HAL
- * @hide
- */
-interface IVirtualCameraService {
-
- /**
- * Registers a new camera with the virtual camera hal.
- * @return true if the camera was successfully registered
- */
- boolean registerCamera(in IVirtualCamera camera);
-
- /**
- * Unregisters the camera from the virtual camera hal. After this call the virtual camera won't
- * be visible to the camera framework anymore.
- */
- void unregisterCamera(in IVirtualCamera camera);
-}
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index 031d94962844..06be3f39dcd1 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -16,207 +16,127 @@
package com.android.server.companion.virtual.camera;
+import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
+import android.companion.virtual.camera.VirtualCameraConfig;
+import android.companion.virtualcamera.IVirtualCameraService;
+import android.companion.virtualcamera.VirtualCameraConfiguration;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.Log;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Set;
/**
* Manages the registration and removal of virtual camera from the server side.
*
* <p>This classes delegate calls to the virtual camera service, so it is dependent on the service
- * to be up and running
+ * to be up and running.
*/
-public class VirtualCameraController implements IBinder.DeathRecipient, ServiceConnection {
-
- private static class VirtualCameraInfo {
-
- private final IVirtualCamera mVirtualCamera;
- private boolean mIsRegistered;
-
- VirtualCameraInfo(IVirtualCamera virtualCamera) {
- mVirtualCamera = virtualCamera;
- }
- }
+public final class VirtualCameraController implements IBinder.DeathRecipient {
+ private static final String VIRTUAL_CAMERA_SERVICE_NAME = "virtual_camera";
private static final String TAG = "VirtualCameraController";
- private static final String VIRTUAL_CAMERA_SERVICE_PACKAGE = "com.android.virtualcamera";
- private static final String VIRTUAL_CAMERA_SERVICE_CLASS = ".VirtualCameraService";
- private final Context mContext;
-
- @Nullable private IVirtualCameraService mVirtualCameraService = null;
+ @Nullable private IVirtualCameraService mVirtualCameraService;
@GuardedBy("mCameras")
- private final Map<IVirtualCamera, VirtualCameraInfo> mCameras = new HashMap<>(1);
+ private final Set<VirtualCameraConfig> mCameras = new ArraySet<>();
- public VirtualCameraController(Context context) {
- mContext = context;
+ public VirtualCameraController() {
connectVirtualCameraService();
}
- private void connectVirtualCameraService() {
- final long callingId = Binder.clearCallingIdentity();
- try {
- Intent intent = new Intent();
- intent.setPackage(VIRTUAL_CAMERA_SERVICE_PACKAGE);
- intent.setComponent(
- ComponentName.createRelative(
- VIRTUAL_CAMERA_SERVICE_PACKAGE, VIRTUAL_CAMERA_SERVICE_CLASS));
- mContext.startServiceAsUser(intent, UserHandle.SYSTEM);
- if (!mContext.bindServiceAsUser(
- intent,
- this,
- Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
- UserHandle.SYSTEM)) {
- mContext.unbindService(this);
- Log.w(
- TAG,
- "connectVirtualCameraService: Failed to connect to the virtual camera "
- + "service");
- }
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
- private void forwardPendingRegistrations() {
- IVirtualCameraService cameraService = mVirtualCameraService;
- if (cameraService == null) {
- return;
- }
- synchronized (mCameras) {
- for (VirtualCameraInfo cameraInfo : mCameras.values()) {
- if (cameraInfo.mIsRegistered) {
- continue;
- }
- try {
- cameraService.registerCamera(cameraInfo.mVirtualCamera);
- cameraInfo.mIsRegistered = true;
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- }
- }
- }
+ @VisibleForTesting
+ VirtualCameraController(IVirtualCameraService virtualCameraService) {
+ mVirtualCameraService = virtualCameraService;
}
/**
- * Remove the virtual camera with the provided name
+ * Register a new virtual camera with the given config.
*
- * @param camera The name of the camera to remove
+ * @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
*/
- public void unregisterCamera(@NonNull IVirtualCamera camera) {
- IVirtualCameraService virtualCameraService = mVirtualCameraService;
- if (virtualCameraService != null) {
- try {
- virtualCameraService.unregisterCamera(camera);
+ public void registerCamera(@NonNull VirtualCameraConfig cameraConfig) {
+ // Try to connect to service if not connected already.
+ if (mVirtualCameraService == null) {
+ connectVirtualCameraService();
+ }
+ // Throw exception if we are unable to connect to service.
+ if (mVirtualCameraService == null) {
+ throw new IllegalStateException("Virtual camera service is not connected.");
+ }
+
+ try {
+ if (registerCameraWithService(cameraConfig)) {
synchronized (mCameras) {
- VirtualCameraInfo cameraInfo = mCameras.remove(camera);
- cameraInfo.mIsRegistered = false;
+ mCameras.add(cameraConfig);
}
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
+ } else {
+ // TODO(b/310857519): Revisit this to find a better way of indicating failure.
+ throw new RuntimeException("Failed to register virtual camera.");
}
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
/**
- * Register a new virtual camera with the provided characteristics.
+ * Unregister the virtual camera with the given config.
*
- * @param camera The {@link IVirtualCamera} producing the image to communicate with the client.
- * @throws IllegalArgumentException if the characteristics could not be parsed.
+ * @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
*/
- public void registerCamera(@NonNull IVirtualCamera camera) {
- IVirtualCameraService service = mVirtualCameraService;
- VirtualCameraInfo virtualCameraInfo = new VirtualCameraInfo(camera);
- synchronized (mCameras) {
- mCameras.put(camera, virtualCameraInfo);
- }
- if (service != null) {
- try {
- if (service.registerCamera(camera)) {
- virtualCameraInfo.mIsRegistered = true;
- return;
- }
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
+ public void unregisterCamera(@NonNull VirtualCameraConfig cameraConfig) {
+ try {
+ if (mVirtualCameraService == null) {
+ Slog.w(TAG, "Virtual camera service is not connected.");
+ } else {
+ mVirtualCameraService.unregisterCamera(cameraConfig.getCallback().asBinder());
}
+ synchronized (mCameras) {
+ mCameras.remove(cameraConfig);
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
-
- // Service was not available or registration failed, save the registration for later
- connectVirtualCameraService();
}
@Override
public void binderDied() {
- Log.d(TAG, "binderDied");
+ Slog.d(TAG, "Virtual camera service died.");
mVirtualCameraService = null;
- }
-
- @Override
- public void onBindingDied(ComponentName name) {
- mVirtualCameraService = null;
- Log.d(TAG, "onBindingDied() called with: name = [" + name + "]");
- }
-
- @Override
- public void onNullBinding(ComponentName name) {
- mVirtualCameraService = null;
- Log.d(TAG, "onNullBinding() called with: name = [" + name + "]");
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d(TAG, "onServiceConnected: " + name.toString());
- mVirtualCameraService = IVirtualCameraService.Stub.asInterface(service);
- try {
- service.linkToDeath(this, 0);
- } catch (RemoteException e) {
- e.rethrowAsRuntimeException();
+ synchronized (mCameras) {
+ mCameras.clear();
}
- forwardPendingRegistrations();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "onServiceDisconnected() called with: name = [" + name + "]");
- mVirtualCameraService = null;
}
/** Release resources associated with this controller. */
public void close() {
- if (mVirtualCameraService == null) {
- return;
- }
synchronized (mCameras) {
- mCameras.forEach(
- (name, cameraInfo) -> {
- try {
- mVirtualCameraService.unregisterCamera(name);
- } catch (RemoteException e) {
- Log.w(
- TAG,
- "close(): Camera failed to be removed on camera service.",
- e);
- }
- });
+ if (mVirtualCameraService == null) {
+ Slog.w(TAG, "Virtual camera service is not connected.");
+ } else {
+ for (VirtualCameraConfig config : mCameras) {
+ try {
+ mVirtualCameraService.unregisterCamera(config.getCallback().asBinder());
+ } catch (RemoteException e) {
+ Slog.w(TAG, "close(): Camera failed to be removed on camera "
+ + "service.", e);
+ }
+ }
+ }
+ mCameras.clear();
}
- mContext.unbindService(this);
+ mVirtualCameraService = null;
}
/** Dumps information about this {@link VirtualCameraController} for debugging purposes. */
@@ -226,20 +146,34 @@ public class VirtualCameraController implements IBinder.DeathRecipient, ServiceC
fout.printf("%sService:%s\n", indent, mVirtualCameraService);
synchronized (mCameras) {
fout.printf("%sRegistered cameras:%d%n\n", indent, mCameras.size());
- for (VirtualCameraInfo info : mCameras.values()) {
- VirtualCameraHalConfig config = null;
- try {
- config = info.mVirtualCamera.getHalConfig();
- } catch (RemoteException ex) {
- Log.w(TAG, ex);
- }
- fout.printf(
- "%s- %s isRegistered: %s, token: %s\n",
- indent,
- config == null ? "" : config.displayName,
- info.mIsRegistered,
- info.mVirtualCamera);
+ for (VirtualCameraConfig config : mCameras) {
+ fout.printf("%s token: %s\n", indent, config);
+ }
+ }
+ }
+
+ private void connectVirtualCameraService() {
+ final long callingId = Binder.clearCallingIdentity();
+ try {
+ IBinder virtualCameraBinder =
+ ServiceManager.waitForService(VIRTUAL_CAMERA_SERVICE_NAME);
+ if (virtualCameraBinder == null) {
+ Slog.e(TAG, "connectVirtualCameraService: Failed to connect to the virtual "
+ + "camera service");
+ return;
}
+ virtualCameraBinder.linkToDeath(this, 0);
+ mVirtualCameraService = IVirtualCameraService.Stub.asInterface(virtualCameraBinder);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
}
}
+
+ private boolean registerCameraWithService(VirtualCameraConfig config) throws RemoteException {
+ VirtualCameraConfiguration serviceConfiguration = getServiceCameraConfiguration(config);
+ return mVirtualCameraService.registerCamera(config.getCallback().asBinder(),
+ serviceConfiguration);
+ }
}
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
new file mode 100644
index 000000000000..202f68bdeb4a
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -0,0 +1,94 @@
+/*
+ * 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 com.android.server.companion.virtual.camera;
+
+import android.annotation.NonNull;
+import android.companion.virtual.camera.IVirtualCameraCallback;
+import android.companion.virtual.camera.VirtualCameraConfig;
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtualcamera.IVirtualCameraService;
+import android.companion.virtualcamera.SupportedStreamConfiguration;
+import android.companion.virtualcamera.VirtualCameraConfiguration;
+import android.os.RemoteException;
+import android.view.Surface;
+
+/** Utilities to convert the client side classes to the virtual camera service ones. */
+public final class VirtualCameraConversionUtil {
+
+ /**
+ * Fetches the configuration of the provided virtual cameraConfig that was provided by its owner
+ * and convert it into the {@link IVirtualCameraService} types: {@link
+ * VirtualCameraConfiguration}.
+ *
+ * @param cameraConfig The cameraConfig sent by the client.
+ * @return The converted configuration to be sent to the {@link IVirtualCameraService}.
+ * @throws RemoteException If there was an issue fetching the configuration from the client.
+ */
+ @NonNull
+ public static android.companion.virtualcamera.VirtualCameraConfiguration
+ getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig)
+ throws RemoteException {
+ VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration();
+
+ serviceConfiguration.supportedStreamConfigs =
+ cameraConfig.getStreamConfigs().stream()
+ .map(VirtualCameraConversionUtil::convertSupportedStreamConfiguration)
+ .toArray(SupportedStreamConfiguration[]::new);
+
+ serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback());
+ return serviceConfiguration;
+ }
+
+ @NonNull
+ private static android.companion.virtualcamera.IVirtualCameraCallback convertCallback(
+ @NonNull IVirtualCameraCallback camera) {
+ return new android.companion.virtualcamera.IVirtualCameraCallback.Stub() {
+ @Override
+ public void onStreamConfigured(
+ int streamId, Surface surface, int width, int height, int pixelFormat)
+ throws RemoteException {
+ VirtualCameraStreamConfig streamConfig =
+ createStreamConfig(width, height, pixelFormat);
+ camera.onStreamConfigured(streamId, surface, streamConfig);
+ }
+
+ @Override
+ public void onStreamClosed(int streamId) throws RemoteException {
+ camera.onStreamClosed(streamId);
+ }
+ };
+ }
+
+ @NonNull
+ private static VirtualCameraStreamConfig createStreamConfig(
+ int width, int height, int pixelFormat) {
+ return new VirtualCameraStreamConfig(width, height, pixelFormat);
+ }
+
+ @NonNull
+ private static SupportedStreamConfiguration convertSupportedStreamConfiguration(
+ VirtualCameraStreamConfig stream) {
+ SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration();
+ supportedConfig.height = stream.getHeight();
+ supportedConfig.width = stream.getWidth();
+ supportedConfig.pixelFormat = stream.getFormat();
+ return supportedConfig;
+ }
+
+ private VirtualCameraConversionUtil() {
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java
deleted file mode 100644
index 8f77e9b8d523..000000000000
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * 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 com.android.server.companion.virtual;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.Manifest;
-import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.IVirtualCameraSession;
-import android.companion.virtual.camera.VirtualCamera;
-import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-import android.companion.virtual.camera.VirtualCameraSession;
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
-import android.companion.virtual.flags.Flags;
-import android.content.ComponentName;
-import android.graphics.ImageFormat;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableContext;
-import android.testing.TestableLooper;
-
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.AdoptShellPermissionsRule;
-import com.android.server.companion.virtual.camera.IVirtualCameraService;
-import com.android.server.companion.virtual.camera.VirtualCameraController;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.HashSet;
-import java.util.Set;
-
-@Presubmit
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class VirtualCameraTest {
-
- private static final String PKG = "com.android.virtualcamera";
- private static final String CLS = ".VirtualCameraService";
- public static final String CAMERA_DISPLAY_NAME = "testCamera";
-
- private final TestableContext mContext =
- new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
- private FakeVirtualCameraService mFakeVirtualCameraService;
- private VirtualCameraController mVirtualCameraController;
-
- @Rule public final VirtualDeviceRule mVirtualDeviceRule = new VirtualDeviceRule(mContext);
-
- @Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
- @Rule
- public AdoptShellPermissionsRule mAdoptShellPermissionsRule =
- new AdoptShellPermissionsRule(
- InstrumentationRegistry.getInstrumentation().getUiAutomation(),
- Manifest.permission.CREATE_VIRTUAL_DEVICE);
-
- @Before
- public void setUp() {
- mVirtualDeviceRule.withVirtualCameraControllerSupplier(() -> mVirtualCameraController);
- mFakeVirtualCameraService = new FakeVirtualCameraService();
- connectFakeService();
- mVirtualCameraController = new VirtualCameraController(mContext);
- }
-
- private VirtualDeviceImpl createVirtualDevice() {
- return mVirtualDeviceRule.createVirtualDevice(new VirtualDeviceParams.Builder().build());
- }
-
- private void connectFakeService() {
- mContext.addMockService(
- ComponentName.createRelative(PKG, CLS), mFakeVirtualCameraService.asBinder());
- }
-
- @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
- @Test
- public void addVirtualCamera() {
- VirtualDeviceImpl virtualDevice = createVirtualDevice();
- VirtualCameraConfig config = createVirtualCameraConfig(null);
- IVirtualCamera.Default camera = new IVirtualCamera.Default();
- virtualDevice.registerVirtualCamera(camera);
-
- assertThat(mFakeVirtualCameraService.mCameras).contains(camera);
- }
-
- @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
- @Test
- public void addVirtualCamera_serviceNotReady() {
- TestableContext context =
- new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
- VirtualCameraController virtualCameraController = new VirtualCameraController(context);
- mVirtualDeviceRule.withVirtualCameraControllerSupplier(() -> virtualCameraController);
-
- VirtualDeviceImpl virtualDevice =
- mVirtualDeviceRule.createVirtualDevice(new VirtualDeviceParams.Builder().build());
- IVirtualCamera.Default camera = new IVirtualCamera.Default();
- VirtualCameraConfig config = createVirtualCameraConfig(null);
- virtualDevice.registerVirtualCamera(camera);
- FakeVirtualCameraService fakeVirtualCameraService = new FakeVirtualCameraService();
-
- // Only add the service after connecting the camera
- virtualCameraController.onServiceConnected(
- ComponentName.createRelative(PKG, CLS), fakeVirtualCameraService.asBinder());
-
- assertThat(fakeVirtualCameraService.mCameras).contains(camera);
- }
-
- @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
- @Test
- public void getCameraConfiguration() {
- VirtualDeviceImpl virtualDevice = createVirtualDevice();
- VirtualCameraSession virtualCameraSession = new VirtualCameraSession() {};
- VirtualCameraConfig config =
- new VirtualCameraConfig.Builder()
- .addStreamConfiguration(10, 10, ImageFormat.RGB_565)
- .setDisplayName(CAMERA_DISPLAY_NAME)
- .setCallback(
- new HandlerExecutor(new Handler(Looper.getMainLooper())),
- () -> virtualCameraSession)
- .build();
-
- VirtualCamera virtualCamera = new VirtualCamera(virtualDevice, config);
-
- VirtualCameraConfig returnedConfig = virtualCamera.getConfig();
- assertThat(returnedConfig).isNotNull();
- assertThat(returnedConfig.getDisplayName()).isEqualTo(CAMERA_DISPLAY_NAME);
- Set<VirtualCameraStreamConfig> streamConfigs = returnedConfig.getStreamConfigs();
- assertThat(streamConfigs).hasSize(1);
- VirtualCameraStreamConfig streamConfig =
- streamConfigs.toArray(new VirtualCameraStreamConfig[0])[0];
- assertThat(streamConfig.format).isEqualTo(ImageFormat.RGB_565);
- assertThat(streamConfig.width).isEqualTo(10);
- assertThat(streamConfig.height).isEqualTo(10);
-
- VirtualCameraHalConfig halConfig = virtualCamera.getHalConfig();
- assertThat(halConfig).isNotNull();
- assertThat(halConfig.displayName).isEqualTo(CAMERA_DISPLAY_NAME);
- assertThat(halConfig.streamConfigs).asList().hasSize(1);
- assertThat(halConfig.streamConfigs[0].format).isEqualTo(ImageFormat.RGB_565);
- assertThat(halConfig.streamConfigs[0].width).isEqualTo(10);
- assertThat(halConfig.streamConfigs[0].height).isEqualTo(10);
- }
-
- @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
- @Test
- public void createCameraWithVirtualCameraInstance() {
- VirtualDeviceImpl virtualDevice = createVirtualDevice();
-
- VirtualCameraSession virtualCameraSession = new VirtualCameraSession() {};
- VirtualCameraConfig config = createVirtualCameraConfig(virtualCameraSession);
- VirtualCamera virtualCamera = new VirtualCamera(virtualDevice, config);
-
- assertThat(mFakeVirtualCameraService.mCameras).contains(virtualCamera);
- assertThat(virtualCamera.open()).isInstanceOf(IVirtualCameraSession.class);
- }
-
- @RequiresFlagsDisabled(Flags.FLAG_VIRTUAL_CAMERA)
- @Test
- public void createCameraDoesNothingWhenControllerIsNull() {
- mVirtualDeviceRule.withVirtualCameraControllerSupplier(() -> null);
- VirtualDeviceImpl virtualDevice = createVirtualDevice();
- IVirtualCamera.Default camera = new IVirtualCamera.Default();
- VirtualCameraConfig config = createVirtualCameraConfig(null);
- virtualDevice.registerVirtualCamera(camera);
-
- assertThat(mFakeVirtualCameraService.mCameras).doesNotContain(camera);
- }
-
- @NonNull
- private static VirtualCameraConfig createVirtualCameraConfig(
- VirtualCameraSession virtualCameraSession) {
- return new VirtualCameraConfig.Builder()
- .addStreamConfiguration(10, 10, ImageFormat.RGB_565)
- .setDisplayName(CAMERA_DISPLAY_NAME)
- .setCallback(
- new HandlerExecutor(new Handler(Looper.getMainLooper())),
- () -> virtualCameraSession)
- .build();
- }
-
- private static class FakeVirtualCameraService extends IVirtualCameraService.Stub {
-
- final Set<IVirtualCamera> mCameras = new HashSet<>();
-
- @Override
- public boolean registerCamera(IVirtualCamera camera) throws RemoteException {
- mCameras.add(camera);
- return true;
- }
-
- @Override
- public void unregisterCamera(IVirtualCamera camera) throws RemoteException {
- mCameras.remove(camera);
- }
- }
-}
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 f978990f0fac..30300ec3ad2e 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
@@ -1940,7 +1940,7 @@ public class VirtualDeviceManagerServiceTest {
mRunningAppsChangedCallback,
params,
new DisplayManagerGlobal(mIDisplayManager),
- new VirtualCameraController(mContext));
+ new VirtualCameraController());
mVdms.addVirtualDevice(virtualDeviceImpl);
assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId());
assertThat(virtualDeviceImpl.getPersistentDeviceId())
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java
deleted file mode 100644
index dbd6c889622a..000000000000
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * 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 com.android.server.companion.virtual;
-
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-
-import android.app.admin.DevicePolicyManager;
-import android.companion.AssociationInfo;
-import android.companion.virtual.IVirtualDeviceActivityListener;
-import android.companion.virtual.IVirtualDeviceSoundEffectListener;
-import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
-import android.content.AttributionSource;
-import android.content.Context;
-import android.hardware.display.DisplayManagerGlobal;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.IDisplayManager;
-import android.net.MacAddress;
-import android.os.Binder;
-import android.testing.TestableContext;
-import android.util.ArraySet;
-import android.view.Display;
-import android.view.DisplayInfo;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.LocalServices;
-import com.android.server.companion.virtual.camera.VirtualCameraController;
-import com.android.server.input.InputManagerInternal;
-import com.android.server.sensors.SensorManagerInternal;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Objects;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/** Test rule to generate instances of {@link VirtualDeviceImpl}. */
-public class VirtualDeviceRule implements TestRule {
-
- private static final int DEVICE_OWNER_UID = 50;
- private static final int VIRTUAL_DEVICE_ID = 42;
-
- private final Context mContext;
- private InputController mInputController;
- private CameraAccessController mCameraAccessController;
- private AssociationInfo mAssociationInfo;
- private VirtualDeviceManagerService mVdms;
- private VirtualDeviceManagerInternal mLocalService;
- private VirtualDeviceLog mVirtualDeviceLog;
-
- // Mocks
- @Mock private InputController.NativeWrapper mNativeWrapperMock;
- @Mock private DisplayManagerInternal mDisplayManagerInternalMock;
- @Mock private IDisplayManager mIDisplayManager;
- @Mock private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback;
- @Mock private DevicePolicyManager mDevicePolicyManagerMock;
- @Mock private InputManagerInternal mInputManagerInternalMock;
- @Mock private SensorManagerInternal mSensorManagerInternalMock;
- @Mock private IVirtualDeviceActivityListener mActivityListener;
- @Mock private IVirtualDeviceSoundEffectListener mSoundEffectListener;
- @Mock private Consumer<ArraySet<Integer>> mRunningAppsChangedCallback;
- @Mock private CameraAccessController.CameraAccessBlockedCallback mCameraAccessBlockedCallback;
-
- // Test instance suppliers
- private Supplier<VirtualCameraController> mVirtualCameraControllerSupplier;
-
- /**
- * Create a new {@link VirtualDeviceRule}
- *
- * @param context The context to be used with the rule.
- */
- public VirtualDeviceRule(@NonNull Context context) {
- Objects.requireNonNull(context);
- mContext = context;
- }
-
- /**
- * Sets a supplier that will supply an instance of {@link VirtualCameraController}. If the
- * supplier returns null, a new instance will be created.
- */
- public VirtualDeviceRule withVirtualCameraControllerSupplier(
- Supplier<VirtualCameraController> virtualCameraControllerSupplier) {
- mVirtualCameraControllerSupplier = virtualCameraControllerSupplier;
- return this;
- }
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- init(new TestableContext(mContext));
- base.evaluate();
- }
- };
- }
-
- private void init(@NonNull TestableContext context) {
- MockitoAnnotations.initMocks(this);
-
- LocalServices.removeServiceForTest(DisplayManagerInternal.class);
- LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
-
- doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
- doNothing().when(mInputManagerInternalMock).setPointerAcceleration(anyFloat(), anyInt());
- doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
- 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 = "uniqueId";
- doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt());
- doReturn(Display.INVALID_DISPLAY).when(mDisplayManagerInternalMock)
- .getDisplayIdToMirror(anyInt());
- LocalServices.removeServiceForTest(DisplayManagerInternal.class);
- LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
-
- context.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManagerMock);
-
- // Allow virtual devices to be created on the looper thread for testing.
- final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
- mInputController =
- new InputController(
- mNativeWrapperMock,
- InstrumentationRegistry.getInstrumentation()
- .getContext()
- .getMainThreadHandler(),
- context.getSystemService(WindowManager.class),
- threadVerifier);
- mCameraAccessController =
- new CameraAccessController(context, mLocalService, mCameraAccessBlockedCallback);
-
- mAssociationInfo =
- new AssociationInfo(
- /* associationId= */ 1,
- 0,
- null,
- null,
- MacAddress.BROADCAST_ADDRESS,
- "",
- null,
- null,
- true,
- false,
- false,
- 0,
- 0,
- -1);
-
- mVdms = new VirtualDeviceManagerService(context);
- mLocalService = mVdms.getLocalServiceInstance();
- mVirtualDeviceLog = new VirtualDeviceLog(context);
- }
-
- /**
- * Create a {@link VirtualDeviceImpl} with the required mocks
- *
- * @param params See {@link
- * android.companion.virtual.VirtualDeviceManager#createVirtualDevice(int,
- * VirtualDeviceParams)}
- */
- public VirtualDeviceImpl createVirtualDevice(VirtualDeviceParams params) {
- VirtualCameraController virtualCameraController = mVirtualCameraControllerSupplier.get();
- if (Flags.virtualCamera()) {
- if (virtualCameraController == null) {
- virtualCameraController = new VirtualCameraController(mContext);
- }
- }
-
- VirtualDeviceImpl virtualDeviceImpl =
- new VirtualDeviceImpl(
- mContext,
- mAssociationInfo,
- mVdms,
- mVirtualDeviceLog,
- new Binder(),
- new AttributionSource(
- DEVICE_OWNER_UID,
- "com.android.virtualdevice.test",
- "virtualdevicerule"),
- VIRTUAL_DEVICE_ID,
- mInputController,
- mCameraAccessController,
- mPendingTrampolineCallback,
- mActivityListener,
- mSoundEffectListener,
- mRunningAppsChangedCallback,
- params,
- new DisplayManagerGlobal(mIDisplayManager),
- virtualCameraController);
- mVdms.addVirtualDevice(virtualDeviceImpl);
- return virtualDeviceImpl;
- }
-}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
new file mode 100644
index 000000000000..258302354e45
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 com.android.server.companion.virtual.camera;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.companion.virtual.camera.VirtualCameraCallback;
+import android.companion.virtual.camera.VirtualCameraConfig;
+import android.companion.virtual.camera.VirtualCameraMetadata;
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtualcamera.IVirtualCameraService;
+import android.companion.virtualcamera.VirtualCameraConfiguration;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Surface;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class VirtualCameraControllerTest {
+
+ private static final int CAMERA_DISPLAY_NAME_RES_ID_1 = 10;
+ private static final int CAMERA_WIDTH_1 = 100;
+ private static final int CAMERA_HEIGHT_1 = 200;
+ private static final int CAMERA_FORMAT_1 = ImageFormat.RGB_565;
+
+ private static final int CAMERA_DISPLAY_NAME_RES_ID_2 = 11;
+ private static final int CAMERA_WIDTH_2 = 400;
+ private static final int CAMERA_HEIGHT_2 = 600;
+ private static final int CAMERA_FORMAT_2 = ImageFormat.YUY2;
+
+ @Mock
+ private IVirtualCameraService mVirtualCameraServiceMock;
+
+ private VirtualCameraController mVirtualCameraController;
+ private final HandlerExecutor mCallbackHandler =
+ new HandlerExecutor(new Handler(Looper.getMainLooper()));
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock);
+ when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true);
+ }
+
+ @Test
+ public void registerCamera_registersCamera() throws Exception {
+ mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1));
+
+ ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
+ ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
+ verify(mVirtualCameraServiceMock).registerCamera(any(), configurationCaptor.capture());
+ VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
+ assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
+ assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
+ CAMERA_HEIGHT_1, CAMERA_FORMAT_1);
+ }
+
+ @Test
+ public void unregisterCamera_unregistersCamera() throws Exception {
+ VirtualCameraConfig config = createVirtualCameraConfig(
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1);
+ mVirtualCameraController.unregisterCamera(config);
+
+ verify(mVirtualCameraServiceMock).unregisterCamera(any());
+ }
+
+ @Test
+ public void close_unregistersAllCameras() throws Exception {
+ mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1));
+ mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+ CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_DISPLAY_NAME_RES_ID_2));
+
+ ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
+ ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
+ mVirtualCameraController.close();
+ verify(mVirtualCameraServiceMock, times(2)).registerCamera(any(),
+ configurationCaptor.capture());
+ List<VirtualCameraConfiguration> virtualCameraConfigurations =
+ configurationCaptor.getAllValues();
+ assertThat(virtualCameraConfigurations).hasSize(2);
+ assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1,
+ CAMERA_HEIGHT_1, CAMERA_FORMAT_1);
+ assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2,
+ CAMERA_HEIGHT_2, CAMERA_FORMAT_2);
+ }
+
+ private VirtualCameraConfig createVirtualCameraConfig(
+ int width, int height, int format, int displayNameResId) {
+ return new VirtualCameraConfig.Builder()
+ .addStreamConfig(width, height, format)
+ .setDisplayNameStringRes(displayNameResId)
+ .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback())
+ .build();
+ }
+
+ private static void assertVirtualCameraConfiguration(
+ VirtualCameraConfiguration configuration, int width, int height, int format) {
+ assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width);
+ assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height);
+ assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format);
+ }
+
+ private static VirtualCameraCallback createNoOpCallback() {
+ return new VirtualCameraCallback() {
+
+ @Override
+ public void onStreamConfigured(
+ int streamId,
+ @NonNull Surface surface,
+ @NonNull VirtualCameraStreamConfig streamConfig) {}
+
+ @Override
+ public void onProcessCaptureRequest(
+ int streamId, long frameId, @Nullable VirtualCameraMetadata metadata) {}
+
+ @Override
+ public void onStreamClosed(int streamId) {}
+ };
+ }
+}