CameraX Custom Controller.

Addresses #12817, #13316, #13389
This commit is contained in:
Nicholas Tinsley
2024-02-22 13:53:56 -05:00
committed by Alex Hart
parent 39bc6d5eb3
commit e91ed88785
5 changed files with 500 additions and 50 deletions

View File

@@ -15,6 +15,7 @@ import android.util.Size;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
@@ -30,18 +31,12 @@ import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.video.FallbackStrategy;
import androidx.camera.video.Quality;
import androidx.camera.video.QualitySelector;
import androidx.camera.view.CameraController;
import androidx.camera.view.LifecycleCameraController;
import androidx.camera.view.PreviewView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.util.Executors;
import com.google.android.material.card.MaterialCardView;
import org.signal.core.util.Stopwatch;
@@ -54,6 +49,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXFlashToggleView;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
@@ -87,7 +83,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private Controller controller;
private View selfieFlash;
private MemoryFileDescriptor videoFileDescriptor;
private LifecycleCameraController cameraController;
private SignalCameraController cameraController;
private CameraXOrientationListener orientationListener;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private CameraXModePolicy cameraXModePolicy;
private CameraScreenBrightnessController cameraScreenBrightnessController;
@@ -124,6 +121,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
if (controller == null) {
throw new IllegalStateException("Parent must implement controller interface.");
}
this.orientationListener = new CameraXOrientationListener(context);
}
@Override
@@ -144,11 +143,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName());
cameraController = new LifecycleCameraController(requireContext());
cameraController.bindToLifecycle(getViewLifecycleOwner());
cameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
cameraController.setTapToFocusEnabled(true);
cameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode());
cameraController = new SignalCameraController(requireContext(), getViewLifecycleOwner(), previewView);
cameraXModePolicy.initialize(cameraController);
cameraScreenBrightnessController = new CameraScreenBrightnessController(
@@ -157,9 +152,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
);
previewView.setScaleType(PREVIEW_SCALE_TYPE);
previewView.setController(cameraController);
onOrientationChanged();
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onViewCreated"));
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
// Let's assume portrait for now, so 9:16
@@ -175,16 +170,29 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
params.height = (int) height;
cameraParent.setLayoutParams(params);
cameraController.setPreviewTargetSize(new CameraController.OutputSize(new Size((int) width, (int) height)));
cameraController.setPreviewTargetSize(new Size((int) width, (int) height));
}
});
}
@Override
public void onStart() {
super.onStart();
orientationListener.enable();
}
@Override
public void onStop() {
super.onStop();
orientationListener.disable();
}
@Override
public void onResume() {
super.onResume();
cameraController.bindToLifecycle(getViewLifecycleOwner());
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onResume"));
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
@@ -237,10 +245,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
Size size = CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, true);
CameraController.OutputSize outputSize = new CameraController.OutputSize(size);
cameraController.setImageCaptureTargetSize(outputSize);
cameraController.setVideoCaptureQualitySelector(QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD)));
cameraController.setImageCaptureTargetSize(size);
controlsContainer.removeAllViews();
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
@@ -339,8 +345,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
previewView.setScaleType(PREVIEW_SCALE_TYPE);
cameraController.getInitializationFuture()
.addListener(() -> initializeFlipButton(flipButton, flashButton), Executors.mainThreadExecutor());
cameraController.addInitializationCompletedListener(cameraProvider -> initializeFlipButton(flipButton, flashButton));
flashButton.setAutoFlashEnabled(cameraController.getImageCaptureFlashMode() >= ImageCapture.FLASH_MODE_AUTO);
flashButton.setFlash(cameraController.getImageCaptureFlashMode());
@@ -476,7 +481,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
);
flashHelper.onWillTakePicture();
cameraController.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedCallback() {
cameraController.takePicture(ContextCompat.getMainExecutor(requireContext()), new ImageCapture.OnImageCapturedCallback() {
@Override
public void onCaptureSuccess(@NonNull ImageProxy image) {
flashHelper.endFlash();
@@ -505,8 +510,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
}
@Override
public void onError(ImageCaptureException exception) {
Log.w(TAG, "Failed to capture image", exception);
public void onError(@NonNull ImageCaptureException exception) {
Log.w(TAG, "Failed to capture image due to error " + exception.getImageCaptureError(), exception.getCause());
flashHelper.endFlash();
controller.onCameraError();
}
@@ -571,9 +576,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
private final CameraController cameraController;
private final SignalCameraController cameraController;
private CameraStateProvider(CameraController cameraController) {
private CameraStateProvider(SignalCameraController cameraController) {
this.cameraController = cameraController;
}
@@ -587,4 +592,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
return cameraController.getImageCaptureFlashMode() == ImageCapture.FLASH_MODE_ON;
}
}
private class CameraXOrientationListener extends OrientationEventListener {
public CameraXOrientationListener(Context context) {
super(context);
}
@Override
public void onOrientationChanged(int orientation) {
if (cameraController != null) {
cameraController.setImageRotation(orientation);
}
}
}
}

View File

@@ -7,23 +7,24 @@ import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.view.CameraController;
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
final class CameraXSelfieFlashHelper {
private static final float MAX_SCREEN_BRIGHTNESS = 1f;
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
private final Window window;
private final CameraController camera;
private final View selfieFlash;
private final Window window;
private final SignalCameraController camera;
private final View selfieFlash;
private float brightnessBeforeFlash;
private boolean inFlash;
private int flashMode = -1;
CameraXSelfieFlashHelper(@NonNull Window window,
@NonNull CameraController camera,
@NonNull SignalCameraController camera,
@NonNull View selfieFlash)
{
this.window = window;
@@ -68,7 +69,7 @@ final class CameraXSelfieFlashHelper {
}
private boolean shouldUseViewBasedFlash() {
CameraSelector cameraSelector = camera.getCameraSelector() ;
CameraSelector cameraSelector = camera.getCameraSelector();
return (camera.getImageCaptureFlashMode() == ImageCapture.FLASH_MODE_ON || flashMode == ImageCapture.FLASH_MODE_ON) &&
cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA;

View File

@@ -17,7 +17,6 @@ import androidx.camera.core.ZoomState;
import androidx.camera.video.FileDescriptorOutputOptions;
import androidx.camera.video.Recording;
import androidx.camera.video.VideoRecordEvent;
import androidx.camera.view.CameraController;
import androidx.camera.view.PreviewView;
import androidx.camera.view.video.AudioConfig;
import androidx.core.util.Consumer;
@@ -27,6 +26,7 @@ import com.bumptech.glide.util.Executors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ContextUtil;
@@ -47,14 +47,14 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
private static final String VIDEO_DEBUG_LABEL = "video-capture";
private static final long VIDEO_SIZE = 10 * 1024 * 1024;
private final @NonNull Fragment fragment;
private final @NonNull PreviewView previewView;
private final @NonNull CameraController cameraController;
private final @NonNull Callback callback;
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
private final @NonNull ValueAnimator updateProgressAnimator;
private final @NonNull Debouncer debouncer;
private final @NonNull CameraXModePolicy cameraXModePolicy;
private final @NonNull Fragment fragment;
private final @NonNull PreviewView previewView;
private final @NonNull SignalCameraController cameraController;
private final @NonNull Callback callback;
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
private final @NonNull ValueAnimator updateProgressAnimator;
private final @NonNull Debouncer debouncer;
private final @NonNull CameraXModePolicy cameraXModePolicy;
private ValueAnimator cameraMetricsAnimator;
@@ -88,7 +88,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
CameraXVideoCaptureHelper(@NonNull Fragment fragment,
@NonNull CameraButtonView captureButton,
@NonNull CameraController cameraController,
@NonNull SignalCameraController cameraController,
@NonNull PreviewView previewView,
@NonNull MemoryFileDescriptor memoryFileDescriptor,
@NonNull CameraXModePolicy cameraXModePolicy,
@@ -150,7 +150,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
FileDescriptorOutputOptions outputOptions = new FileDescriptorOutputOptions.Builder(memoryFileDescriptor.getParcelFileDescriptor()).build();
AudioConfig audioConfig = AudioConfig.create(true);
activeRecording = cameraController.startRecording(outputOptions, audioConfig, Executors.mainThreadExecutor(), videoSavedListener);
activeRecording = cameraController.startRecording(outputOptions, audioConfig, videoSavedListener);
updateProgressAnimator.start();
debouncer.publish(this::onVideoCaptureComplete);

View File

@@ -15,11 +15,11 @@ sealed class CameraXModePolicy {
abstract val isVideoSupported: Boolean
abstract fun initialize(cameraController: CameraController)
abstract fun initialize(cameraController: SignalCameraController)
open fun setToImage(cameraController: CameraController) = Unit
open fun setToImage(cameraController: SignalCameraController) = Unit
open fun setToVideo(cameraController: CameraController) = Unit
open fun setToVideo(cameraController: SignalCameraController) = Unit
/**
* The device supports having Image and Video enabled at the same time
@@ -28,7 +28,7 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = true
override fun initialize(cameraController: CameraController) {
override fun initialize(cameraController: SignalCameraController) {
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)
}
}
@@ -40,15 +40,15 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = true
override fun initialize(cameraController: CameraController) {
override fun initialize(cameraController: SignalCameraController) {
setToImage(cameraController)
}
override fun setToImage(cameraController: CameraController) {
override fun setToImage(cameraController: SignalCameraController) {
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
}
override fun setToVideo(cameraController: CameraController) {
override fun setToVideo(cameraController: SignalCameraController) {
cameraController.setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
@@ -60,7 +60,7 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = false
override fun initialize(cameraController: CameraController) {
override fun initialize(cameraController: SignalCameraController) {
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
}
}

View File

@@ -0,0 +1,430 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.mediasend.camerax
import android.Manifest
import android.content.Context
import android.util.Size
import android.view.MotionEvent
import android.view.Surface
import android.view.View
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.camera.core.Camera
import androidx.camera.core.CameraProvider
import androidx.camera.core.CameraSelector
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.ViewPort
import androidx.camera.core.ZoomState
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.FileDescriptorOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.CameraController
import androidx.camera.view.PreviewView
import androidx.camera.view.video.AudioConfig
import androidx.core.content.ContextCompat
import androidx.core.util.Consumer
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import com.google.common.util.concurrent.ListenableFuture
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executor
/**
* This is a class to manage the camera resource, and relies on the AndroidX CameraX library.
*
* The API is a subset of the [CameraController] class, but with a few additions such as [setImageRotation].
*/
class SignalCameraController(val context: Context, val lifecycleOwner: LifecycleOwner, private val previewView: PreviewView) {
companion object {
val TAG = Log.tag(SignalCameraController::class.java)
@JvmStatic
private fun isLandscape(surfaceRotation: Int): Boolean {
return surfaceRotation == Surface.ROTATION_90 || surfaceRotation == Surface.ROTATION_270
}
}
private val videoQualitySelector: QualitySelector = QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD))
private val imageMode = CameraXUtil.getOptimalCaptureMode()
private val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(context)
private val viewPort: ViewPort? = previewView.getViewPort(Surface.ROTATION_0)
private val initializationCompleteListeners: MutableSet<InitializationListener> = mutableSetOf()
private var imageRotation = 0
private var recording: Recording? = null
private var previewTargetSize: Size? = null
private var imageCaptureTargetSize: Size? = null
private var cameraSelector: CameraSelector = CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(context))
private var enabledUseCases: Int = CameraController.IMAGE_CAPTURE
private var previewUseCase: Preview = createPreviewUseCase()
private var imageCaptureUseCase: ImageCapture = createImageCaptureUseCase()
private var videoCaptureUseCase: VideoCapture<Recorder> = createVideoCaptureRecorder()
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var camera: Camera
@RequiresPermission(Manifest.permission.CAMERA)
fun bindToLifecycle(onCameraBoundListener: Runnable) {
ThreadUtil.assertMainThread()
if (this::cameraProvider.isInitialized) {
bindToLifecycleInternal()
onCameraBoundListener.run()
} else {
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
initializationCompleteListeners.forEach { it.onInitialized(cameraProvider) }
bindToLifecycleInternal()
onCameraBoundListener.run()
}, ContextCompat.getMainExecutor(context))
}
}
@MainThread
fun unbind() {
ThreadUtil.assertMainThread()
cameraProvider.unbindAll()
}
@MainThread
private fun bindToLifecycleInternal() {
ThreadUtil.assertMainThread()
try {
if (!this::cameraProvider.isInitialized) {
Log.d(TAG, "Camera provider not yet initialized.")
return
}
camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
buildUseCaseGroup()
)
initializeTapToFocus()
} catch (e: Exception) {
Log.e(TAG, "Use case binding failed", e)
}
}
@MainThread
fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
ThreadUtil.assertMainThread()
assertImageEnabled()
imageCaptureUseCase.takePicture(executor, callback)
}
@RequiresApi(26)
@MainThread
fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, videoSavedListener: Consumer<VideoRecordEvent>): Recording {
ThreadUtil.assertMainThread()
assertVideoEnabled()
recording?.stop()
recording = null
val startedRecording = videoCaptureUseCase.output
.prepareRecording(context, outputOptions)
.apply {
if (audioConfig.audioEnabled) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) {
videoSavedListener.accept(it)
if (it is VideoRecordEvent.Finalize) {
recording = null
}
}
recording = startedRecording
return startedRecording
}
@MainThread
fun setEnabledUseCases(useCaseFlags: Int) {
ThreadUtil.assertMainThread()
if (enabledUseCases == useCaseFlags) {
return
}
val oldEnabledUseCases = enabledUseCases
enabledUseCases = useCaseFlags
if (isRecording()) {
stopRecording()
}
tryToBindCamera { enabledUseCases = oldEnabledUseCases }
}
@MainThread
fun getImageCaptureFlashMode(): Int {
ThreadUtil.assertMainThread()
return imageCaptureUseCase.flashMode
}
@MainThread
fun setPreviewTargetSize(size: Size) {
ThreadUtil.assertMainThread()
if (size == previewTargetSize || previewTargetSize?.equals(size) == true) {
return
}
Log.d(TAG, "Setting Preview dimensions to $size")
previewTargetSize = size
if (this::cameraProvider.isInitialized) {
cameraProvider.unbind(previewUseCase)
}
previewUseCase = createPreviewUseCase()
tryToBindCamera(null)
}
@MainThread
fun setImageCaptureTargetSize(size: Size) {
ThreadUtil.assertMainThread()
if (size == imageCaptureTargetSize || imageCaptureTargetSize?.equals(size) == true) {
return
}
imageCaptureTargetSize = size
if (this::cameraProvider.isInitialized) {
cameraProvider.unbind(imageCaptureUseCase)
}
imageCaptureUseCase = createImageCaptureUseCase()
tryToBindCamera(null)
}
@MainThread
fun setImageRotation(rotation: Int) {
ThreadUtil.assertMainThread()
val newRotation = UseCase.snapToSurfaceRotation(rotation.coerceIn(0, 359))
if (newRotation == imageRotation) {
return
}
if (isLandscape(newRotation) != isLandscape(imageRotation)) {
imageCaptureTargetSize = imageCaptureTargetSize?.swap()
}
videoCaptureUseCase.targetRotation = newRotation
imageCaptureUseCase.targetRotation = newRotation
imageRotation = newRotation
}
@MainThread
fun setImageCaptureFlashMode(flashMode: Int) {
ThreadUtil.assertMainThread()
imageCaptureUseCase.flashMode = flashMode
}
@MainThread
fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
ThreadUtil.assertMainThread()
return camera.cameraControl.setZoomRatio(ratio)
}
@MainThread
fun getZoomState(): LiveData<ZoomState> {
ThreadUtil.assertMainThread()
return camera.cameraInfo.zoomState
}
@MainThread
fun setCameraSelector(selector: CameraSelector) {
ThreadUtil.assertMainThread()
if (selector == cameraSelector) {
return
}
val oldCameraSelector: CameraSelector = cameraSelector
cameraSelector = selector
if (!this::cameraProvider.isInitialized) {
return
}
cameraProvider.unbindAll()
tryToBindCamera { cameraSelector = oldCameraSelector }
}
@MainThread
fun getCameraSelector(): CameraSelector {
ThreadUtil.assertMainThread()
return cameraSelector
}
@MainThread
fun hasCamera(selectedCamera: CameraSelector): Boolean {
ThreadUtil.assertMainThread()
return cameraProvider.hasCamera(selectedCamera)
}
@MainThread
fun addInitializationCompletedListener(listener: InitializationListener) {
ThreadUtil.assertMainThread()
initializationCompleteListeners.add(listener)
}
@MainThread
private fun tryToBindCamera(restoreStateRunnable: (() -> Unit)?) {
ThreadUtil.assertMainThread()
try {
bindToLifecycleInternal()
} catch (e: RuntimeException) {
Log.i(TAG, "Could not re-bind camera!", e)
restoreStateRunnable?.invoke()
}
}
@MainThread
private fun stopRecording() {
ThreadUtil.assertMainThread()
recording?.close()
}
private fun createVideoCaptureRecorder() = VideoCapture.Builder(
Recorder.Builder()
.setQualitySelector(videoQualitySelector)
.build()
)
.setTargetRotation(imageRotation)
.build()
private fun createPreviewUseCase() = Preview.Builder()
.apply {
setTargetRotation(Surface.ROTATION_0)
val size = previewTargetSize
if (size != null) {
setResolutionSelector(
ResolutionSelector.Builder()
.setResolutionStrategy(ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER))
.build()
)
}
}.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
private fun createImageCaptureUseCase(): ImageCapture = ImageCapture.Builder()
.apply {
setCaptureMode(imageMode)
setTargetRotation(imageRotation)
val size = imageCaptureTargetSize
if (size != null) {
setResolutionSelector(
ResolutionSelector.Builder()
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
.setResolutionStrategy(ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER))
.build()
)
}
}.build()
private fun buildUseCaseGroup() = UseCaseGroup.Builder().apply {
addUseCase(previewUseCase)
if (isUseCaseEnabled(CameraController.IMAGE_CAPTURE)) {
addUseCase(imageCaptureUseCase)
} else {
cameraProvider.unbind(imageCaptureUseCase)
}
if (isUseCaseEnabled(CameraController.VIDEO_CAPTURE)) {
addUseCase(videoCaptureUseCase)
} else {
cameraProvider.unbind(videoCaptureUseCase)
}
if (viewPort != null) {
setViewPort(viewPort)
} else {
Log.d(TAG, "ViewPort was null, not adding to UseCase builder.")
}
}.build()
@MainThread
private fun initializeTapToFocus() {
ThreadUtil.assertMainThread()
previewView.setOnTouchListener { v: View?, event: MotionEvent ->
if (event.action == MotionEvent.ACTION_DOWN) {
return@setOnTouchListener true
}
if (event.action == MotionEvent.ACTION_UP) {
focusAndMeterOnPoint(event.x, event.y)
v?.performClick()
return@setOnTouchListener true
}
false
}
}
@MainThread
private fun focusAndMeterOnPoint(x: Float, y: Float) {
ThreadUtil.assertMainThread()
if (this::camera.isInitialized) {
Log.d(TAG, "Can't tap to focus before camera is initialized.")
return
}
val factory = previewView.meteringPointFactory
val point = factory.createPoint(x, y)
val action = FocusMeteringAction.Builder(point).build()
val future: ListenableFuture<FocusMeteringResult> = camera.cameraControl.startFocusAndMetering(action)
future.addListener({
try {
val result = future.get()
Log.d(TAG, "Tap to focus was successful? ${result.isFocusSuccessful}")
} catch (e: ExecutionException) {
Log.d(TAG, "Tap to focus could not be completed due to an exception.", e)
} catch (e: InterruptedException) {
Log.d(TAG, "Tap to focus could not be completed due to an exception.", e)
}
}, ContextCompat.getMainExecutor(context))
}
private fun isRecording(): Boolean {
return recording != null
}
private fun isUseCaseEnabled(mask: Int): Boolean {
return (enabledUseCases and mask) != 0
}
private fun assertVideoEnabled() {
if (!isUseCaseEnabled(CameraController.VIDEO_CAPTURE)) {
throw IllegalStateException("VideoCapture disabled.")
}
}
private fun assertImageEnabled() {
if (!isUseCaseEnabled(CameraController.IMAGE_CAPTURE)) {
throw IllegalStateException("ImageCapture disabled.")
}
}
private fun Size.swap(): Size {
return Size(this.height, this.width)
}
interface InitializationListener {
fun onInitialized(cameraProvider: CameraProvider)
}
}