Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f1d97412 authored by Dorin Drimus's avatar Dorin Drimus Committed by Android (Google) Code Review
Browse files

Merge "Add CameraCharacteristics in VirtualCameraConfig" into main

parents bdc30cb9 7188d1d3
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -3658,6 +3658,13 @@ package android.companion.virtual.audio {
package android.companion.virtual.camera {
  @FlaggedApi("android.companion.virtualdevice.flags.virtual_camera_metadata") public final class CameraCharacteristicsBuilder {
    ctor public CameraCharacteristicsBuilder();
    ctor public CameraCharacteristicsBuilder(@NonNull android.hardware.camera2.CameraCharacteristics);
    method @NonNull public android.hardware.camera2.CameraCharacteristics build();
    method @NonNull public <T> android.companion.virtual.camera.CameraCharacteristicsBuilder set(@NonNull android.hardware.camera2.CameraCharacteristics.Key<T>, T);
  }
  public final class VirtualCamera implements java.io.Closeable {
    method public void close();
    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
+84 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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.SuppressLint;
import android.annotation.SystemApi;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.impl.CameraMetadataNative;

/**
 * Builder class for creating {@link CameraCharacteristics} instances to be used in a
 * {@link VirtualCameraConfig}.
 * @hide
 */
// There is no CameraCharacteristics.Builder for now, so this helps VDM clients to build
// CameraCharacteristics instances for their VirtualCameraConfig.
@SuppressLint("TopLevelBuilder")
@SystemApi
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_CAMERA_METADATA)
public final class CameraCharacteristicsBuilder {
    private final CameraMetadataNative mNativeMetadata;

    /**
     * Builder for creating {@link CameraCharacteristics} starting from an empty list of keys.
     */
    public CameraCharacteristicsBuilder() {
        mNativeMetadata = new CameraMetadataNative();
    }

    /**
     * Builder for creating {@link CameraCharacteristics} starting from a copy of
     * the passed characteristics.
     */
    public CameraCharacteristicsBuilder(@NonNull CameraCharacteristics characteristics) {
        mNativeMetadata = new CameraMetadataNative(characteristics.getNativeMetadata());
    }

    /**
     * Set a camera characteristics field to a value. The field definitions can be found in
     * {@link CameraCharacteristics}.
     *
     * <p>Setting a field to {@code null} will remove that field from the camera
     * characteristics.
     * Unless the field is optional, removing it will likely produce an error from the camera
     * device when the camera characteristics are set.</p>
     *
     * @param key   The metadata field to write.
     * @param value The value to set the field to, which must be of a matching type to the key.
     */
    @SuppressLint("KotlinOperator")
    @NonNull
    public <T> CameraCharacteristicsBuilder set(@NonNull CameraCharacteristics.Key<T> key,
            T value) {
        mNativeMetadata.set(key, value);
        return this;
    }

    /**
     * Builds the {@link CameraCharacteristics} object with the set
     * {@link CameraCharacteristics.Key}s.
     *
     * @return A new {@link CameraCharacteristics} instance to be set in the
     * {@link VirtualCameraConfig} of a Virtual Camera.
     */
    public @NonNull CameraCharacteristics build() {
        return new CameraCharacteristics(mNativeMetadata);
    }
}
+20 −1
Original line number Diff line number Diff line
@@ -113,6 +113,10 @@ public final class VirtualCameraConfig implements Parcelable {
                throw new IllegalArgumentException("Different values are set for "
                        + "lensFacing and CameraCharacteristics.LENS_FACING");
            }
        } else {
            if (lensFacing == LENS_FACING_UNKNOWN) {
                throw new IllegalArgumentException("Lens facing must be set");
            }
        }
        mLensFacing = lensFacing;
        mStreamConfigurations =
@@ -226,6 +230,9 @@ public final class VirtualCameraConfig implements Parcelable {
     */
    @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA_METADATA)
    public boolean isPerFrameCameraMetadataEnabled() {
        if (!Flags.virtualCameraMetadata()) {
            throw new UnsupportedOperationException("virtual_camera_metadata not enabled!");
        }
        return mPerFrameCameraMetadataEnabled;
    }

@@ -237,6 +244,9 @@ public final class VirtualCameraConfig implements Parcelable {
    @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA_METADATA)
    @Nullable
    public CameraCharacteristics getCameraCharacteristics() {
        if (!Flags.virtualCameraMetadata()) {
            throw new UnsupportedOperationException("virtual_camera_metadata not enabled!");
        }
        return mCameraCharacteristics;
    }

@@ -247,7 +257,8 @@ public final class VirtualCameraConfig implements Parcelable {
     * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}.
     * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
     *     VirtualCameraCallback)}
     * <li>A lens facing must be set with {@link #setLensFacing(int)}
     * <li>A lens facing must be set with {@link #setLensFacing(int)} or
     * {@link CameraCharacteristics} with {@link #setCameraCharacteristics(CameraCharacteristics)}
     */
    public static final class Builder {

@@ -317,6 +328,7 @@ public final class VirtualCameraConfig implements Parcelable {
        /**
         * Sets the sensor orientation of the virtual camera. This field is optional and can be
         * omitted (defaults to {@link #SENSOR_ORIENTATION_0}).
         * <p>Only used if camera characteristics are not set.
         *
         * @param sensorOrientation The sensor orientation of the camera, which represents the
         *                          clockwise angle (in degrees) through which the output image
@@ -338,6 +350,7 @@ public final class VirtualCameraConfig implements Parcelable {

        /**
         * Sets the lens facing direction of the virtual camera.
         * <p>Only used if camera characteristics are not set.
         *
         * <p>A {@link VirtualDevice} can have at most one {@link VirtualCamera} with
         * {@link CameraMetadata#LENS_FACING_FRONT} and at most one {@link VirtualCamera} with
@@ -384,6 +397,9 @@ public final class VirtualCameraConfig implements Parcelable {
        @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA_METADATA)
        @NonNull
        public Builder setPerFrameCameraMetadataEnabled(boolean perFrameCameraMetadataEnabled) {
            if (!Flags.virtualCameraMetadata()) {
                throw new UnsupportedOperationException("virtual_camera_metadata not enabled!");
            }
            mPerFrameCameraMetadataEnabled = perFrameCameraMetadataEnabled;
            return this;
        }
@@ -404,6 +420,9 @@ public final class VirtualCameraConfig implements Parcelable {
        @NonNull
        public Builder setCameraCharacteristics(
                @Nullable CameraCharacteristics cameraCharacteristics) {
            if (!Flags.virtualCameraMetadata()) {
                throw new UnsupportedOperationException("virtual_camera_metadata not enabled!");
            }
            mCameraCharacteristics = cameraCharacteristics;
            return this;
        }
+32 −3
Original line number Diff line number Diff line
@@ -24,15 +24,21 @@ import android.companion.virtualcamera.Format;
import android.companion.virtualcamera.IVirtualCameraService;
import android.companion.virtualcamera.SupportedStreamConfiguration;
import android.companion.virtualcamera.VirtualCameraConfiguration;
import android.companion.virtualcamera.VirtualCameraMetadata;
import android.companion.virtualdevice.flags.Flags;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.hardware.camera2.CameraCharacteristics;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Slog;
import android.view.Surface;

/** Utilities to convert the client side classes to the virtual camera service ones. */
public final class VirtualCameraConversionUtil {

    private static final String TAG = "VirtualCameraConversionUtil";

    /**
     * Fetches the configuration of the provided virtual cameraConfig that was provided by its owner
     * and convert it into the {@link IVirtualCameraService} types: {@link
@@ -44,8 +50,7 @@ public final class VirtualCameraConversionUtil {
     */
    @NonNull
    public static android.companion.virtualcamera.VirtualCameraConfiguration
            getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig)
                    throws RemoteException {
            getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig) {
        VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration();
        serviceConfiguration.supportedStreamConfigs =
                cameraConfig.getStreamConfigs().stream()
@@ -54,6 +59,12 @@ public final class VirtualCameraConversionUtil {
        serviceConfiguration.sensorOrientation = cameraConfig.getSensorOrientation();
        serviceConfiguration.lensFacing = cameraConfig.getLensFacing();
        serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback());
        if (Flags.virtualCameraMetadata()) {
            serviceConfiguration.perFrameCameraMetadataEnabled =
                    cameraConfig.isPerFrameCameraMetadataEnabled();
            serviceConfiguration.cameraCharacteristics = convertToVirtualCameraMetadata(
                    cameraConfig.getCameraCharacteristics());
        }
        return serviceConfiguration;
    }

@@ -114,6 +125,24 @@ public final class VirtualCameraConversionUtil {
        };
    }

    private VirtualCameraConversionUtil() {
    private static VirtualCameraMetadata convertToVirtualCameraMetadata(
            CameraCharacteristics cameraCharacteristics) {
        if (cameraCharacteristics == null) {
            return null;
        }

        VirtualCameraMetadata virtualCameraMetadata = new VirtualCameraMetadata();
        Parcel parcel = Parcel.obtain();
        try {
            cameraCharacteristics.getNativeMetadata().writeToParcel(parcel, 0);
            parcel.setDataPosition(0);
            virtualCameraMetadata.metadata = parcel.readBlob();
        } catch (Exception e) {
            Slog.w(TAG, "Failed to convert CameraCharacteristics to VirtualCameraMetadata.");
            return null;
        } finally {
            parcel.recycle();
        }
        return virtualCameraMetadata;
    }
}
+89 −0
Original line number Diff line number Diff line
@@ -26,8 +26,11 @@ import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK;
import static android.hardware.camera2.CameraMetadata.LENS_FACING_EXTERNAL;
import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT;

import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -35,12 +38,16 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.companion.virtual.camera.CameraCharacteristicsBuilder;
import android.companion.virtual.camera.VirtualCameraCallback;
import android.companion.virtual.camera.VirtualCameraConfig;
import android.companion.virtual.camera.VirtualCameraStreamConfig;
import android.companion.virtualcamera.IVirtualCameraService;
import android.companion.virtualcamera.VirtualCameraConfiguration;
import android.companion.virtualcamera.VirtualCameraMetadata;
import android.companion.virtualdevice.flags.Flags;
import android.content.AttributionSource;
import android.hardware.camera2.CameraCharacteristics;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
@@ -50,6 +57,8 @@ import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.TestableLooper;

import com.google.common.collect.Iterables;

import junitparams.JUnitParamsRunner;
import junitparams.Parameters;

@@ -260,6 +269,33 @@ public class VirtualCameraControllerTest {
                        LENS_FACING_EXTERNAL), AttributionSource.myAttributionSource()));
    }

    public static void assertVirtualCameraConfigFromCharacteristics(VirtualCameraConfig config,
            int width, int height, int format, int maximumFramesPerSecond,
            int characteristicSensorOrientation, int characteristicLensFacing, String name) {
        assertThat(config.getName()).isEqualTo(name);
        assertThat(config.getStreamConfigs()).hasSize(1);
        VirtualCameraStreamConfig streamConfig =
                Iterables.getOnlyElement(config.getStreamConfigs());
        assertThat(streamConfig.getWidth()).isEqualTo(width);
        assertThat(streamConfig.getHeight()).isEqualTo(height);
        assertThat(streamConfig.getFormat()).isEqualTo(format);
        assertThat(streamConfig.getMaximumFramesPerSecond()).isEqualTo(maximumFramesPerSecond);
        assertThat(config.getCameraCharacteristics().get(CameraCharacteristics.SENSOR_ORIENTATION))
                .isEqualTo(characteristicSensorOrientation);
        assertThat(config.getCameraCharacteristics().get(CameraCharacteristics.LENS_FACING))
                .isEqualTo(characteristicLensFacing);
    }

    private static void assertVirtualCameraConfigurationWithCharacteristics(
            VirtualCameraConfiguration configuration, int width, int height, int format,
            int maxFps, VirtualCameraMetadata expectedMetadata) {
        assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width);
        assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height);
        assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format);
        assertThat(configuration.supportedStreamConfigs[0].maxFps).isEqualTo(maxFps);
        assertArrayEquals(configuration.cameraCharacteristics.metadata, expectedMetadata.metadata);
    }

    private VirtualCameraConfig createVirtualCameraConfig(
            int width, int height, int format, int maximumFramesPerSecond,
            String name, int sensorOrientation, int lensFacing) {
@@ -282,6 +318,59 @@ public class VirtualCameraControllerTest {
        assertThat(configuration.lensFacing).isEqualTo(lensFacing);
    }

    @Parameters(method = "getAllLensFacingDirections")
    @Test
    @EnableFlags(Flags.FLAG_VIRTUAL_CAMERA_METADATA)
    public void registerCameraWithCharacteristics_registersCamera(int lensFacing) throws Exception {
        CameraCharacteristics characteristics = new CameraCharacteristicsBuilder()
                .set(CameraCharacteristics.SENSOR_ORIENTATION, CAMERA_SENSOR_ORIENTATION_1)
                .set(CameraCharacteristics.LENS_FACING, lensFacing)
                .build();

        VirtualCameraConfig config = new VirtualCameraConfig.Builder(CAMERA_NAME_1)
                .addStreamConfig(CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1)
                .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock)
                .setCameraCharacteristics(characteristics)
                .build();

        VirtualCameraConfiguration originalConfig = getServiceCameraConfiguration(config);

        mVirtualCameraController.registerCamera(config, AttributionSource.myAttributionSource());

        ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
                ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
        ArgumentCaptor<Integer> deviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
        verify(mVirtualCameraServiceMock).registerCamera(any(), configurationCaptor.capture(),
                deviceIdCaptor.capture());
        assertThat(deviceIdCaptor.getValue()).isEqualTo(DEVICE_ID);
        VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
        assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
        assertVirtualCameraConfigurationWithCharacteristics(virtualCameraConfiguration,
                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1,
                originalConfig.cameraCharacteristics);
    }

    @Test
    @EnableFlags(Flags.FLAG_VIRTUAL_CAMERA_METADATA)
    public void unregisterCameraWithCharacteristics_unregistersCamera() throws Exception {
        CameraCharacteristics characteristics = new CameraCharacteristicsBuilder()
                .set(CameraCharacteristics.SENSOR_ORIENTATION, CAMERA_SENSOR_ORIENTATION_1)
                .set(CameraCharacteristics.LENS_FACING, CAMERA_LENS_FACING_1)
                .build();

        VirtualCameraConfig config = new VirtualCameraConfig.Builder(CAMERA_NAME_1)
                .addStreamConfig(CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1)
                .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock)
                .setCameraCharacteristics(characteristics)
                .build();

        mVirtualCameraController.registerCamera(config, AttributionSource.myAttributionSource());

        mVirtualCameraController.unregisterCamera(config);

        verify(mVirtualCameraServiceMock).unregisterCamera(any());
    }

    @SuppressWarnings("unused") // Parameter for parametrized tests
    private static Integer[] getFixedCamerasLensFacingDirections() {
        return new Integer[]{