blob: 923e44a81f7a67b0047779606053204e1fa32188 [file] [log] [blame]
/*
* 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.internal;
import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_OFF;
import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON;
import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_EXTERNAL_FLASH;
import static android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_AUTO;
import static android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE;
import static android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_OFF;
import static android.hardware.camera2.CameraMetadata.CONTROL_AWB_MODE_AUTO;
import static android.hardware.camera2.CameraMetadata.CONTROL_AWB_MODE_OFF;
import static android.hardware.camera2.CameraMetadata.FLASH_MODE_OFF;
import static android.hardware.camera2.CameraMetadata.FLASH_MODE_TORCH;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeThat;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
import androidx.camera.camera2.internal.util.TestUtil;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraXConfig;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraControlInternal;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.testing.impl.CameraUtil;
import androidx.camera.testing.impl.CameraXUtil;
import androidx.camera.testing.impl.HandlerUtil;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.os.HandlerCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@SmallTest
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 21)
public final class Camera2CameraControlImplDeviceTest {
@Rule
public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
);
private Camera2CameraControlImpl mCamera2CameraControlImpl;
private CameraControlInternal.ControlUpdateCallback mControlUpdateCallback;
@SuppressWarnings("unchecked")
private ArgumentCaptor<List<CaptureConfig>> mCaptureConfigArgumentCaptor =
ArgumentCaptor.forClass(List.class);
private HandlerThread mHandlerThread;
private Handler mHandler;
private CameraCharacteristics mCameraCharacteristics;
private CameraCharacteristicsCompat mCameraCharacteristicsCompat;
private boolean mHasFlashUnit;
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private CameraUseCaseAdapter mCamera;
private Quirks mCameraQuirks;
@Before
public void setUp() throws InterruptedException {
mHandlerThread = new HandlerThread("ControlThread");
mHandlerThread.start();
mHandler = HandlerCompat.createAsync(mHandlerThread.getLooper());
Context context = ApplicationProvider.getApplicationContext();
CameraXConfig config = Camera2Config.defaultConfig();
CameraXUtil.initialize(context, config);
setUp(CameraSelector.LENS_FACING_BACK);
}
private void setUp(int lensFacing) throws InterruptedException {
assumeTrue(CameraUtil.hasCameraWithLensFacing(lensFacing));
mCameraCharacteristics = CameraUtil.getCameraCharacteristics(lensFacing);
Boolean hasFlashUnit =
mCameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
mHasFlashUnit = hasFlashUnit != null && hasFlashUnit.booleanValue();
mControlUpdateCallback = mock(CameraControlInternal.ControlUpdateCallback.class);
ScheduledExecutorService executorService = CameraXExecutors.newHandlerExecutor(mHandler);
String cameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing);
mCameraCharacteristicsCompat = CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
mCameraCharacteristics, cameraId);
mCamera2CameraControlImpl = new Camera2CameraControlImpl(mCameraCharacteristicsCompat,
executorService, executorService, mControlUpdateCallback);
mCameraQuirks = CameraQuirks.get(cameraId, mCameraCharacteristicsCompat);
mCamera2CameraControlImpl.incrementUseCount();
mCamera2CameraControlImpl.setActive(true);
HandlerUtil.waitForLooperToIdle(mHandler);
}
@After
public void tearDown() throws InterruptedException, ExecutionException, TimeoutException {
if (mCamera != null) {
mInstrumentation.runOnMainSync(() ->
//TODO: The removeUseCases() call might be removed after clarifying the
// abortCaptures() issue in b/162314023.
mCamera.removeUseCases(mCamera.getUseCases())
);
}
CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS);
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
}
}
private boolean isAndroidRZoomEnabled() {
return ZoomControl.isAndroidRZoomSupported(mCameraCharacteristicsCompat);
}
private int getMaxAfRegionCount() {
return mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF);
}
private int getMaxAeRegionCount() {
return mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE);
}
private int getMaxAwbRegionCount() {
return mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB);
}
private boolean isAeSupported() {
int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
for (int mode : modes) {
if (mode == CONTROL_AE_MODE_ON) {
return true;
}
}
return false;
}
@Test
public void canIncrementDecrementUseCount() {
// incrementUseCount() in setup()
assertThat(mCamera2CameraControlImpl.getUseCount()).isEqualTo(1);
mCamera2CameraControlImpl.decrementUseCount();
assertThat(mCamera2CameraControlImpl.getUseCount()).isEqualTo(0);
}
@Test(expected = IllegalStateException.class)
public void decrementUseCountLessThanZero_getException() {
// incrementUseCount() in setup()
assertThat(mCamera2CameraControlImpl.getUseCount()).isEqualTo(1);
mCamera2CameraControlImpl.decrementUseCount();
mCamera2CameraControlImpl.decrementUseCount();
}
@Test
public void setTemplate_updateCameraControlSessionConfig() {
mCamera2CameraControlImpl.setTemplate(CameraDevice.TEMPLATE_RECORD);
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
assertThat(sessionConfig.getTemplateType()).isEqualTo(CameraDevice.TEMPLATE_RECORD);
}
@Test
public void setTemplatePreview_afModeToContinuousPicture() {
mCamera2CameraControlImpl.setTemplate(CameraDevice.TEMPLATE_PREVIEW);
Camera2ImplConfig camera2Config =
new Camera2ImplConfig(mCamera2CameraControlImpl.getSessionOptions());
assertAfMode(camera2Config, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
}
@Test
public void setTemplateRecord_afModeToContinuousVideo() {
mCamera2CameraControlImpl.setTemplate(CameraDevice.TEMPLATE_RECORD);
Camera2ImplConfig camera2Config =
new Camera2ImplConfig(mCamera2CameraControlImpl.getSessionOptions());
assertAfMode(camera2Config, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
}
@Test
public void defaultAFAWBMode_ShouldBeCAFWhenNotFocusLocked() {
Camera2ImplConfig singleConfig = new Camera2ImplConfig(
mCamera2CameraControlImpl.getSessionOptions());
assertThat(
singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF))
.isEqualTo(CaptureRequest.CONTROL_MODE_AUTO);
assertAfMode(singleConfig, CONTROL_AF_MODE_CONTINUOUS_PICTURE);
assertAwbMode(singleConfig, CONTROL_AWB_MODE_AUTO);
}
@Test
public void setFlashModeAuto_aeModeSetAndRequestUpdated() throws InterruptedException {
mCamera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_AUTO);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAeMode(camera2Config, CONTROL_AE_MODE_ON_AUTO_FLASH);
assertThat(mCamera2CameraControlImpl.getFlashMode()).isEqualTo(
ImageCapture.FLASH_MODE_AUTO);
// ZSL only support API >= 23. ZslControlImpl will be created for API >= 23, otherwise
// ZslControlNoOpImpl will be created, which always return false for this flag.
if (Build.VERSION.SDK_INT >= 23) {
assertThat(
mCamera2CameraControlImpl.getZslControl().isZslDisabledByFlashMode()).isTrue();
} else {
assertThat(
mCamera2CameraControlImpl.getZslControl().isZslDisabledByFlashMode()).isFalse();
}
}
@Test
public void setFlashModeOff_aeModeSetAndRequestUpdated() throws InterruptedException {
mCamera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAeMode(camera2Config, CONTROL_AE_MODE_ON);
assertThat(mCamera2CameraControlImpl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
assertThat(mCamera2CameraControlImpl.getZslControl().isZslDisabledByFlashMode()).isFalse();
}
@Test
public void setFlashModeOn_aeModeSetAndRequestUpdated() throws InterruptedException {
mCamera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_ON);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAeMode(camera2Config, CONTROL_AE_MODE_ON_ALWAYS_FLASH);
assertThat(mCamera2CameraControlImpl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_ON);
// ZSL only support API >= 23. ZslControlImpl will be created for API >= 23, otherwise
// ZslControlNoOpImpl will be created, which always return false for this flag.
if (Build.VERSION.SDK_INT >= 23) {
assertThat(
mCamera2CameraControlImpl.getZslControl().isZslDisabledByFlashMode()).isTrue();
} else {
assertThat(
mCamera2CameraControlImpl.getZslControl().isZslDisabledByFlashMode()).isFalse();
}
}
@Test
@SdkSuppress(minSdkVersion = 28)
public void enableExternalFlashAeMode_aeModeSetAndRequestUpdated() throws InterruptedException {
setUp(CameraSelector.LENS_FACING_FRONT);
assumeThat("CONTROL_AE_MODE_ON_EXTERNAL_FLASH not supported",
mCamera2CameraControlImpl.getSupportedAeMode(CONTROL_AE_MODE_ON_EXTERNAL_FLASH),
equalTo(CONTROL_AE_MODE_ON_EXTERNAL_FLASH));
mCamera2CameraControlImpl.getFocusMeteringControl().enableExternalFlashAeMode(true);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAeMode(camera2Config, CONTROL_AE_MODE_ON_EXTERNAL_FLASH);
}
@Test
@SdkSuppress(minSdkVersion = 28)
public void disableExternalFlashAeMode_aeModeUnsetAndRequestUpdated()
throws InterruptedException {
setUp(CameraSelector.LENS_FACING_FRONT);
assumeThat("CONTROL_AE_MODE_ON_EXTERNAL_FLASH not supported",
mCamera2CameraControlImpl.getSupportedAeMode(CONTROL_AE_MODE_ON_EXTERNAL_FLASH),
equalTo(CONTROL_AE_MODE_ON_EXTERNAL_FLASH));
mCamera2CameraControlImpl.getFocusMeteringControl().enableExternalFlashAeMode(true);
HandlerUtil.waitForLooperToIdle(mHandler);
mCamera2CameraControlImpl.getFocusMeteringControl().enableExternalFlashAeMode(false);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(2)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertThat(camera2Config.getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
null)).isNotEqualTo(CONTROL_AE_MODE_ON_EXTERNAL_FLASH);
}
@Test
public void enableTorch_aeModeSetAndRequestUpdated() throws InterruptedException {
assumeTrue(mHasFlashUnit);
mCamera2CameraControlImpl.enableTorch(true);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAeMode(camera2Config, CONTROL_AE_MODE_ON);
assertThat(
camera2Config.getCaptureRequestOption(
CaptureRequest.FLASH_MODE, FLASH_MODE_OFF))
.isEqualTo(FLASH_MODE_TORCH);
}
@Test
public void disableTorchFlashModeAuto_aeModeSetAndRequestUpdated() throws InterruptedException {
assumeTrue(mHasFlashUnit);
mCamera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_AUTO);
mCamera2CameraControlImpl.enableTorch(false);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(2)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAeMode(camera2Config, CONTROL_AE_MODE_ON_AUTO_FLASH);
assertThat(camera2Config.getCaptureRequestOption(
CaptureRequest.FLASH_MODE, -1))
.isEqualTo(-1);
verify(mControlUpdateCallback, times(1)).onCameraControlCaptureRequests(
mCaptureConfigArgumentCaptor.capture());
CaptureConfig captureConfig = mCaptureConfigArgumentCaptor.getValue().get(0);
Camera2ImplConfig resultCaptureConfig =
new Camera2ImplConfig(captureConfig.getImplementationOptions());
assertAeMode(resultCaptureConfig, CONTROL_AE_MODE_ON);
}
@Test
@LargeTest
public void triggerAf_futureSucceeds() throws Exception {
Camera2CameraControlImpl camera2CameraControlImpl =
createCamera2CameraControlWithPhysicalCamera();
ListenableFuture<CameraCaptureResult> future = CallbackToFutureAdapter.getFuture(c -> {
camera2CameraControlImpl.mExecutor.execute(() ->
camera2CameraControlImpl.getFocusMeteringControl().triggerAf(
c, /* overrideAeMode */ false));
return "triggerAf";
});
future.get(5, TimeUnit.SECONDS);
}
@Test
public void captureMaxQuality_shouldSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
captureTest(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
}
@Test
public void captureMiniLatency_shouldSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
captureTest(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
}
@Test
public void captureMaxQuality_torchAsFlash_shouldSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
captureTest(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
}
@Test
public void captureMiniLatency_torchAsFlash_shouldSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
captureTest(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
}
private void captureTest(int captureMode, int flashType)
throws ExecutionException, InterruptedException, TimeoutException {
ImageCapture imageCapture = new ImageCapture.Builder().build();
mCamera = CameraUtil.createCameraAndAttachUseCase(
ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
imageCapture);
Camera2CameraControlImpl camera2CameraControlImpl =
TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureConfigBuilder.addSurface(imageCapture.getSessionConfig().getSurfaces().get(0));
captureConfigBuilder.addCameraCaptureCallback(captureCallback);
ListenableFuture<List<Void>> future = camera2CameraControlImpl.submitStillCaptureRequests(
Arrays.asList(captureConfigBuilder.build()), captureMode, flashType);
// The future should successfully complete
future.get(10, TimeUnit.SECONDS);
// CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
verify(captureCallback, timeout(3000).times(1))
.onCaptureCompleted(any(CameraCaptureResult.class));
}
private Camera2CameraControlImpl createCamera2CameraControlWithPhysicalCamera() {
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
// Make ImageAnalysis active.
imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), (image) -> image.close());
mCamera = CameraUtil.createCameraAndAttachUseCase(
ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
imageAnalysis);
return TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
}
private <T> void assertArraySize(T[] array, int expectedSize) {
if (expectedSize == 0) {
assertTrue(array == null || array.length == 0);
} else {
assertThat(array).hasLength(expectedSize);
}
}
@Test
public void startFocusAndMetering_3ARegionsUpdatedInSessionAndSessionOptions()
throws InterruptedException {
assumeTrue(getMaxAfRegionCount() > 0 || getMaxAeRegionCount() > 0
|| getMaxAwbRegionCount() > 0);
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0))
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig repeatingConfig = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
// Here we verify only 3A region count is correct. Values correctness are left to
// FocusMeteringControlTest.
int expectedAfCount = Math.min(getMaxAfRegionCount(), 1);
int expectedAeCount = Math.min(getMaxAeRegionCount(), 1);
int expectedAwbCount = Math.min(getMaxAwbRegionCount(), 1);
assertArraySize(repeatingConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_REGIONS, null), expectedAfCount);
assertArraySize(repeatingConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_REGIONS, null), expectedAeCount);
assertArraySize(repeatingConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AWB_REGIONS, null), expectedAwbCount);
Camera2ImplConfig singleConfig = new Camera2ImplConfig(
mCamera2CameraControlImpl.getSessionOptions());
assertArraySize(singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_REGIONS, null), expectedAfCount);
assertArraySize(singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_REGIONS, null), expectedAeCount);
assertArraySize(singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AWB_REGIONS, null), expectedAwbCount);
}
@Test
public void startFocusAndMetering_AfIsTriggeredProperly() throws InterruptedException {
assumeTrue(getMaxAfRegionCount() > 0);
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0))
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
verifyAfMode(CaptureRequest.CONTROL_AF_MODE_AUTO);
verify(mControlUpdateCallback).onCameraControlCaptureRequests(
mCaptureConfigArgumentCaptor.capture());
CaptureConfig captureConfig = mCaptureConfigArgumentCaptor.getValue().get(0);
Camera2ImplConfig resultCaptureConfig =
new Camera2ImplConfig(captureConfig.getImplementationOptions());
// Trigger AF
assertThat(resultCaptureConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_TRIGGER, null))
.isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
// Ensures AE_MODE is overridden to CONTROL_AE_MODE_ON to prevent from flash being fired.
if (isAeSupported()) {
assertThat(resultCaptureConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_MODE, null))
.isEqualTo(CONTROL_AE_MODE_ON);
}
}
@Test
public void startFocusAndMetering_AFNotInvolved_AfIsNotTriggered() throws InterruptedException {
assumeTrue(getMaxAeRegionCount() > 0 || getMaxAwbRegionCount() > 0);
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0),
FocusMeteringAction.FLAG_AE | FocusMeteringAction.FLAG_AWB)
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
verifyAfMode(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
verify(mControlUpdateCallback, never()).onCameraControlCaptureRequests(any());
}
@Test
public void cancelFocusAndMetering_3ARegionsReset() throws InterruptedException {
assumeTrue(getMaxAfRegionCount() > 0 || getMaxAeRegionCount() > 0
|| getMaxAwbRegionCount() > 0);
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0))
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
Mockito.reset(mControlUpdateCallback);
mCamera2CameraControlImpl.cancelFocusAndMetering();
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig repeatingConfig = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertThat(
repeatingConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_REGIONS, null)).isNull();
assertThat(
repeatingConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_REGIONS, null)).isNull();
assertThat(
repeatingConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AWB_REGIONS, null)).isNull();
Camera2ImplConfig singleConfig = new Camera2ImplConfig(
mCamera2CameraControlImpl.getSessionOptions());
assertThat(
singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_REGIONS, null)).isNull();
assertThat(
singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_REGIONS, null)).isNull();
assertThat(
singleConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AWB_REGIONS, null)).isNull();
}
@Test
public void cancelFocusAndMetering_cancelAfProperly() throws InterruptedException {
assumeTrue(getMaxAfRegionCount() > 0);
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0))
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
Mockito.reset(mControlUpdateCallback);
mCamera2CameraControlImpl.cancelFocusAndMetering();
HandlerUtil.waitForLooperToIdle(mHandler);
verifyAfMode(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
verify(mControlUpdateCallback).onCameraControlCaptureRequests(
mCaptureConfigArgumentCaptor.capture());
CaptureConfig captureConfig = mCaptureConfigArgumentCaptor.getValue().get(0);
Camera2ImplConfig resultCaptureConfig =
new Camera2ImplConfig(captureConfig.getImplementationOptions());
// Trigger AF
assertThat(resultCaptureConfig.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_TRIGGER, null))
.isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
}
private void verifyAfMode(int expectAfMode) {
verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig repeatingConfig = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
assertAfMode(repeatingConfig, expectAfMode);
}
@Test
public void cancelFocusAndMetering_AFNotInvolved_notCancelAF() throws InterruptedException {
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0),
FocusMeteringAction.FLAG_AE)
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
Mockito.reset(mControlUpdateCallback);
mCamera2CameraControlImpl.cancelFocusAndMetering();
HandlerUtil.waitForLooperToIdle(mHandler);
verify(mControlUpdateCallback, never()).onCameraControlCaptureRequests(any());
verifyAfMode(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
}
@Test
public void startFocus_afModeIsSetToAuto() throws InterruptedException {
assumeTrue(getMaxAfRegionCount() > 0);
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f,
1.0f);
FocusMeteringAction action = new FocusMeteringAction.Builder(factory.createPoint(0, 0))
.build();
mCamera2CameraControlImpl.startFocusAndMetering(action);
HandlerUtil.waitForLooperToIdle(mHandler);
Camera2ImplConfig singleConfig = new Camera2ImplConfig(
mCamera2CameraControlImpl.getSessionOptions());
assertAfMode(singleConfig, CaptureRequest.CONTROL_AF_MODE_AUTO);
mCamera2CameraControlImpl.cancelFocusAndMetering();
HandlerUtil.waitForLooperToIdle(mHandler);
Camera2ImplConfig singleConfig2 = new Camera2ImplConfig(
mCamera2CameraControlImpl.getSessionOptions());
assertAfMode(singleConfig2, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
}
private boolean isAfModeSupported(int afMode) {
int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
return isModeInList(afMode, modes);
}
private boolean isAeModeSupported(int aeMode) {
int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
return isModeInList(aeMode, modes);
}
private boolean isAwbModeSupported(int awbMode) {
int[] modes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
return isModeInList(awbMode, modes);
}
private boolean isModeInList(int mode, int[] modeList) {
if (modeList == null) {
return false;
}
for (int m : modeList) {
if (mode == m) {
return true;
}
}
return false;
}
private void assertAfMode(Camera2ImplConfig config, int afMode) {
if (isAfModeSupported(afMode)) {
assertThat(config.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_MODE, null)).isEqualTo(afMode);
} else {
int fallbackMode;
if (isAfModeSupported(CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
fallbackMode = CONTROL_AF_MODE_CONTINUOUS_PICTURE;
} else if (isAfModeSupported(CONTROL_AF_MODE_AUTO)) {
fallbackMode = CONTROL_AF_MODE_AUTO;
} else {
fallbackMode = CONTROL_AF_MODE_OFF;
}
assertThat(config.getCaptureRequestOption(
CaptureRequest.CONTROL_AF_MODE, null)).isEqualTo(fallbackMode);
}
}
private void assertAeMode(Camera2ImplConfig config, int aeMode) {
AutoFlashAEModeDisabler aeModeCorrector = new AutoFlashAEModeDisabler(mCameraQuirks);
int aeModeCorrected = aeModeCorrector.getCorrectedAeMode(aeMode);
if (isAeModeSupported(aeModeCorrected)) {
assertThat(config.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_MODE, null)).isEqualTo(aeModeCorrected);
} else {
int fallbackMode;
if (isAeModeSupported(CONTROL_AE_MODE_ON)) {
fallbackMode = CONTROL_AE_MODE_ON;
} else {
fallbackMode = CONTROL_AE_MODE_OFF;
}
assertThat(config.getCaptureRequestOption(
CaptureRequest.CONTROL_AE_MODE, null)).isEqualTo(fallbackMode);
}
}
private void assertAwbMode(Camera2ImplConfig config, int awbMode) {
if (isAwbModeSupported(awbMode)) {
assertThat(config.getCaptureRequestOption(
CaptureRequest.CONTROL_AWB_MODE, null)).isEqualTo(awbMode);
} else {
int fallbackMode;
if (isAwbModeSupported(CONTROL_AWB_MODE_AUTO)) {
fallbackMode = CONTROL_AWB_MODE_AUTO;
} else {
fallbackMode = CONTROL_AWB_MODE_OFF;
}
assertThat(config.getCaptureRequestOption(
CaptureRequest.CONTROL_AWB_MODE, null)).isEqualTo(fallbackMode);
}
}
private boolean isZoomSupported() {
return mCameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
> 1.0f;
}
private Rect getSensorRect() {
Rect rect = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
// Some device like pixel 2 will have (0, 8) as the left-top corner.
return new Rect(0, 0, rect.width(), rect.height());
}
// Here we just test if setZoomRatio / setLinearZoom is working. For thorough tests, we
// do it on ZoomControlTest and ZoomControlRoboTest.
@Test
public void setZoomRatio_CropRegionIsUpdatedCorrectly() throws InterruptedException {
assumeTrue(isZoomSupported());
assumeFalse(isAndroidRZoomEnabled());
mCamera2CameraControlImpl.setZoomRatio(2.0f);
HandlerUtil.waitForLooperToIdle(mHandler);
Rect sessionCropRegion = getSessionCropRegion(mControlUpdateCallback);
Rect sensorRect = getSensorRect();
int cropX = (sensorRect.width() / 4);
int cropY = (sensorRect.height() / 4);
Rect cropRect = new Rect(cropX, cropY, cropX + sensorRect.width() / 2,
cropY + sensorRect.height() / 2);
assertThat(sessionCropRegion).isEqualTo(cropRect);
}
@Test
@SdkSuppress(minSdkVersion = 30)
public void setZoomRatio_androidRZoomRatioIsUpdatedCorrectly() throws InterruptedException {
assumeTrue(isAndroidRZoomEnabled());
mCamera2CameraControlImpl.setZoomRatio(2.0f);
HandlerUtil.waitForLooperToIdle(mHandler);
float sessionZoomRatio = getSessionZoomRatio(mControlUpdateCallback);
assertThat(sessionZoomRatio).isEqualTo(2.0f);
}
@NonNull
private Rect getSessionCropRegion(
CameraControlInternal.ControlUpdateCallback controlUpdateCallback)
throws InterruptedException {
verify(controlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
reset(controlUpdateCallback);
return camera2Config.getCaptureRequestOption(
CaptureRequest.SCALER_CROP_REGION, null);
}
@NonNull
@RequiresApi(30)
private Float getSessionZoomRatio(
CameraControlInternal.ControlUpdateCallback controlUpdateCallback)
throws InterruptedException {
verify(controlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
Camera2ImplConfig camera2Config = new Camera2ImplConfig(
sessionConfig.getImplementationOptions());
reset(controlUpdateCallback);
return camera2Config.getCaptureRequestOption(
CaptureRequest.CONTROL_ZOOM_RATIO, null);
}
@Test
public void setLinearZoom_CropRegionIsUpdatedCorrectly() throws InterruptedException {
assumeTrue(isZoomSupported());
assumeFalse(isAndroidRZoomEnabled());
mCamera2CameraControlImpl.setLinearZoom(1.0f);
HandlerUtil.waitForLooperToIdle(mHandler);
Rect cropRegionMaxZoom = getSessionCropRegion(mControlUpdateCallback);
Rect cropRegionMinZoom = getSensorRect();
mCamera2CameraControlImpl.setLinearZoom(0.5f);
HandlerUtil.waitForLooperToIdle(mHandler);
Rect cropRegionHalfZoom = getSessionCropRegion(mControlUpdateCallback);
Assert.assertEquals(cropRegionHalfZoom.width(),
(cropRegionMinZoom.width() + cropRegionMaxZoom.width()) / 2.0f, 1
/* 1 pixel tolerance */);
}
@Test
@SdkSuppress(minSdkVersion = 30)
public void setLinearZoom_androidRZoomRatioUpdatedCorrectly() throws InterruptedException {
assumeTrue(isAndroidRZoomEnabled());
final float cropWidth = 10000f;
mCamera2CameraControlImpl.setLinearZoom(1.0f);
HandlerUtil.waitForLooperToIdle(mHandler);
float sessionZoomRatioForLinearMax = getSessionZoomRatio(mControlUpdateCallback);
float cropWidthForLinearMax = cropWidth / sessionZoomRatioForLinearMax;
mCamera2CameraControlImpl.setLinearZoom(0f);
HandlerUtil.waitForLooperToIdle(mHandler);
float sessionZoomRatioForLinearMin = getSessionZoomRatio(mControlUpdateCallback);
float cropWidthForLinearMin = cropWidth / sessionZoomRatioForLinearMin;
mCamera2CameraControlImpl.setLinearZoom(0.5f);
HandlerUtil.waitForLooperToIdle(mHandler);
float sessionZoomRatioForLinearHalf = getSessionZoomRatio(mControlUpdateCallback);
float cropWidthForLinearHalf = cropWidth / sessionZoomRatioForLinearHalf;
Assert.assertEquals(cropWidthForLinearHalf,
(cropWidthForLinearMin + cropWidthForLinearMax) / 2.0f, 1
/* 1 pixel tolerance */);
}
@Test
public void setZoomRatio_cameraControlInactive_operationCanceled() {
mCamera2CameraControlImpl.setActive(false);
ListenableFuture<Void> listenableFuture = mCamera2CameraControlImpl.setZoomRatio(1.0f);
try {
listenableFuture.get(1000, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof CameraControl.OperationCanceledException) {
assertTrue(true);
return;
}
} catch (Exception e) {
}
fail();
}
@Test
public void setLinearZoom_cameraControlInactive_operationCanceled() {
mCamera2CameraControlImpl.setActive(false);
ListenableFuture<Void> listenableFuture = mCamera2CameraControlImpl.setLinearZoom(0.0f);
try {
listenableFuture.get(1000, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof CameraControl.OperationCanceledException) {
assertTrue(true);
return;
}
} catch (Exception e) {
}
fail();
}
@Test
@LargeTest
public void addSessionCameraCaptureCallback_canAddWithoutUpdateSessionConfig()
throws Exception {
Camera2CameraControlImpl camera2CameraControlImpl =
createCamera2CameraControlWithPhysicalCamera();
camera2CameraControlImpl.updateSessionConfig();
HandlerUtil.waitForLooperToIdle(mHandler);
TestCameraCaptureCallback callback1 = new TestCameraCaptureCallback();
TestCameraCaptureCallback callback2 = new TestCameraCaptureCallback();
camera2CameraControlImpl.addSessionCameraCaptureCallback(CameraXExecutors.directExecutor(),
callback1);
camera2CameraControlImpl.addSessionCameraCaptureCallback(CameraXExecutors.directExecutor(),
callback2);
callback1.assertCallbackIsCalled(5000);
callback2.assertCallbackIsCalled(5000);
}
@Test
@LargeTest
public void removeSessionCameraCaptureCallback() throws Exception {
Camera2CameraControlImpl camera2CameraControlImpl =
createCamera2CameraControlWithPhysicalCamera();
camera2CameraControlImpl.updateSessionConfig();
HandlerUtil.waitForLooperToIdle(mHandler);
TestCameraCaptureCallback callback1 = new TestCameraCaptureCallback();
camera2CameraControlImpl.addSessionCameraCaptureCallback(CameraXExecutors.directExecutor(),
callback1);
callback1.assertCallbackIsCalled(5000);
camera2CameraControlImpl.removeSessionCameraCaptureCallback(callback1);
HandlerUtil.waitForLooperToIdle(mHandler);
callback1.assertCallbackIsNotCalled(200);
}
@Test
@LargeTest
public void sessionCameraCaptureCallback_invokedOnSpecifiedExecutor()
throws Exception {
Camera2CameraControlImpl camera2CameraControlImpl =
createCamera2CameraControlWithPhysicalCamera();
camera2CameraControlImpl.updateSessionConfig();
HandlerUtil.waitForLooperToIdle(mHandler);
TestCameraCaptureCallback callback = new TestCameraCaptureCallback();
TestExecutor executor = new TestExecutor();
camera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
callback.assertCallbackIsCalled(5000);
executor.assertExecutorIsCalled(5000);
}
private static class TestCameraCaptureCallback extends CameraCaptureCallback {
private CountDownLatch mLatchForOnCaptureCompleted;
@Override
public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
synchronized (this) {
if (mLatchForOnCaptureCompleted != null) {
mLatchForOnCaptureCompleted.countDown();
}
}
}
public void assertCallbackIsCalled(long timeoutInMs) throws InterruptedException {
CountDownLatch latch;
synchronized (this) {
mLatchForOnCaptureCompleted = new CountDownLatch(1);
latch = mLatchForOnCaptureCompleted;
}
assertThat(latch.await(timeoutInMs, TimeUnit.MILLISECONDS))
.isTrue();
}
public void assertCallbackIsNotCalled(long timeoutInMs) throws InterruptedException {
CountDownLatch latch;
synchronized (this) {
mLatchForOnCaptureCompleted = new CountDownLatch(1);
latch = mLatchForOnCaptureCompleted;
}
assertThat(latch.await(timeoutInMs, TimeUnit.MILLISECONDS))
.isFalse();
}
}
private static class TestExecutor implements Executor {
private CountDownLatch mLatch = new CountDownLatch(1);
@Override
public void execute(@NonNull Runnable command) {
command.run();
mLatch.countDown();
}
public void assertExecutorIsCalled(long timeoutInMS) throws InterruptedException {
assertThat(mLatch.await(timeoutInMS, TimeUnit.MILLISECONDS)).isTrue();
}
}
}