diff options
author | Scott Nien <scottnien@google.com> | 2019-09-24 16:56:08 +0800 |
---|---|---|
committer | Scott Nien <scottnien@google.com> | 2019-10-28 12:13:01 +0800 |
commit | f60a1ac99a4144d5bfb0529fa0c769f43eb10c9f (patch) | |
tree | 60f5623044f4ad7a3990dd10d0e9cddbec3459f0 | |
parent | 95b83f80af058444256dfc7925131af8a56fd579 (diff) | |
download | support-f60a1ac99a4144d5bfb0529fa0c769f43eb10c9f.tar.gz |
[Zoom] Expose zoom APIs in CameraControl and CameraInfo, adding active state for FocusMetering APIs.
* Supports setZoomRatio() / setZoomPercentage() in CameraControl.
* Supports getZoomRatio() / getZoomPercentage() / getMaxZoomRatio / getMinZoomRatio() in CameraInfo.
* CameraControl is active when there are attached online use cases otherwise it is inactive.
Calling setZoomRatio/setZoomPercentage/startFocusAndMetering when inactive will do nothing.
Test: Camera2CameraControlTest / CameraImplTest / FocusMeteringControlTest
Bug: 128986739
Change-Id: I3fc3791583fdf47e70f8fadd255517d07c6da630
16 files changed, 529 insertions, 26 deletions
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraControlTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraControlTest.java index 31473157473..80828929a13 100644 --- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraControlTest.java +++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraControlTest.java @@ -30,6 +30,8 @@ import static android.hardware.camera2.CameraMetadata.FLASH_MODE_TORCH; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -48,7 +50,9 @@ import android.os.Build; import android.os.Handler; import android.os.HandlerThread; +import androidx.annotation.NonNull; import androidx.camera.camera2.Camera2Config; +import androidx.camera.core.CameraControl; import androidx.camera.core.CameraControlInternal; import androidx.camera.core.CameraInfoUnavailableException; import androidx.camera.core.CaptureConfig; @@ -66,7 +70,10 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.google.common.util.concurrent.ListenableFuture; + import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -74,7 +81,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @SmallTest @RunWith(AndroidJUnit4.class) @@ -111,6 +120,7 @@ public final class Camera2CameraControlTest { mCamera2CameraControl = new Camera2CameraControl(mCameraCharacteristics, executorService, executorService, mControlUpdateCallback); + mCamera2CameraControl.setActive(true); HandlerUtil.waitForLooperToIdle(mHandler); // Reset the method call onCameraControlUpdateSessionConfig() in Camera2CameraControl @@ -627,4 +637,102 @@ public final class Camera2CameraControlTest { 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 / setZoomPercentage is working. For thorough tests, we + // do it on ZoomControlTest and ZoomControlRoboTest. + @Test + public void setZoomRatio_CropRegionIsUpdatedCorrectly() throws InterruptedException { + assumeTrue(isZoomSupported()); + mCamera2CameraControl.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); + } + + @NonNull + private Rect getSessionCropRegion( + CameraControlInternal.ControlUpdateCallback controlUpdateCallback) + throws InterruptedException { + verify(controlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig( + mSessionConfigArgumentCaptor.capture()); + SessionConfig sessionConfig = mSessionConfigArgumentCaptor.getValue(); + Camera2Config camera2Config = new Camera2Config(sessionConfig.getImplementationOptions()); + + reset(controlUpdateCallback); + return camera2Config.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null); + } + + @Test + public void setZoomPercentage_CropRegionIsUpdatedCorrectly() throws InterruptedException { + assumeTrue(isZoomSupported()); + mCamera2CameraControl.setZoomPercentage(1.0f); + HandlerUtil.waitForLooperToIdle(mHandler); + + Rect cropRegionMaxZoom = getSessionCropRegion(mControlUpdateCallback); + Rect cropRegionMinZoom = getSensorRect(); + + mCamera2CameraControl.setZoomPercentage(0.5f); + + HandlerUtil.waitForLooperToIdle(mHandler); + + Rect cropRegionHalfZoom = getSessionCropRegion(mControlUpdateCallback); + + Assert.assertEquals(cropRegionHalfZoom.width(), + (cropRegionMinZoom.width() + cropRegionMaxZoom.width()) / 2.0f, 1 + /* 1 pixel tolerance */); + } + + @Test + public void setZoomRatio_cameraControlInactive_operationCanceled() { + mCamera2CameraControl.setActive(false); + ListenableFuture<Void> listenableFuture = mCamera2CameraControl.setZoomRatio(2.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 setZoomPercentage_cameraControlInactive_operationCanceled() { + mCamera2CameraControl.setActive(false); + ListenableFuture<Void> listenableFuture = mCamera2CameraControl.setZoomPercentage(0.0f); + try { + listenableFuture.get(1000, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + if (e.getCause() instanceof CameraControl.OperationCanceledException) { + assertTrue(true); + return; + } + } catch (Exception e) { + } + + fail(); + } + } diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraImplTest.java index 94d5754ffd2..8a312cffe07 100644 --- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraImplTest.java +++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CameraImplTest.java @@ -44,6 +44,7 @@ import androidx.annotation.Nullable; import androidx.camera.camera2.impl.compat.CameraManagerCompat; import androidx.camera.core.CameraCaptureCallback; import androidx.camera.core.CameraCaptureResult; +import androidx.camera.core.CameraControl; import androidx.camera.core.CameraDeviceConfig; import androidx.camera.core.CameraFactory; import androidx.camera.core.CameraInternal; @@ -88,6 +89,8 @@ import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; /** @@ -848,6 +851,51 @@ public final class Camera2CameraImplTest { verify(useCase3, times(0)).onStateOffline(eq(mCameraId)); } + private boolean isCameraControlActive(Camera2CameraControl camera2CameraControl) { + ListenableFuture<Void> listenableFuture = camera2CameraControl.setZoomRatio(2.0f); + try { + // setZoom() will fail immediately when Cameracontrol is not active. + listenableFuture.get(50, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + if (e.getCause() instanceof CameraControl.OperationCanceledException) { + return false; + } + } catch (InterruptedException | TimeoutException e) { + } + return true; + } + + @Test + public void activateCameraControl_whenExsitsOnlineUseCases() throws InterruptedException { + Camera2CameraControl camera2CameraControl = + (Camera2CameraControl) mCamera2CameraImpl.getCameraControlInternal(); + + assertThat(isCameraControlActive(camera2CameraControl)).isFalse(); + + UseCase useCase1 = createUseCase(); + + mCamera2CameraImpl.addOnlineUseCase(Arrays.asList(useCase1)); + HandlerUtil.waitForLooperToIdle(mCameraHandler); + + assertThat(isCameraControlActive(camera2CameraControl)).isTrue(); + } + + @Test + public void deactivateCameraControl_whenNoOnlineUseCases() throws InterruptedException { + Camera2CameraControl camera2CameraControl = + (Camera2CameraControl) mCamera2CameraImpl.getCameraControlInternal(); + UseCase useCase1 = createUseCase(); + + mCamera2CameraImpl.addOnlineUseCase(Arrays.asList(useCase1)); + HandlerUtil.waitForLooperToIdle(mCameraHandler); + assertThat(isCameraControlActive(camera2CameraControl)).isTrue(); + + mCamera2CameraImpl.removeOnlineUseCase(Arrays.asList(useCase1)); + HandlerUtil.waitForLooperToIdle(mCameraHandler); + + assertThat(isCameraControlActive(camera2CameraControl)).isFalse(); + } + private DeferrableSurface getUseCaseSurface(UseCase useCase) { return useCase.getSessionConfig(mCameraId).getSurfaces().get(0); } diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraControl.java index 58442e3002a..86f1dbdfb49 100644 --- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraControl.java +++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraControl.java @@ -40,6 +40,8 @@ import androidx.camera.core.FocusMeteringAction; import androidx.camera.core.SessionConfig; import androidx.core.util.Preconditions; +import com.google.common.util.concurrent.ListenableFuture; + import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -65,9 +67,11 @@ public final class Camera2CameraControl implements CameraControlInternal { @SuppressWarnings("WeakerAccess") /* synthetic accessor */ volatile Rational mPreviewAspectRatio = null; private final FocusMeteringControl mFocusMeteringControl; + private final ZoomControl mZoomControl; // use volatile modifier to make these variables in sync in all threads. private volatile boolean mIsTorchOn = false; private volatile FlashMode mFlashMode = FlashMode.OFF; + private volatile boolean mIsActive = false; //******************** Should only be accessed by executor *****************************// private Rect mCropRect = null; @@ -98,11 +102,30 @@ public final class Camera2CameraControl implements CameraControlInternal { CaptureCallbackContainer.create(mSessionCallback)); mFocusMeteringControl = new FocusMeteringControl(this, scheduler, mExecutor); + mZoomControl = new ZoomControl(this, mCameraCharacteristics); // Initialize the session config mExecutor.execute(this::updateSessionConfig); } + @NonNull + public ZoomControl getZoomControl() { + return mZoomControl; + } + + /** + * Set current active state. Set active if it is ready to trigger camera control operation. + * + * <p>Most operations during inactive state do nothing. Some states are reset to default + * once it is changed to inactive state. + */ + void setActive(boolean isActive) { + mIsActive = isActive; + mFocusMeteringControl.setActive(isActive); + mZoomControl.setActive(isActive); + } + + @WorkerThread public void setPreviewAspectRatio(@Nullable Rational previewAspectRatio) { mPreviewAspectRatio = previewAspectRatio; } @@ -118,6 +141,18 @@ public final class Camera2CameraControl implements CameraControlInternal { mExecutor.execute(mFocusMeteringControl::cancelFocusAndMetering); } + @NonNull + @Override + public ListenableFuture<Void> setZoomRatio(float ratio) { + return mZoomControl.setZoomRatio(ratio); + } + + @NonNull + @Override + public ListenableFuture<Void> setZoomPercentage(float percentage) { + return mZoomControl.setZoomPercentage(percentage); + } + /** {@inheritDoc} */ @Override public void setCropRegion(@Nullable final Rect crop) { diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraImpl.java index b656f64410e..ec589e51a8a 100644 --- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraImpl.java +++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraImpl.java @@ -713,6 +713,9 @@ final class Camera2CameraImpl implements CameraInternal { } } + // CameraControl is active when there are any online use cases. + mCameraControlInternal.setActive(true); + if (Looper.myLooper() != mHandler.getLooper()) { mHandler.post(new Runnable() { @Override @@ -768,7 +771,6 @@ final class Camera2CameraImpl implements CameraInternal { }); } - private void updateCameraControlPreviewAspectRatio(Collection<UseCase> useCases) { for (UseCase useCase : useCases) { if (useCase instanceof Preview) { @@ -780,8 +782,8 @@ final class Camera2CameraImpl implements CameraInternal { } } - private void clearCameraControlPreviewAspectRatio(Collection<UseCase> useCases) { - for (UseCase useCase : useCases) { + private void clearCameraControlPreviewAspectRatio(Collection<UseCase> removedUseCases) { + for (UseCase useCase : removedUseCases) { if (useCase instanceof Preview) { mCameraControlInternal.setPreviewAspectRatio(null); return; @@ -810,6 +812,7 @@ final class Camera2CameraImpl implements CameraInternal { } Log.d(TAG, "Use cases " + useCases + " OFFLINE for camera " + mCameraId); + clearCameraControlPreviewAspectRatio(useCases); synchronized (mAttachedUseCaseLock) { List<UseCase> useCasesChangedToOffline = new ArrayList<>(); for (UseCase useCase : useCases) { @@ -826,6 +829,7 @@ final class Camera2CameraImpl implements CameraInternal { notifyStateOfflineToUseCases(useCasesChangedToOffline); if (mUseCaseAttachState.getOnlineUseCases().isEmpty()) { + mCameraControlInternal.setActive(false); resetCaptureSession(/*abortInFlightCaptures=*/false); close(); return; @@ -839,7 +843,6 @@ final class Camera2CameraImpl implements CameraInternal { openCaptureSession(); } - clearCameraControlPreviewAspectRatio(useCases); } /** Returns an interface to retrieve characteristics of the camera. */ @@ -849,7 +852,8 @@ final class Camera2CameraImpl implements CameraInternal { synchronized (mCameraInfoLock) { if (mCameraInfoInternal == null) { // Lazily instantiate camera info - mCameraInfoInternal = new Camera2CameraInfo(mCameraManager.unwrap(), mCameraId); + mCameraInfoInternal = new Camera2CameraInfo(mCameraManager.unwrap(), mCameraId, + mCameraControlInternal.getZoomControl()); } return mCameraInfoInternal; diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraInfo.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraInfo.java index 272691f227a..4add6295c5b 100644 --- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraInfo.java +++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2CameraInfo.java @@ -40,10 +40,13 @@ import androidx.lifecycle.MutableLiveData; final class Camera2CameraInfo implements CameraInfoInternal { private final CameraCharacteristics mCameraCharacteristics; + private final ZoomControl mZoomControl; private static final String TAG = "Camera2CameraInfo"; private MutableLiveData<Boolean> mFlashAvailability; - Camera2CameraInfo(CameraManager cameraManager, String cameraId) + + Camera2CameraInfo(@NonNull CameraManager cameraManager, @NonNull String cameraId, + @NonNull ZoomControl zoomControl) throws CameraInfoUnavailableException { try { mCameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId); @@ -52,6 +55,7 @@ final class Camera2CameraInfo implements CameraInfoInternal { "Unable to retrieve info for camera " + cameraId, e); } + mZoomControl = zoomControl; mFlashAvailability = new MutableLiveData<>( mCameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)); checkCharacteristicAvailable( @@ -158,4 +162,27 @@ final class Camera2CameraInfo implements CameraInfoInternal { return mFlashAvailability; } + @NonNull + @Override + public LiveData<Float> getZoomRatio() { + return mZoomControl.getZoomRatio(); + } + + @NonNull + @Override + public LiveData<Float> getMaxZoomRatio() { + return mZoomControl.getMaxZoomRatio(); + } + + @NonNull + @Override + public LiveData<Float> getMinZoomRatio() { + return mZoomControl.getMinZoomRatio(); + } + + @NonNull + @Override + public LiveData<Float> getZoomPercentage() { + return mZoomControl.getZoomPercentage(); + } } diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/FocusMeteringControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/FocusMeteringControl.java index d34d9a8b47d..3e7eec67c1a 100644 --- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/FocusMeteringControl.java +++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/FocusMeteringControl.java @@ -23,6 +23,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.params.MeteringRectangle; import android.os.Build; +import android.util.Log; import android.util.Rational; import androidx.annotation.NonNull; @@ -61,12 +62,15 @@ import java.util.concurrent.TimeUnit; * construct the 3A regions and append them to all repeating requests and single requests. */ class FocusMeteringControl { + private static final String TAG = "FocusMeteringControl"; private final Camera2CameraControl mCameraControl; @SuppressWarnings("WeakerAccess") /* synthetic accessor */ @CameraExecutor final Executor mExecutor; private final ScheduledExecutorService mScheduler; + private volatile boolean mIsActive = false; + //******************** Should only be accessed by executor (WorkThread) ****************// private FocusMeteringAction mCurrentFocusMeteringAction; private boolean mIsInAfAutoMode = false; @@ -99,6 +103,25 @@ class FocusMeteringControl { } /** + * Set current active state. Set active if it is ready to accept focus/metering operations. + * + * <p> In inactive state, startFocusAndMetering does nothing while cancelFocusAndMetering + * still works to cancel current operation. cancelFocusAndMetering is performed automatically + * when active state is changed to false. + */ + void setActive(boolean isActive) { + if (isActive == mIsActive) { + return; + } + + mIsActive = isActive; + + if (!mIsActive) { + mExecutor.execute(() -> cancelFocusAndMetering()); + } + } + + /** * Called by {@link Camera2CameraControl} to append the 3A regions to the shared options. It * applies to all repeating requests and single requests. */ @@ -111,7 +134,6 @@ class FocusMeteringControl { configBuilder.setCaptureRequestOption( CaptureRequest.CONTROL_AF_MODE, mCameraControl.getSupportedAfMode(afMode)); - if (mAfRects.length != 0) { configBuilder.setCaptureRequestOption( CaptureRequest.CONTROL_AF_REGIONS, mAfRects); @@ -193,6 +215,11 @@ class FocusMeteringControl { @WorkerThread void startFocusAndMetering(@NonNull FocusMeteringAction action, @Nullable Rational defaultAspectRatio) { + if (!mIsActive) { + Log.e(TAG, "Ignore startFocusAndMetering because camera is not active."); + return; + } + if (mCurrentFocusMeteringAction != null) { cancelFocusAndMetering(); } @@ -251,6 +278,10 @@ class FocusMeteringControl { @WorkerThread void triggerAf() { + if (!mIsActive) { + return; + } + CaptureConfig.Builder builder = new CaptureConfig.Builder(); builder.setTemplateType(getDefaultTemplate()); builder.setUseRepeatingSurface(true); @@ -263,6 +294,10 @@ class FocusMeteringControl { @WorkerThread void triggerAePrecapture() { + if (!mIsActive) { + return; + } + CaptureConfig.Builder builder = new CaptureConfig.Builder(); builder.setTemplateType(getDefaultTemplate()); builder.setUseRepeatingSurface(true); @@ -276,6 +311,10 @@ class FocusMeteringControl { @WorkerThread void cancelAfAeTrigger(final boolean cancelAfTrigger, final boolean cancelAePrecaptureTrigger) { + if (!mIsActive) { + return; + } + CaptureConfig.Builder builder = new CaptureConfig.Builder(); builder.setUseRepeatingSurface(true); builder.setTemplateType(getDefaultTemplate()); diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2CameraInfoTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2CameraInfoTest.java index 56849432570..b1b89c545b1 100644 --- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2CameraInfoTest.java +++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2CameraInfoTest.java @@ -18,6 +18,9 @@ package androidx.camera.camera2.impl; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import android.content.Context; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; @@ -27,6 +30,7 @@ import android.view.Surface; import androidx.camera.core.CameraInfoInternal; import androidx.camera.core.CameraInfoUnavailableException; import androidx.camera.core.LensFacing; +import androidx.lifecycle.MutableLiveData; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; @@ -73,13 +77,15 @@ public class Camera2CameraInfoTest { @Test public void canCreateCameraInfo() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + mock(ZoomControl.class)); assertThat(cameraInfoInternal).isNotNull(); } @Test public void cameraInfo_canReturnSensorOrientation() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + mock(ZoomControl.class)); assertThat(cameraInfoInternal.getSensorRotationDegrees()).isEqualTo( CAMERA0_SENSOR_ORIENTATION); } @@ -87,7 +93,8 @@ public class Camera2CameraInfoTest { @Test public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + mock(ZoomControl.class)); // Note: these numbers depend on the camera being a back-facing camera. assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0)) @@ -103,7 +110,8 @@ public class Camera2CameraInfoTest { @Test public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA1_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA1_ID, + mock(ZoomControl.class)); // Note: these numbers depend on the camera being a front-facing camera. assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0)) @@ -118,14 +126,16 @@ public class Camera2CameraInfoTest { @Test public void cameraInfo_canReturnLensFacing() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + mock(ZoomControl.class)); assertThat(cameraInfoInternal.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM); } @Test public void cameraInfo_canReturnFlashAvailable_forBackCamera() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + mock(ZoomControl.class)); assertThat(cameraInfoInternal.isFlashAvailable().getValue().booleanValue()).isEqualTo( CAMERA0_FLASH_INFO_BOOLEAN); } @@ -133,11 +143,52 @@ public class Camera2CameraInfoTest { @Test public void cameraInfo_canReturnFlashAvailable_forFrontCamera() throws CameraInfoUnavailableException { - CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA1_ID); + CameraInfoInternal cameraInfoInternal = new Camera2CameraInfo(mCameraManager, CAMERA1_ID, + mock(ZoomControl.class)); assertThat(cameraInfoInternal.isFlashAvailable().getValue().booleanValue()).isEqualTo( CAMERA1_FLASH_INFO_BOOLEAN); } + // zoom related tests just ensure it uses ZoomControl to get the value + // Full tests are performed at ZoomControlTest / ZoomControlRoboTest. + @Test + public void cameraInfo_getZoomRatio_valueIsCorrect() throws CameraInfoUnavailableException { + ZoomControl zoomControl = mock(ZoomControl.class); + CameraInfoInternal cameraInfo = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + zoomControl); + when(zoomControl.getZoomRatio()).thenReturn(new MutableLiveData<>(3.0f)); + assertThat(cameraInfo.getZoomRatio().getValue()).isEqualTo(3.0f); + } + + @Test + public void cameraInfo_getZoomPercentage_valueIsCorrect() + throws CameraInfoUnavailableException { + ZoomControl zoomControl = mock(ZoomControl.class); + CameraInfoInternal cameraInfo = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + zoomControl); + when(zoomControl.getZoomPercentage()).thenReturn(new MutableLiveData<>(0.2f)); + assertThat(cameraInfo.getZoomPercentage().getValue()).isEqualTo(0.2f); + } + + @Test + public void cameraInfo_getMaxZoomRatio_valueIsCorrect() throws CameraInfoUnavailableException { + ZoomControl zoomControl = mock(ZoomControl.class); + CameraInfoInternal cameraInfo = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + zoomControl); + when(zoomControl.getMaxZoomRatio()).thenReturn(new MutableLiveData<>(8.0f)); + assertThat(cameraInfo.getMaxZoomRatio().getValue()).isEqualTo(8.0f); + } + + @Test + public void cameraInfo_getMinZoomRatio_valueIsCorrect() throws CameraInfoUnavailableException { + ZoomControl zoomControl = mock(ZoomControl.class); + CameraInfoInternal cameraInfo = new Camera2CameraInfo(mCameraManager, CAMERA0_ID, + zoomControl); + when(zoomControl.getMinZoomRatio()).thenReturn(new MutableLiveData<>(1.0f)); + assertThat(cameraInfo.getMinZoomRatio().getValue()).isEqualTo(1.0f); + } + + private void initCameras() { // **** Camera 0 characteristics ****// CameraCharacteristics characteristics0 = diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2DeviceSurfaceManagerTest.java index 2c461e71566..cfcc36052c9 100644 --- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2DeviceSurfaceManagerTest.java +++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2DeviceSurfaceManagerTest.java @@ -23,6 +23,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; @@ -580,7 +581,7 @@ public final class Camera2DeviceSurfaceManagerTest { LensFacing lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(lensFacing); mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null, - new Camera2CameraInfo(cameraManager, cameraId))); + new Camera2CameraInfo(cameraManager, cameraId, mock(ZoomControl.class)))); } private void initCameraX() { diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/FocusMeteringControlTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/FocusMeteringControlTest.java index 0b4ede7efd1..43e06a2e358 100644 --- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/FocusMeteringControlTest.java +++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/FocusMeteringControlTest.java @@ -121,6 +121,7 @@ public class FocusMeteringControlTest { public void setUp() throws CameraAccessException { initCameras(); mFocusMeteringControl = spy(initFocusMeteringControl(CAMERA0_ID)); + mFocusMeteringControl.setActive(true); } @After @@ -149,9 +150,10 @@ public class FocusMeteringControlTest { mCameraExecutor, updateCallback)); - - return new FocusMeteringControl(mCamera2CameraControl, + FocusMeteringControl focusMeteringControl = new FocusMeteringControl(mCamera2CameraControl, CameraXExecutors.mainThreadExecutor(), mCameraExecutor); + focusMeteringControl.setActive(true); + return focusMeteringControl; } private void initCameras() { @@ -896,5 +898,4 @@ public class FocusMeteringControlTest { mFocusMeteringControl.cancelFocusAndMetering(); verifyAfMode(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE); } - } diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/SupportedSurfaceCombinationTest.java index c5ed8d5a29b..c9bce8128a0 100644 --- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/SupportedSurfaceCombinationTest.java +++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/SupportedSurfaceCombinationTest.java @@ -23,6 +23,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; @@ -1122,7 +1123,7 @@ public final class SupportedSurfaceCombinationTest { LensFacing lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(lensFacing); mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null, - new Camera2CameraInfo(cameraManager, cameraId))); + new Camera2CameraInfo(cameraManager, cameraId, mock(ZoomControl.class)))); } private void initCameraX() { diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java index 28a86fc07d0..85e4780de16 100644 --- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java +++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java @@ -16,10 +16,13 @@ package androidx.camera.core; +import androidx.annotation.FloatRange; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.camera.core.FocusMeteringAction.OnAutoFocusListener; +import com.google.common.util.concurrent.ListenableFuture; + /** * An interface for controlling camera's zoom, focus and metering across all use cases. * @@ -29,8 +32,8 @@ public interface CameraControl { /** * Starts a focus and metering action by the {@link FocusMeteringAction}. * - * The {@link FocusMeteringAction} contains the configuration of multiple 3A - * {@link MeteringPoint}s, auto-cancel duration and{@link OnAutoFocusListener} to receive the + * <p>The {@link FocusMeteringAction} contains the configuration of multiple 3A + * {@link MeteringPoint}s, auto-cancel duration and{ @link OnAutoFocusListener} to receive the * auto-focus result. Check {@link FocusMeteringAction} for more details. * * @param action the {@link FocusMeteringAction} to be executed. @@ -41,12 +44,49 @@ public interface CameraControl { * Cancels current {@link FocusMeteringAction}. * * <p>It clears the 3A regions and update current AF mode to CONTINOUS AF (if supported). - * If auto-focus does not completes, it will notify the {@link OnAutoFocusListener} with + * If auto-focus does not complete, it will notify the {@link OnAutoFocusListener} with * isFocusLocked set to false. */ void cancelFocusAndMetering(); /** + * Sets current zoom by ratio. + * + * <p>It modifies both current zoom ratio and zoom percentage so if apps are observing + * zoomRatio or zoomPercentage, they will get the update as well. If the ratio is + * smaller than {@link CameraInfo#getMinZoomRatio()} or larger than + * {@link CameraInfo#getMaxZoomRatio()}, it won't modify current zoom ratio. It is + * applications' duty to clamp the ratio. + * + * @return a {@link ListenableFuture} which is finished when current repeating request + * result contains the requested zoom ratio. It fails with + * {@link OperationCanceledException} if there is newer value being set or camera is closed. + * If ratio is out of range, it fails with + * {@link CameraControl.ArgumentOutOfRangeException}. + */ + @NonNull + ListenableFuture<Void> setZoomRatio(float ratio); + + /** + * Sets current zoom by percentage ranging from 0f to 1.0f. Percentage 0f represents the + * minimum zoom while percentage 1.0f represents the maximum zoom. One advantage of zoom + * percentage is that it ensures FOV varies linearly with the percentage value. + * + * <p>It modifies both current zoom ratio and zoom percentage so if apps are observing + * zoomRatio or zoomPercentage, they will get the update as well. If the percentage is not in + * the range [0..1], it won't modify current zoom percentage and zoom ratio. It is + * applications' duty to clamp the zoomPercentage within [0..1]. + * + * @return a {@link ListenableFuture} which is finished when current repeating request + * result contains the requested zoom percentage. It fails with + * {@link OperationCanceledException} if there is newer value being set or camera is closed. + * If percentage is out of range, it fails with + * {@link CameraControl.ArgumentOutOfRangeException}. + */ + @NonNull + ListenableFuture<Void> setZoomPercentage(@FloatRange(from = 0f, to = 1f) float percentage); + + /** * An exception thrown when the argument is out of range. */ final class ArgumentOutOfRangeException extends Exception { diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControlInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControlInternal.java index 989b89d788a..e5e60ee1d5a 100644 --- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControlInternal.java +++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControlInternal.java @@ -22,6 +22,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; +import androidx.camera.core.impl.utils.futures.Futures; + +import com.google.common.util.concurrent.ListenableFuture; import java.util.List; @@ -131,6 +134,18 @@ public interface CameraControlInternal extends CameraControl { public void cancelFocusAndMetering() { } + + @NonNull + @Override + public ListenableFuture<Void> setZoomRatio(float ratio) { + return Futures.immediateFuture(null); + } + + @NonNull + @Override + public ListenableFuture<Void> setZoomPercentage(float percentage) { + return Futures.immediateFuture(null); + } }; /** Listener called when CameraControlInternal need to notify event. */ diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java index 9d42f84f0d7..ddfb0a0afa9 100644 --- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java +++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java @@ -19,7 +19,9 @@ package androidx.camera.core; import android.view.Surface; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; /** * An interface for retrieving camera information. @@ -50,4 +52,62 @@ public interface CameraInfo { /** Returns if flash unit is available or not. */ @NonNull LiveData<Boolean> isFlashAvailable(); + + /** + * Returns a {@link LiveData} of current zoom ratio. + * + * <p>Apps can either get immediate value via {@link LiveData#getValue()} (The value is never + * null, it has default value in the beginning) or they can observe it via + * {@link LiveData#observe(LifecycleOwner, Observer)} to update zoom UI accordingly. + * + * <p>Setting zoom ratio or zoom percentage will both trigger the change event. + * + * @return a {@link LiveData} containing current zoom ratio. + */ + @NonNull + LiveData<Float> getZoomRatio(); + + /** + * Returns a {@link LiveData} of the maximum zoom ratio. + * + * <p>Apps can either get immediate value via {@link LiveData#getValue()} (The value is never + * null, it has default value in the beginning) or they can observe it via + * {@link LiveData#observe(LifecycleOwner, Observer)} to update zoom UI accordingly. + * + * <p>While the value is fixed most of the time, enabling extension could change the maximum + * zoom ratio. + * + * @return a {@link LiveData} containing the maximum zoom ratio value. + */ + @NonNull + LiveData<Float> getMaxZoomRatio(); + + /** + * Returns a {@link LiveData} of the minimum zoom ratio. + * + * <p>Apps can either get immediate value via {@link LiveData#getValue()} (The value is never + * null, it has default value in the beginning) or they can observe it via + * {@link LiveData#observe(LifecycleOwner, Observer)} to update zoom UI accordingly. + * + * <p>While the value is fixed most of the time, enabling extension could change the minimum + * zoom ratio value. + * + * @return a {@link LiveData} containing the minimum zoom ratio value. + */ + @NonNull + LiveData<Float> getMinZoomRatio(); + + /** + * Returns a {@link LiveData} of current zoom percentage which is in range [0..1]. + * Percentage 0 represents the maximum zoom while percentage 1.0 represents the maximum zoom. + * + * <p>Apps can either get immediate value via {@link LiveData#getValue()} (The value is never + * null, it has default value in the beginning) or they can observe it via + * {@link LiveData#observe(LifecycleOwner, Observer)} to update zoom UI accordingly. + * <p>Setting zoom ratio or zoom percentage will both trigger the change event. + * + * @return a {@link LiveData} containing current zoom percentage. + */ + @NonNull + LiveData<Float> getZoomPercentage(); } diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ShadowCameraX.java b/camera/camera-core/src/test/java/androidx/camera/core/ShadowCameraX.java index c117a72d43f..0cbda8fb131 100644 --- a/camera/camera-core/src/test/java/androidx/camera/core/ShadowCameraX.java +++ b/camera/camera-core/src/test/java/androidx/camera/core/ShadowCameraX.java @@ -35,13 +35,19 @@ public class ShadowCameraX { new ImageAnalysisConfig.Builder().setSessionOptionUnpacker( new SessionConfig.OptionUnpacker() { @Override - public void unpack(UseCaseConfig<?> config, SessionConfig.Builder builder) { + public void unpack(@NonNull UseCaseConfig<?> config, + @NonNull SessionConfig.Builder builder) { // no op. } }).build(); private static final CameraInfo DEFAULT_CAMERA_INFO = new CameraInfoInternal() { MutableLiveData<Boolean> mFlashAvailability = new MutableLiveData<>(Boolean.TRUE); + MutableLiveData<Float> mZoomRatio = new MutableLiveData<>(1.0f); + MutableLiveData<Float> mMaxZoomRatio = new MutableLiveData<>(4.0f); + MutableLiveData<Float> mMinZoomRatio = new MutableLiveData<>(1.0f); + MutableLiveData<Float> mZoomPercentage = new MutableLiveData<>(0f); + @Override public LensFacing getLensFacing() { return LensFacing.BACK; @@ -62,6 +68,30 @@ public class ShadowCameraX { public LiveData<Boolean> isFlashAvailable() { return mFlashAvailability; } + + @NonNull + @Override + public LiveData<Float> getZoomRatio() { + return mZoomRatio; + } + + @NonNull + @Override + public LiveData<Float> getMaxZoomRatio() { + return mMaxZoomRatio; + } + + @NonNull + @Override + public LiveData<Float> getMinZoomRatio() { + return mMinZoomRatio; + } + + @NonNull + @Override + public LiveData<Float> getZoomPercentage() { + return mZoomPercentage; + } }; private static final CameraDeviceSurfaceManager DEFAULT_DEVICE_SURFACE_MANAGER = diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java index a107557f6a9..d1dae01edfc 100644 --- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java +++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java @@ -29,6 +29,9 @@ import androidx.camera.core.CaptureConfig; import androidx.camera.core.FlashMode; import androidx.camera.core.FocusMeteringAction; import androidx.camera.core.SessionConfig; +import androidx.camera.core.impl.utils.futures.Futures; + +import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.List; @@ -157,6 +160,18 @@ public final class FakeCameraControl implements CameraControlInternal { mOnNewCaptureRequestListener = listener; } + @NonNull + @Override + public ListenableFuture<Void> setZoomRatio(float ratio) { + return Futures.immediateFuture(null); + } + + @NonNull + @Override + public ListenableFuture<Void> setZoomPercentage(float percentage) { + return Futures.immediateFuture(null); + } + /** A listener which are used to notify when there are new submitted capture requests */ public interface OnNewCaptureRequestListener { /** Called when there are new submitted capture request */ diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java index b8de8d5cf49..77fffb2b0e0 100644 --- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java +++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java @@ -36,17 +36,21 @@ public final class FakeCameraInfoInternal implements CameraInfoInternal { private final int mSensorRotation; private final LensFacing mLensFacing; - private MutableLiveData<Boolean> mFlashAvailability; + private MutableLiveData<Boolean> mFlashAvailability = new MutableLiveData<>(Boolean.TRUE); + private MutableLiveData<Float> mMaxZoom = new MutableLiveData<>(4.0f); + private MutableLiveData<Float> mMinZoom = new MutableLiveData<>(1.0f); + private MutableLiveData<Float> mZoomRatio = new MutableLiveData<>(1.0f); + private MutableLiveData<Float> mZoomPerecentage = new MutableLiveData<>(0f); + + public FakeCameraInfoInternal() { this(/*sensorRotation=*/ 0, /*lensFacing=*/ LensFacing.BACK); - mFlashAvailability = new MutableLiveData<>(Boolean.TRUE); } public FakeCameraInfoInternal(int sensorRotation, @NonNull LensFacing lensFacing) { mSensorRotation = sensorRotation; mLensFacing = lensFacing; - mFlashAvailability = new MutableLiveData<>(Boolean.TRUE); } @Nullable @@ -79,4 +83,28 @@ public final class FakeCameraInfoInternal implements CameraInfoInternal { public LiveData<Boolean> isFlashAvailable() { return mFlashAvailability; } + + @NonNull + @Override + public LiveData<Float> getZoomRatio() { + return mZoomRatio; + } + + @NonNull + @Override + public LiveData<Float> getMaxZoomRatio() { + return mMaxZoom; + } + + @NonNull + @Override + public LiveData<Float> getMinZoomRatio() { + return mMinZoom; + } + + @NonNull + @Override + public LiveData<Float> getZoomPercentage() { + return mZoomPerecentage; + } } |