@@ -103,6 +103,14 @@
override fun getLensFacing(): Int = getCameraSelectorLensFacing(cameraProperties.metadata[CameraCharacteristics.LENS_FACING]!!)+ override fun getCameraCharacteristics(): Any {+ TODO("Not yet implemented")+ }++ override fun getPhysicalCameraCharacteristics(physicalCameraId: String): Any? {+ TODO("Not yet implemented")+ }+ @CameraSelector.LensFacing private fun getCameraSelectorLensFacing(lensFacingInt: Int): Int { return when (lensFacingInt) {
@@ -108,6 +108,14 @@
throw NotImplementedError("Not used in testing") }+ override fun getCameraCharacteristics(): Any {+ throw NotImplementedError("Not used in testing")+ }++ override fun getPhysicalCameraCharacteristics(physicalCameraId: String): Any? {+ throw NotImplementedError("Not used in testing")+ }+ override fun hasFlashUnit(): Boolean { throw NotImplementedError("Not used in testing") }
@@ -1,65 +0,0 @@
-/*- * Copyright 2019 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 androidx.camera.camera2.impl;--import android.hardware.camera2.CaptureFailure;-import android.hardware.camera2.CaptureResult;--import androidx.annotation.NonNull;-import androidx.annotation.Nullable;-import androidx.annotation.RequiresApi;-import androidx.camera.camera2.internal.Camera2CameraCaptureFailure;-import androidx.camera.camera2.internal.Camera2CameraCaptureResult;-import androidx.camera.core.impl.CameraCaptureFailure;-import androidx.camera.core.impl.CameraCaptureResult;--/**-* An utility class to convert {@link CameraCaptureResult} to camera2 {@link CaptureResult}.-*/-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java-public final class Camera2CameraCaptureResultConverter {- /**- * Converts {@link CameraCaptureResult} to camera2 {@link CaptureResult}.- *- * @return The CaptureResult instance or {@code null} if there is no underlying CaptureResult.- */- @Nullable- public static CaptureResult getCaptureResult(- @Nullable CameraCaptureResult cameraCaptureResult) {- if (cameraCaptureResult instanceof Camera2CameraCaptureResult) {- return ((Camera2CameraCaptureResult) cameraCaptureResult).getCaptureResult();- } else {- return null;- }- }-- /**- * Converts {@link CameraCaptureFailure} to camera2 {@link CaptureFailure}.- *- * @return The CaptureFailure instance or {@code null} if there is no underlying CaptureFailure.- */- @Nullable- public static CaptureFailure getCaptureFailure(- @NonNull CameraCaptureFailure cameraCaptureFailure) {- if (cameraCaptureFailure instanceof Camera2CameraCaptureFailure) {- return ((Camera2CameraCaptureFailure) cameraCaptureFailure).getCaptureFailure();- } else {- return null;- }- }-- private Camera2CameraCaptureResultConverter() {}-}
@@ -1,89 +0,0 @@
-/*- * Copyright 2019 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 androidx.camera.camera2.impl;--import static com.google.common.truth.Truth.assertThat;--import android.hardware.camera2.CaptureFailure;-import android.hardware.camera2.CaptureResult;-import android.os.Build;--import androidx.camera.camera2.internal.Camera2CameraCaptureFailure;-import androidx.camera.camera2.internal.Camera2CameraCaptureResult;-import androidx.camera.core.impl.CameraCaptureFailure;-import androidx.camera.core.impl.CameraCaptureResult;-import androidx.camera.core.impl.TagBundle;--import org.junit.Test;-import org.junit.runner.RunWith;-import org.mockito.Mockito;-import org.robolectric.RobolectricTestRunner;-import org.robolectric.annotation.Config;-import org.robolectric.annotation.internal.DoNotInstrument;--@RunWith(RobolectricTestRunner.class)-@DoNotInstrument-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)-public final class Camera2CameraCaptureResultConverterTest {- private CaptureResult mCaptureResult = Mockito.mock(CaptureResult.class);-- @Test- public void canRetrieveCaptureResult() {- CameraCaptureResult cameraCaptureResult =- new Camera2CameraCaptureResult(TagBundle.emptyBundle(), mCaptureResult);-- CaptureResult captureResult = Camera2CameraCaptureResultConverter.getCaptureResult(- cameraCaptureResult);-- assertThat(captureResult).isSameInstanceAs(mCaptureResult);- }-- @Test- public void retrieveNullIfNotCamera2CameraCaptureResult() {- CameraCaptureResult cameraCaptureResult =- new CameraCaptureResult.EmptyCameraCaptureResult();-- CaptureResult captureResult = Camera2CameraCaptureResultConverter.getCaptureResult(- cameraCaptureResult);-- assertThat(captureResult).isNull();- }-- @Test- public void canRetrieveCaptureFailure() {- CaptureFailure captureFailure = Mockito.mock(CaptureFailure.class);- CameraCaptureFailure cameraCaptureFailure =- new Camera2CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR, captureFailure);-- CaptureFailure retrievedCaptureResult =- Camera2CameraCaptureResultConverter.getCaptureFailure(- cameraCaptureFailure);-- assertThat(retrievedCaptureResult).isSameInstanceAs(captureFailure);- }-- @Test- public void retrieveNullIfNotCamera2CameraCaptureFailure() {- CameraCaptureFailure cameraCaptureFailure =- new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR);-- CaptureFailure captureFailure = Camera2CameraCaptureResultConverter.getCaptureFailure(- cameraCaptureFailure);-- assertThat(captureFailure).isNull();- }-}
@@ -17,6 +17,7 @@
package androidx.camera.core.impl; import androidx.annotation.NonNull;+import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; /**@@ -51,4 +52,13 @@
public enum Reason { ERROR, }++ /**+ * Returns the capture failure object of the current implementation. In camera2 implementation,+ * the object is of type {@link android.hardware.camera2.CaptureFailure}.+ */+ @Nullable+ public Object getCaptureFailure() {+ return null;+ } }
@@ -22,6 +22,7 @@
import android.util.Size; import androidx.annotation.NonNull;+import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.camera.core.CameraInfo; import androidx.camera.core.CameraSelector;@@ -54,6 +55,26 @@
String getCameraId(); /**+ * Returns the camera characteristics of this camera. The actual type is determined by the+ * underlying camera implementation. For camera2 implementation, the actual type of the+ * returned object is {@link android.hardware.camera2.CameraCharacteristics}.+ */+ @NonNull+ Object getCameraCharacteristics();++ /**+ * Returns the camera characteristics of the specified physical camera id associated with+ * the current camera.+ *+ *
It returns {@code null} if the physical camera id does not belong to
+ * the current logical camera. The actual type is determined by the underlying camera+ * implementation. For camera2 implementation, the actual type of the returned object is+ * {@link android.hardware.camera2.CameraCharacteristics}.+ */+ @Nullable+ Object getPhysicalCameraCharacteristics(@NonNull String physicalCameraId);++ /** * Adds a {@link CameraCaptureCallback} which will be invoked when session capture request is * completed, failed or cancelled. *
@@ -20,7 +20,6 @@
import android.util.Range import android.util.Size import androidx.annotation.NonNull-import androidx.camera.camera2.interop.Camera2CameraInfo import androidx.camera.core.Camera import androidx.camera.core.CameraInfo import androidx.camera.core.CameraSelector@@ -30,6 +29,7 @@
import androidx.camera.extensions.impl.ExtensionsTestlibControl import androidx.camera.extensions.internal.ClientVersion import androidx.camera.extensions.internal.ExtensionVersion+import androidx.camera.extensions.internal.ExtensionsUtils import androidx.camera.extensions.internal.VendorExtender import androidx.camera.extensions.internal.Version import androidx.camera.extensions.util.ExtensionsTestUtil@@ -207,8 +207,8 @@
) for (cameraInfo in cameraProvider.availableCameraInfos) {- val characteristics = Camera2CameraInfo.extractCameraCharacteristics(cameraInfo)-+ val characteristics =+ (cameraInfo as CameraInfoInternal).cameraCharacteristics as CameraCharacteristics // Checks lens facing first val currentLensFacing = characteristics.get(CameraCharacteristics.LENS_FACING) if (currentLensFacing != lensFacing) {@@ -236,7 +236,8 @@
// Checks each camera in the available camera list that the selected camera must be the // first one supporting the specified extension mode in the same lens facing for (cameraInfo in cameraProvider.availableCameraInfos) {- val characteristics = Camera2CameraInfo.extractCameraCharacteristics(cameraInfo)+ val characteristics =+ (cameraInfo as CameraInfoInternal).cameraCharacteristics as CameraCharacteristics // Checks lens facing first val currentLensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)@@ -246,7 +247,7 @@
// Checks whether the specified extension mode is available by camera info val isSupported = isExtensionAvailableByCameraInfo(cameraInfo)- val currentCameraId = (cameraInfo as CameraInfoInternal).cameraId+ val currentCameraId = cameraInfo.cameraId if (currentCameraId.equals(cameraId)) { assertThat(isSupported).isTrue()@@ -632,11 +633,10 @@
private fun isExtensionAvailableByCameraInfo(cameraInfo: CameraInfo): Boolean { var vendorExtender = ExtensionsTestUtil.createVendorExtender(extensionMode) vendorExtender.init(cameraInfo)- val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo)- val cameraId = camera2CameraInfo.cameraId+ val cameraId = (cameraInfo as CameraInfoInternal).cameraId return vendorExtender.isExtensionAvailable(cameraId,- camera2CameraInfo.cameraCharacteristicsMap)+ ExtensionsUtils.getCameraCharacteristicsMap(cameraInfo)) } private fun createVideoCapture(): VideoCapture {
@@ -0,0 +1,299 @@
+/*+ * Copyright 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 androidx.camera.extensions.internal;++import android.graphics.Rect;+import android.hardware.camera2.CameraMetadata;+import android.hardware.camera2.CaptureResult;+import android.os.Build;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.camera.core.Logger;+import androidx.camera.core.impl.CameraCaptureMetaData;+import androidx.camera.core.impl.CameraCaptureResult;+import androidx.camera.core.impl.TagBundle;+import androidx.camera.core.impl.utils.ExifData;++import java.nio.BufferUnderflowException;++/**+ * The camera2 implementation for the capture result of a single image capture.+ *+ *
Copied from camera-camera2 since we don't want the camera-camera2 dependency but we need to
+ * generate the {@link CameraCaptureResult} from a+ * {@link android.hardware.camera2.CaptureResult} instance.+ */+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java+public class Camera2CameraCaptureResult implements CameraCaptureResult {+ private static final String TAG = "C2CameraCaptureResult";++ private final TagBundle mTagBundle;++ /** The actual camera2 {@link CaptureResult}. */+ private final CaptureResult mCaptureResult;++ public Camera2CameraCaptureResult(@NonNull TagBundle tagBundle,+ @NonNull CaptureResult captureResult) {+ mTagBundle = tagBundle;+ mCaptureResult = captureResult;+ }++ public Camera2CameraCaptureResult(@NonNull CaptureResult captureResult) {+ this(TagBundle.emptyBundle(), captureResult);+ }++ /**+ * Converts the camera2 {@link CaptureResult#CONTROL_AF_MODE} to+ * {@link CameraCaptureMetaData.AfMode}.+ *+ * @return the {@link CameraCaptureMetaData.AfMode}.+ */+ @NonNull+ @Override+ public CameraCaptureMetaData.AfMode getAfMode() {+ Integer mode = mCaptureResult.get(CaptureResult.CONTROL_AF_MODE);+ if (mode == null) {+ return CameraCaptureMetaData.AfMode.UNKNOWN;+ }+ switch (mode) {+ case CaptureResult.CONTROL_AF_MODE_OFF:+ case CaptureResult.CONTROL_AF_MODE_EDOF:+ return CameraCaptureMetaData.AfMode.OFF;+ case CaptureResult.CONTROL_AF_MODE_AUTO:+ case CaptureResult.CONTROL_AF_MODE_MACRO:+ return CameraCaptureMetaData.AfMode.ON_MANUAL_AUTO;+ case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE:+ case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO:+ return CameraCaptureMetaData.AfMode.ON_CONTINUOUS_AUTO;+ default: // fall out+ }+ Logger.e(TAG, "Undefined af mode: " + mode);+ return CameraCaptureMetaData.AfMode.UNKNOWN;+ }++ /**+ * Converts the camera2 {@link CaptureResult#CONTROL_AF_STATE} to+ * {@link CameraCaptureMetaData.AfState}.+ *+ * @return the {@link CameraCaptureMetaData.AfState}.+ */+ @NonNull+ @Override+ public CameraCaptureMetaData.AfState getAfState() {+ Integer state = mCaptureResult.get(CaptureResult.CONTROL_AF_STATE);+ if (state == null) {+ return CameraCaptureMetaData.AfState.UNKNOWN;+ }+ switch (state) {+ case CaptureResult.CONTROL_AF_STATE_INACTIVE:+ return CameraCaptureMetaData.AfState.INACTIVE;+ case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN:+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:+ return CameraCaptureMetaData.AfState.SCANNING;+ case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:+ return CameraCaptureMetaData.AfState.LOCKED_FOCUSED;+ case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:+ return CameraCaptureMetaData.AfState.LOCKED_NOT_FOCUSED;+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED:+ return CameraCaptureMetaData.AfState.PASSIVE_NOT_FOCUSED;+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:+ return CameraCaptureMetaData.AfState.PASSIVE_FOCUSED;++ default: // fall out+ }+ Logger.e(TAG, "Undefined af state: " + state);+ return CameraCaptureMetaData.AfState.UNKNOWN;+ }++ /**+ * Converts the camera2 {@link CaptureResult#CONTROL_AE_STATE} to+ * {@link CameraCaptureMetaData.AeState}.+ *+ * @return the {@link CameraCaptureMetaData.AeState}.+ */+ @NonNull+ @Override+ public CameraCaptureMetaData.AeState getAeState() {+ Integer state = mCaptureResult.get(CaptureResult.CONTROL_AE_STATE);+ if (state == null) {+ return CameraCaptureMetaData.AeState.UNKNOWN;+ }+ switch (state) {+ case CaptureResult.CONTROL_AE_STATE_INACTIVE:+ return CameraCaptureMetaData.AeState.INACTIVE;+ case CaptureResult.CONTROL_AE_STATE_SEARCHING:+ case CaptureResult.CONTROL_AE_STATE_PRECAPTURE:+ return CameraCaptureMetaData.AeState.SEARCHING;+ case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:+ return CameraCaptureMetaData.AeState.FLASH_REQUIRED;+ case CaptureResult.CONTROL_AE_STATE_CONVERGED:+ return CameraCaptureMetaData.AeState.CONVERGED;+ case CaptureResult.CONTROL_AE_STATE_LOCKED:+ return CameraCaptureMetaData.AeState.LOCKED;+ default: // fall out+ }+ Logger.e(TAG, "Undefined ae state: " + state);+ return CameraCaptureMetaData.AeState.UNKNOWN;+ }++ /**+ * Converts the camera2 {@link CaptureResult#CONTROL_AWB_STATE} to+ * {@link CameraCaptureMetaData.AwbState}.+ *+ * @return the {@link CameraCaptureMetaData.AwbState}.+ */+ @NonNull+ @Override+ public CameraCaptureMetaData.AwbState getAwbState() {+ Integer state = mCaptureResult.get(CaptureResult.CONTROL_AWB_STATE);+ if (state == null) {+ return CameraCaptureMetaData.AwbState.UNKNOWN;+ }+ switch (state) {+ case CaptureResult.CONTROL_AWB_STATE_INACTIVE:+ return CameraCaptureMetaData.AwbState.INACTIVE;+ case CaptureResult.CONTROL_AWB_STATE_SEARCHING:+ return CameraCaptureMetaData.AwbState.METERING;+ case CaptureResult.CONTROL_AWB_STATE_CONVERGED:+ return CameraCaptureMetaData.AwbState.CONVERGED;+ case CaptureResult.CONTROL_AWB_STATE_LOCKED:+ return CameraCaptureMetaData.AwbState.LOCKED;+ default: // fall out+ }+ Logger.e(TAG, "Undefined awb state: " + state);+ return CameraCaptureMetaData.AwbState.UNKNOWN;+ }++ /**+ * Converts the camera2 {@link CaptureResult#FLASH_STATE} to+ * {@link CameraCaptureMetaData.FlashState}.+ *+ * @return the {@link CameraCaptureMetaData.FlashState}.+ */+ @NonNull+ @Override+ public CameraCaptureMetaData.FlashState getFlashState() {+ Integer state = mCaptureResult.get(CaptureResult.FLASH_STATE);+ if (state == null) {+ return CameraCaptureMetaData.FlashState.UNKNOWN;+ }+ switch (state) {+ case CaptureResult.FLASH_STATE_UNAVAILABLE:+ case CaptureResult.FLASH_STATE_CHARGING:+ return CameraCaptureMetaData.FlashState.NONE;+ case CaptureResult.FLASH_STATE_READY:+ return CameraCaptureMetaData.FlashState.READY;+ case CaptureResult.FLASH_STATE_FIRED:+ case CaptureResult.FLASH_STATE_PARTIAL:+ return CameraCaptureMetaData.FlashState.FIRED;+ default: // fall out+ }+ Logger.e(TAG, "Undefined flash state: " + state);+ return CameraCaptureMetaData.FlashState.UNKNOWN;+ }++ /** {@inheritDoc} */+ @Override+ public long getTimestamp() {+ Long timestamp = mCaptureResult.get(CaptureResult.SENSOR_TIMESTAMP);+ if (timestamp == null) {+ return -1L;+ }++ return timestamp;+ }++ @NonNull+ @Override+ public TagBundle getTagBundle() {+ return mTagBundle;+ }++ @Override+ public void populateExifData(@NonNull ExifData.Builder exifData) {+ // Call interface default to set flash mode+ CameraCaptureResult.super.populateExifData(exifData);++ // Set dimensions+ Rect cropRegion = mCaptureResult.get(CaptureResult.SCALER_CROP_REGION);+ if (cropRegion != null) {+ exifData.setImageWidth(cropRegion.width())+ .setImageHeight(cropRegion.height());+ }++ // Set orientation+ try {+ Integer jpegOrientation = mCaptureResult.get(CaptureResult.JPEG_ORIENTATION);+ if (jpegOrientation != null) {+ exifData.setOrientationDegrees(jpegOrientation);+ }+ } catch (BufferUnderflowException exception) {+ // On certain devices, e.g. Pixel 3 XL API 31, getting JPEG orientation on YUV stream+ // throws BufferUnderflowException. The value will be overridden in post-processing+ // anyway, so it's safe to ignore.+ Logger.w(TAG, "Failed to get JPEG orientation.");+ }++ // Set exposure time+ Long exposureTimeNs = mCaptureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME);+ if (exposureTimeNs != null) {+ exifData.setExposureTimeNanos(exposureTimeNs);+ }++ // Set the aperture+ Float aperture = mCaptureResult.get(CaptureResult.LENS_APERTURE);+ if (aperture != null) {+ exifData.setLensFNumber(aperture);+ }++ // Set the ISO+ Integer iso = mCaptureResult.get(CaptureResult.SENSOR_SENSITIVITY);+ if (iso != null) {+ if (Build.VERSION.SDK_INT >= 24) {+ Integer postRawSensitivityBoost =+ mCaptureResult.get(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST);+ if (postRawSensitivityBoost != null) {+ iso *= (int) (postRawSensitivityBoost / 100f);+ }+ }+ exifData.setIso(iso);+ }++ // Set the focal length+ Float focalLength = mCaptureResult.get(CaptureResult.LENS_FOCAL_LENGTH);+ if (focalLength != null) {+ exifData.setFocalLength(focalLength);+ }++ // Set white balance MANUAL/AUTO+ Integer whiteBalanceMode = mCaptureResult.get(CaptureResult.CONTROL_AWB_MODE);+ if (whiteBalanceMode != null) {+ ExifData.WhiteBalanceMode wbMode = ExifData.WhiteBalanceMode.AUTO;+ if (whiteBalanceMode == CameraMetadata.CONTROL_AWB_MODE_OFF) {+ wbMode = ExifData.WhiteBalanceMode.MANUAL;+ }+ exifData.setWhiteBalanceMode(wbMode);+ }+ }++ @NonNull+ @Override+ public CaptureResult getCaptureResult() {+ return mCaptureResult;+ }+}
@@ -0,0 +1,95 @@
+/*+ * Copyright 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 androidx.camera.extensions.internal;++import android.hardware.camera2.CameraCharacteristics;+import android.os.Build;++import androidx.annotation.DoNotInline;+import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.camera.core.impl.CameraInfoInternal;++import java.util.Collections;+import java.util.LinkedHashMap;+import java.util.Map;+import java.util.Objects;+import java.util.Set;++/**+ * Utils for camera-extensions.+ */+@RequiresApi(21)+public class ExtensionsUtils {+ private ExtensionsUtils() {}++ /**+ * Returns a map consisting of the camera ids and the {@link CameraCharacteristics}s.+ *+ *
For every camera, the map contains at least the CameraCharacteristics for the camera id.
+ * If the camera is logical camera, it will also contain associated physical camera ids and+ * their CameraCharacteristics.+ *+ */+ @NonNull+ public static Map getCameraCharacteristicsMap(+ @NonNull CameraInfoInternal cameraInfoInternal) {+ LinkedHashMap map = new LinkedHashMap<>();+ String cameraId = cameraInfoInternal.getCameraId();+ CameraCharacteristics cameraCharacteristics =+ (CameraCharacteristics) cameraInfoInternal.getCameraCharacteristics();+ map.put(cameraId, cameraCharacteristics);++ if (Build.VERSION.SDK_INT < 28) {+ return map;+ }++ Set physicalCameraIds = Api28Impl.getPhysicalCameraIds(cameraCharacteristics);+ if (physicalCameraIds == null) {+ return map;+ }++ for (String physicalCameraId : physicalCameraIds) {+ if (Objects.equals(physicalCameraId, cameraId)) {+ continue;+ }+ map.put(physicalCameraId, (CameraCharacteristics)+ cameraInfoInternal.getPhysicalCameraCharacteristics(physicalCameraId));+ }+ return map;+ }++ /**+ * Nested class to avoid verification errors for methods introduced in API 28.+ */+ @RequiresApi(28)+ private static class Api28Impl {++ private Api28Impl() {+ }++ @DoNotInline+ static Set getPhysicalCameraIds(+ @NonNull CameraCharacteristics cameraCharacteristics) {+ try {+ return cameraCharacteristics.getPhysicalCameraIds();+ } catch (Exception e) {+ return Collections.emptySet();+ }+ }+ }+}
@@ -0,0 +1,102 @@
+/*+ * Copyright 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 androidx.camera.extensions.internal;++import android.hardware.camera2.CaptureRequest;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.annotation.VisibleForTesting;+import androidx.camera.core.impl.Config;+import androidx.camera.core.impl.MutableOptionsBundle;+import androidx.camera.core.impl.OptionsBundle;+import androidx.camera.core.impl.ReadableConfig;++/**+ * Builder for creating {@link Config} that contains capture request options.+ */+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java+public class RequestOptionConfig implements ReadableConfig {+ static final String CAPTURE_REQUEST_ID_STEM = "camera2.captureRequest.option.";++ @NonNull+ private Config mConfig;++ private RequestOptionConfig(@NonNull Config config) {+ mConfig = config;+ }++ @NonNull+ @Override+ public Config getConfig() {+ return mConfig;+ }++ @VisibleForTesting+ @NonNull+ static Option+ return Option.create(CAPTURE_REQUEST_ID_STEM + key.getName(),+ Object.class,+ key);+ }++ /**+ * Builder for constructing {@link RequestOptionConfig} instances.+ */+ public static class Builder {+ private MutableOptionsBundle mMutableOptionsBundle = MutableOptionsBundle.create();++ /**+ * Extract the capture request options from the given {@link Config} and create a+ * {@link Builder} consisting of these capture request options.+ */+ @NonNull+ public static Builder from(@NonNull Config config) {+ Builder builder = new Builder();+ config.findOptions(+ CAPTURE_REQUEST_ID_STEM,+ option -> {+ @SuppressWarnings("unchecked")+ Config.Option+ builder.mMutableOptionsBundle.insertOption(objectOpt,+ config.getOptionPriority(objectOpt),+ config.retrieveOption(objectOpt));+ return true;+ });+ return builder;+ }++ /**+ * Sets the capture request option.+ */+ @NonNull+ public Builder setCaptureRequestOption(+ @NonNull CaptureRequest.Key key, @NonNull ValueT value) {+ Option option = createOptionFromKey(key);+ mMutableOptionsBundle.insertOption(option, value);+ return this;+ }++ /**+ * Construct the instance.+ */+ @NonNull+ public RequestOptionConfig build() {+ return new RequestOptionConfig(OptionsBundle.from(mMutableOptionsBundle));+ }+ }+}
@@ -0,0 +1,92 @@
+/*+ * Copyright 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 androidx.camera.extensions.internal++import android.content.Context+import android.graphics.Rect+import android.hardware.camera2.CameraCharacteristics+import androidx.camera.testing.fakes.FakeCameraInfoInternal+import androidx.test.core.app.ApplicationProvider+import com.google.common.truth.Truth.assertThat+import org.junit.Test+import org.junit.runner.RunWith+import org.mockito.Mockito+import org.robolectric.RobolectricTestRunner+import org.robolectric.annotation.Config+import org.robolectric.annotation.internal.DoNotInstrument+import org.robolectric.shadow.api.Shadow+import org.robolectric.shadows.ShadowCameraManager++@RunWith(RobolectricTestRunner::class)+@DoNotInstrument+@Config(+ minSdk = 28,+ instrumentedPackages = arrayOf("androidx.camera.extensions.internal")+)+class ExtensionsUtilsTest {+ companion object {+ val ACTIVE_ARRAY_0 = Rect(0, 0, 1920, 1080)+ val ACTIVE_ARRAY_2 = Rect(0, 0, 640, 480)+ val ACTIVE_ARRAY_3 = Rect(0, 0, 320, 240)+ }++ @Test+ fun canReturnCameraCharacteriticsMap() {+ registerCameraCharacteristics(+ "0", ACTIVE_ARRAY_0, physicalCameraId = setOf("0", "2", "3"))+ registerCameraCharacteristics("2", ACTIVE_ARRAY_2)+ registerCameraCharacteristics("3", ACTIVE_ARRAY_3)++ val cameraInfo = FakeCameraInfoInternal("0", ApplicationProvider.getApplicationContext())++ val characteristicsMap = ExtensionsUtils.getCameraCharacteristicsMap(cameraInfo)+ assertThat(characteristicsMap.size).isEqualTo(3)+ assertThat(characteristicsMap.get("0")!!+ .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE))+ .isSameInstanceAs(ACTIVE_ARRAY_0)+ assertThat(characteristicsMap.get("2")!!+ .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE))+ .isSameInstanceAs(ACTIVE_ARRAY_2)+ assertThat(characteristicsMap.get("3")!!+ .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE))+ .isSameInstanceAs(ACTIVE_ARRAY_3)+ }++ private fun registerCameraCharacteristics(+ cameraId: String,+ activeArray: Rect,+ physicalCameraId: Set? = null+ ) {+ val characteristics0 = Mockito.mock(+ CameraCharacteristics::class.java+ )+ physicalCameraId?.let {+ Mockito.`when`(characteristics0.physicalCameraIds)+ .thenReturn(physicalCameraId)+ }+ Mockito.`when`(characteristics0.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE))+ .thenReturn(activeArray)++ // Add the camera to the camera service+ val shadowCameraManager = Shadow.extract(+ ApplicationProvider.getApplicationContext()+ .getSystemService(Context.CAMERA_SERVICE)+ ) as ShadowCameraManager++ shadowCameraManager.addCamera(cameraId, characteristics0)+ }+}
@@ -0,0 +1,76 @@
+/*+ * Copyright 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 androidx.camera.extensions.internal++import android.hardware.camera2.CaptureRequest+import android.os.Build+import androidx.camera.core.impl.Config.Option+import androidx.camera.core.impl.MutableOptionsBundle+import com.google.common.truth.Truth.assertThat+import org.junit.Test+import org.junit.runner.RunWith+import org.robolectric.RobolectricTestRunner+import org.robolectric.annotation.Config+import org.robolectric.annotation.internal.DoNotInstrument++@RunWith(RobolectricTestRunner::class)+@DoNotInstrument+@Config(+ minSdk = Build.VERSION_CODES.LOLLIPOP,+ instrumentedPackages = arrayOf("androidx.camera.extensions.internal")+)+class RequestOptionConfigTest {+ @Test+ fun canBuildWithCaptureRequestOptions() {+ val config = RequestOptionConfig.Builder()+ .setCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE,+ CaptureRequest.CONTROL_AF_MODE_AUTO)+ .setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION, 90)+ .build()++ assertThat(config.listOptions().size).isEqualTo(2)+ assertThat(config.retrieveOption(+ RequestOptionConfig.createOptionFromKey(CaptureRequest.CONTROL_AF_MODE))+ ).isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO)+ assertThat(config.retrieveOption(+ RequestOptionConfig.createOptionFromKey(CaptureRequest.JPEG_ORIENTATION))+ ).isEqualTo(90)+ }++ @Test+ fun canBuildFromConfig() {+ val mutableOptionConfig = MutableOptionsBundle.create()+ mutableOptionConfig.insertOption(+ Option.create("NonCaptureOption", String::class.java, null), "value1")+ mutableOptionConfig.insertOption(+ Option.create("NonCaptureOption2", Integer::class.java, null), 99)+ mutableOptionConfig.insertOption(+ RequestOptionConfig.createOptionFromKey(CaptureRequest.CONTROL_AF_MODE),+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)++ val requestOptionConfig = RequestOptionConfig.Builder.from(mutableOptionConfig)+ .setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION, 180)+ .build()+ assertThat(requestOptionConfig.listOptions().size).isEqualTo(2)+ assertThat(requestOptionConfig.retrieveOption(+ RequestOptionConfig.createOptionFromKey(CaptureRequest.CONTROL_AF_MODE))+ ).isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)+ assertThat(requestOptionConfig.retrieveOption(+ RequestOptionConfig.createOptionFromKey(CaptureRequest.JPEG_ORIENTATION))+ ).isEqualTo(180)+ }+ }