diff options
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) {} + }; + } +} |