Clean up unused custom camera controller.

This commit is contained in:
Nicholas Tinsley 2024-08-16 11:03:18 -04:00 committed by mtang-signal
parent 66278a0eac
commit 8d38f6f5e7
11 changed files with 65 additions and 880 deletions

View file

@ -6,16 +6,12 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
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;
@ -29,10 +25,16 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.AspectRatio;
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;
@ -41,6 +43,7 @@ import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.card.MaterialCardView;
import com.google.common.util.concurrent.ListenableFuture;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.concurrent.SimpleTask;
@ -50,19 +53,15 @@ import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXController;
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.PlatformCameraController;
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;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
@ -76,7 +75,6 @@ import java.util.concurrent.Executors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import kotlin.Unit;
import static org.thoughtcrime.securesms.permissions.PermissionDeniedBottomSheet.showPermissionFragment;
@ -91,7 +89,6 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private static final String IS_QR_SCAN_ENABLED = "is_qr_scan_enabled";
private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
private static final PreviewView.ScaleType PREVIEW_SCALE_TYPE = PreviewView.ScaleType.FILL_CENTER;
private PreviewView previewView;
@ -100,8 +97,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private Controller controller;
private View selfieFlash;
private MemoryFileDescriptor videoFileDescriptor;
private CameraXController cameraController;
private CameraXOrientationListener orientationListener;
private LifecycleCameraController cameraController;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private CameraXModePolicy cameraXModePolicy;
private CameraScreenBrightnessController cameraScreenBrightnessController;
@ -148,8 +144,6 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
if (controller == null) {
throw new IllegalStateException("Parent must implement controller interface.");
}
this.orientationListener = new CameraXOrientationListener(context);
}
@Override
@ -178,62 +172,49 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
previewView.setScaleType(PREVIEW_SCALE_TYPE);
if (RemoteConfig.customCameraXController()) {
View focusIndicator = view.findViewById(R.id.camerax_focus_indicator);
cameraController = new SignalCameraController(requireContext(), getViewLifecycleOwner(), previewView, focusIndicator);
} else {
PlatformCameraController platformController = new PlatformCameraController(requireContext());
platformController.initializeAndBind(requireContext(), getViewLifecycleOwner());
previewView.setController(platformController.getDelegate());
cameraController = platformController;
}
cameraXModePolicy.initialize(cameraController);
final LifecycleCameraController lifecycleCameraController = new LifecycleCameraController(requireContext());
cameraController = lifecycleCameraController;
lifecycleCameraController.bindToLifecycle(getViewLifecycleOwner());
lifecycleCameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
lifecycleCameraController.setTapToFocusEnabled(true);
lifecycleCameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode());
lifecycleCameraController.setVideoCaptureQualitySelector(QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD)));
previewView.setController(lifecycleCameraController);
cameraXModePolicy.initialize(lifecycleCameraController);
cameraScreenBrightnessController = new CameraScreenBrightnessController(
requireActivity().getWindow(),
new CameraStateProvider(cameraController)
new CameraStateProvider(lifecycleCameraController)
);
previewView.setScaleType(PREVIEW_SCALE_TYPE);
onOrientationChanged();
lifecycleCameraController.setImageCaptureTargetSize(new CameraController.OutputSize(AspectRatio.RATIO_16_9));
if (RemoteConfig.customCameraXController()) {
cameraController.initializeAndBind(requireContext(), getViewLifecycleOwner());
}
controlsContainer.removeAllViews();
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(R.layout.camera_controls_portrait, controlsContainer, false));
initControls(lifecycleCameraController);
if (requireArguments().getBoolean(IS_QR_SCAN_ENABLED, false)) {
cameraController.setImageAnalysisAnalyzer(qrAnalysisExecutor, imageProxy -> {
try {
lifecycleCameraController.setImageAnalysisAnalyzer(qrAnalysisExecutor, imageProxy -> {
try (imageProxy) {
String data = qrProcessor.getScannedData(imageProxy);
if (data != null) {
controller.onQrCodeFound(data);
}
} finally {
imageProxy.close();
}
});
}
}
@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(), () -> Log.d(TAG, "Camera init complete from onResume"));
cameraController.bindToLifecycle(getViewLifecycleOwner());
Log.d(TAG, "Camera init complete from onResume");
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
if (hasCameraPermission()) {
missingPermissionsContainer.setVisibility(View.GONE);
@ -334,19 +315,6 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
return Permissions.hasAll(requireContext(), Manifest.permission.CAMERA);
}
private void onOrientationChanged() {
int layout = R.layout.camera_controls_portrait;
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
Size size = CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, true);
cameraController.setImageCaptureTargetSize(size);
controlsContainer.removeAllViews();
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
initControls();
}
private void presentRecentItemThumbnail(@Nullable Media media) {
View thumbBackground = controlsContainer.findViewById(R.id.camera_gallery_button_background);
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
@ -414,7 +382,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
}
@SuppressLint({ "ClickableViewAccessibility", "MissingPermission" })
private void initControls() {
private void initControls(LifecycleCameraController lifecycleCameraController) {
View flipButton = requireView().findViewById(R.id.camera_flip_button);
CameraButtonView captureButton = requireView().findViewById(R.id.camera_capture_button);
View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
@ -430,8 +398,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
selfieFlash = requireView().findViewById(R.id.camera_selfie_flash);
final ListenableFuture<Void> cameraInitFuture = lifecycleCameraController.getInitializationFuture();
captureButton.setOnClickListener(v -> {
if (hasCameraPermission() && cameraController.isInitialized()) {
if (hasCameraPermission() && cameraInitFuture.isDone()) {
captureButton.setEnabled(false);
flipButton.setEnabled(false);
flashButton.setEnabled(false);
@ -443,10 +412,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
previewView.setScaleType(PREVIEW_SCALE_TYPE);
cameraController.addInitializationCompletedListener(ContextCompat.getMainExecutor(requireContext()), () -> initializeFlipButton(flipButton, flashButton));
cameraInitFuture.addListener(() -> initializeFlipButton(flipButton, flashButton), ContextCompat.getMainExecutor(requireContext()));
flashButton.setAutoFlashEnabled(cameraController.getImageCaptureFlashMode() >= ImageCapture.FLASH_MODE_AUTO);
flashButton.setFlash(cameraController.getImageCaptureFlashMode());
flashButton.setAutoFlashEnabled(lifecycleCameraController.getImageCaptureFlashMode() >= ImageCapture.FLASH_MODE_AUTO);
flashButton.setFlash(lifecycleCameraController.getImageCaptureFlashMode());
flashButton.setOnFlashModeChangedListener(mode -> {
cameraController.setImageCaptureFlashMode(mode);
cameraScreenBrightnessController.onCameraFlashChanged(mode == ImageCapture.FLASH_MODE_ON);
@ -473,7 +442,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper(
this,
captureButton,
cameraController,
lifecycleCameraController,
previewView,
videoFileDescriptor,
cameraXModePolicy,
@ -630,14 +599,14 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
}
@SuppressLint({ "MissingPermission" })
private Unit initializeFlipButton(@NonNull View flipButton, @NonNull CameraXFlashToggleView flashButton) {
private void initializeFlipButton(@NonNull View flipButton, @NonNull CameraXFlashToggleView flashButton) {
if (getContext() == null) {
Log.w(TAG, "initializeFlipButton called either before or after fragment was attached.");
return Unit.INSTANCE;
return;
}
if (!getLifecycle().getCurrentState().isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED)) {
return Unit.INSTANCE;
return;
}
getViewLifecycleOwner().getLifecycle().addObserver(cameraScreenBrightnessController);
@ -661,7 +630,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
GestureDetector gestureDetector = new GestureDetector(requireContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
public boolean onDoubleTap(@NonNull MotionEvent e) {
if (flipButton.isEnabled()) {
flipButton.performClick();
}
@ -674,14 +643,13 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
} else {
flipButton.setVisibility(View.GONE);
}
return Unit.INSTANCE;
}
private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
private final CameraXController cameraController;
private final CameraController cameraController;
private CameraStateProvider(CameraXController cameraController) {
private CameraStateProvider(CameraController cameraController) {
this.cameraController = cameraController;
}
@ -695,20 +663,4 @@ 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) {
if (RemoteConfig.customCameraXController()) {
cameraController.setImageRotation(orientation);
}
}
}
}
}

View file

@ -7,9 +7,7 @@ import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXController;
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
import androidx.camera.view.CameraController;
final class CameraXSelfieFlashHelper {
@ -17,7 +15,7 @@ final class CameraXSelfieFlashHelper {
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
private final Window window;
private final CameraXController camera;
private final CameraController camera;
private final View selfieFlash;
private float brightnessBeforeFlash;
@ -25,7 +23,7 @@ final class CameraXSelfieFlashHelper {
private int flashMode = -1;
CameraXSelfieFlashHelper(@NonNull Window window,
@NonNull CameraXController camera,
@NonNull CameraController camera,
@NonNull View selfieFlash)
{
this.window = window;

View file

@ -17,6 +17,7 @@ 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.content.ContextCompat;
@ -25,7 +26,6 @@ import androidx.fragment.app.Fragment;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXController;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ContextUtil;
@ -46,14 +46,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 CameraXController 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 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 ValueAnimator cameraMetricsAnimator;
@ -87,7 +87,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
CameraXVideoCaptureHelper(@NonNull Fragment fragment,
@NonNull CameraButtonView captureButton,
@NonNull CameraXController cameraController,
@NonNull CameraController cameraController,
@NonNull PreviewView previewView,
@NonNull MemoryFileDescriptor memoryFileDescriptor,
@NonNull CameraXModePolicy cameraXModePolicy,

View file

@ -1,85 +0,0 @@
/*
* 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 androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ZoomState
import androidx.camera.video.FileDescriptorOutputOptions
import androidx.camera.video.Recording
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.video.AudioConfig
import androidx.core.util.Consumer
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.Executor
interface CameraXController {
fun isInitialized(): Boolean
fun initializeAndBind(context: Context, lifecycleOwner: LifecycleOwner)
@RequiresPermission(Manifest.permission.CAMERA)
fun bindToLifecycle(lifecycleOwner: LifecycleOwner, onCameraBoundListener: Runnable)
@MainThread
fun unbind()
@MainThread
fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback)
@RequiresApi(26)
@MainThread
fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, executor: Executor, videoSavedListener: Consumer<VideoRecordEvent>): Recording
@MainThread
fun setImageAnalysisAnalyzer(executor: Executor, analyzer: ImageAnalysis.Analyzer)
@MainThread
fun setEnabledUseCases(useCaseFlags: Int)
@MainThread
fun getImageCaptureFlashMode(): Int
@MainThread
fun setPreviewTargetSize(size: Size)
@MainThread
fun setImageCaptureTargetSize(size: Size)
@MainThread
fun setImageRotation(rotation: Int)
@MainThread
fun setImageCaptureFlashMode(flashMode: Int)
@MainThread
fun setZoomRatio(ratio: Float): ListenableFuture<Void>
@MainThread
fun getZoomState(): LiveData<ZoomState>
@MainThread
fun setCameraSelector(selector: CameraSelector)
@MainThread
fun getCameraSelector(): CameraSelector
@MainThread
fun hasCamera(selectedCamera: CameraSelector): Boolean
@MainThread
fun addInitializationCompletedListener(executor: Executor, onComplete: () -> Unit)
}

View file

@ -17,9 +17,9 @@ sealed class CameraXModePolicy {
abstract val isQrScanEnabled: Boolean
abstract fun initialize(cameraController: CameraXController)
abstract fun initialize(cameraController: CameraController)
open fun initialize(cameraController: CameraXController, useCaseFlags: Int) {
open fun initialize(cameraController: CameraController, useCaseFlags: Int) {
if (isQrScanEnabled) {
cameraController.setEnabledUseCases(useCaseFlags or CameraController.IMAGE_ANALYSIS)
} else {
@ -27,9 +27,9 @@ sealed class CameraXModePolicy {
}
}
open fun setToImage(cameraController: CameraXController) = Unit
open fun setToImage(cameraController: CameraController) = Unit
open fun setToVideo(cameraController: CameraXController) = Unit
open fun setToVideo(cameraController: CameraController) = Unit
/**
* The device supports having Image and Video enabled at the same time
@ -38,7 +38,7 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = true
override fun initialize(cameraController: CameraXController) {
override fun initialize(cameraController: CameraController) {
super.initialize(cameraController, CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)
}
}
@ -50,15 +50,15 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = true
override fun initialize(cameraController: CameraXController) {
override fun initialize(cameraController: CameraController) {
setToImage(cameraController)
}
override fun setToImage(cameraController: CameraXController) {
override fun setToImage(cameraController: CameraController) {
super.initialize(cameraController, CameraController.IMAGE_CAPTURE)
}
override fun setToVideo(cameraController: CameraXController) {
override fun setToVideo(cameraController: CameraController) {
super.initialize(cameraController, CameraController.VIDEO_CAPTURE)
}
}
@ -70,7 +70,7 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = false
override fun initialize(cameraController: CameraXController) {
override fun initialize(cameraController: CameraController) {
super.initialize(cameraController, CameraController.IMAGE_CAPTURE)
}
}

View file

@ -14,7 +14,6 @@ import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.os.Build;
import android.util.Pair;
import android.util.Rational;
import android.util.Size;
import androidx.annotation.NonNull;
@ -127,21 +126,6 @@ public class CameraXUtil {
: ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY;
}
public static int getIdealResolution(int displayWidth, int displayHeight) {
int maxDisplay = Math.max(displayWidth, displayHeight);
return Math.max(maxDisplay, 1920);
}
public static @NonNull Size buildResolutionForRatio(int longDimension, @NonNull Rational ratio, boolean isPortrait) {
int shortDimension = longDimension * ratio.getDenominator() / ratio.getNumerator();
if (isPortrait) {
return new Size(shortDimension, longDimension);
} else {
return new Size(longDimension, shortDimension);
}
}
private static byte[] transformByteArray(@NonNull byte[] data, @Nullable Rect cropRect, int rotation, boolean flip) throws IOException {
Stopwatch stopwatch = new Stopwatch("transform");
Bitmap in;

View file

@ -1,115 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.mediasend.camerax
import android.content.Context
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ZoomState
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.FileDescriptorOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recording
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.video.AudioConfig
import androidx.core.util.Consumer
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import com.google.common.util.concurrent.ListenableFuture
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.util.concurrent.Executor
class PlatformCameraController(context: Context) : CameraXController {
val delegate = LifecycleCameraController(context)
override fun isInitialized(): Boolean {
return delegate.initializationFuture.isDone
}
override fun initializeAndBind(context: Context, lifecycleOwner: LifecycleOwner) {
delegate.bindToLifecycle(lifecycleOwner)
delegate.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(context)))
delegate.setTapToFocusEnabled(true)
delegate.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode())
delegate.setVideoCaptureQualitySelector(QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD)))
}
override fun bindToLifecycle(lifecycleOwner: LifecycleOwner, onCameraBoundListener: Runnable) {
delegate.bindToLifecycle(lifecycleOwner)
onCameraBoundListener.run()
}
override fun unbind() {
delegate.unbind()
}
override fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
delegate.takePicture(executor, callback)
}
@RequiresApi(26)
override fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, executor: Executor, videoSavedListener: Consumer<VideoRecordEvent>): Recording {
return delegate.startRecording(outputOptions, audioConfig, executor, videoSavedListener)
}
override fun setImageAnalysisAnalyzer(executor: Executor, analyzer: ImageAnalysis.Analyzer) {
delegate.setImageAnalysisAnalyzer(executor, analyzer)
}
override fun setEnabledUseCases(useCaseFlags: Int) {
delegate.setEnabledUseCases(useCaseFlags)
}
override fun getImageCaptureFlashMode(): Int {
return delegate.imageCaptureFlashMode
}
override fun setPreviewTargetSize(size: Size) {
delegate.previewTargetSize = CameraController.OutputSize(size)
}
override fun setImageCaptureTargetSize(size: Size) {
delegate.imageCaptureTargetSize = CameraController.OutputSize(size)
}
override fun setImageRotation(rotation: Int) {
throw NotImplementedError("Not supported by the platform camera controller!")
}
override fun setImageCaptureFlashMode(flashMode: Int) {
delegate.imageCaptureFlashMode = flashMode
}
override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
return delegate.setZoomRatio(ratio)
}
override fun getZoomState(): LiveData<ZoomState> {
return delegate.zoomState
}
override fun setCameraSelector(selector: CameraSelector) {
delegate.cameraSelector = selector
}
override fun getCameraSelector(): CameraSelector {
return delegate.cameraSelector
}
override fun hasCamera(selectedCamera: CameraSelector): Boolean {
return delegate.hasCamera(selectedCamera)
}
override fun addInitializationCompletedListener(executor: Executor, onComplete: () -> Unit) {
delegate.initializationFuture.addListener(onComplete, executor)
}
}

View file

@ -1,540 +0,0 @@
/*
* 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.ScaleGestureDetector
import android.view.Surface
import android.view.View
import android.view.ViewConfiguration
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.DisplayOrientedMeteringPointFactory
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
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.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager
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.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController.InitializationListener
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.util.concurrent.Executor
import kotlin.math.max
import kotlin.math.min
/**
* 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(
private val context: Context,
private val lifecycleOwner: LifecycleOwner,
private val previewView: PreviewView,
private val focusIndicator: View
) : CameraXController {
companion object {
val TAG = Log.tag(SignalCameraController::class.java)
private const val AF_SIZE = 1.0f / 6.0f
private const val AE_SIZE = AF_SIZE * 1.5f
@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 scaleGestureDetector = ScaleGestureDetector(context, PinchToZoomGestureListener())
private val initializationCompleteListeners: MutableSet<InitializationListener> = mutableSetOf()
private val customUseCases: MutableList<UseCase> = mutableListOf()
private var tapToFocusEvents = 0
private var listenerAdded = false
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 extensionsManager: ExtensionsManager
private lateinit var cameraProperty: Camera
override fun isInitialized(): Boolean {
return this::cameraProvider.isInitialized && this::extensionsManager.isInitialized
}
override fun initializeAndBind(context: Context, lifecycleOwner: LifecycleOwner) {
bindToLifecycle(lifecycleOwner) { Log.d(TAG, "Camera initialization and binding complete.") }
}
@RequiresPermission(Manifest.permission.CAMERA)
override fun bindToLifecycle(lifecycleOwner: LifecycleOwner, onCameraBoundListener: Runnable) {
ThreadUtil.assertMainThread()
if (isInitialized()) {
bindToLifecycleInternal()
onCameraBoundListener.run()
} else if (!listenerAdded) {
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val extensionsManagerFuture =
ExtensionsManager.getInstanceAsync(context, cameraProvider)
extensionsManagerFuture.addListener({
extensionsManager = extensionsManagerFuture.get()
initializationCompleteListeners.forEach { it.onInitialized(cameraProvider) }
bindToLifecycleInternal()
onCameraBoundListener.run()
}, ContextCompat.getMainExecutor(context))
}, ContextCompat.getMainExecutor(context))
listenerAdded = true
}
}
@MainThread
override fun unbind() {
ThreadUtil.assertMainThread()
cameraProvider.unbindAll()
}
@MainThread
fun bindToLifecycleInternal() {
ThreadUtil.assertMainThread()
try {
if (!this::cameraProvider.isInitialized || !this::extensionsManager.isInitialized) {
Log.d(TAG, "Camera provider not yet initialized.")
return
}
val extCameraSelector = if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.AUTO)) {
Log.d(TAG, "Using CameraX ExtensionMode.AUTO")
extensionsManager.getExtensionEnabledCameraSelector(
cameraSelector,
ExtensionMode.AUTO
)
} else {
Log.d(TAG, "Using standard camera selector")
cameraSelector
}
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
extCameraSelector,
buildUseCaseGroup()
)
initializeTapToFocusAndPinchToZoom(camera)
this.cameraProperty = camera
} catch (e: Exception) {
Log.e(TAG, "Use case binding failed", e)
}
}
@MainThread
override fun setImageAnalysisAnalyzer(executor: Executor, analyzer: ImageAnalysis.Analyzer) {
ThreadUtil.assertMainThread()
val imageAnalysis = ImageAnalysis.Builder()
.setResolutionSelector(ResolutionSelector.Builder().setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build())
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(executor, analyzer)
customUseCases += imageAnalysis
if (isRecording()) {
stopRecording()
}
tryToBindCamera()
}
@MainThread
override fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
ThreadUtil.assertMainThread()
assertImageEnabled()
imageCaptureUseCase.takePicture(executor, callback)
}
@RequiresApi(26)
@MainThread
override fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, executor: Executor, 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
override fun setEnabledUseCases(useCaseFlags: Int) {
ThreadUtil.assertMainThread()
if (enabledUseCases == useCaseFlags) {
return
}
val oldEnabledUseCases = enabledUseCases
enabledUseCases = useCaseFlags
if (isRecording()) {
stopRecording()
}
tryToBindCamera { enabledUseCases = oldEnabledUseCases }
}
@MainThread
override fun getImageCaptureFlashMode(): Int {
ThreadUtil.assertMainThread()
return imageCaptureUseCase.flashMode
}
@MainThread
override 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
override 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
override 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
override fun setImageCaptureFlashMode(flashMode: Int) {
ThreadUtil.assertMainThread()
imageCaptureUseCase.flashMode = flashMode
}
@MainThread
override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
ThreadUtil.assertMainThread()
return cameraProperty.cameraControl.setZoomRatio(ratio)
}
@MainThread
override fun getZoomState(): LiveData<ZoomState> {
ThreadUtil.assertMainThread()
return cameraProperty.cameraInfo.zoomState
}
@MainThread
override 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
override fun getCameraSelector(): CameraSelector {
ThreadUtil.assertMainThread()
return cameraSelector
}
@MainThread
override fun hasCamera(selectedCamera: CameraSelector): Boolean {
ThreadUtil.assertMainThread()
return cameraProvider.hasCamera(selectedCamera)
}
override fun addInitializationCompletedListener(executor: Executor, onComplete: () -> Unit) {
ThreadUtil.assertMainThread()
initializationCompleteListeners.add(InitializationListener { onComplete() })
}
@MainThread
private fun tryToBindCamera(restoreStateRunnable: (() -> Unit)? = null) {
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)
}
for (useCase in customUseCases) {
addUseCase(useCase)
}
previewView.getViewPort(Surface.ROTATION_0)?.let { setViewPort(it) } ?: Log.d(TAG, "ViewPort was null, not adding to UseCase builder.")
}.build()
@MainThread
private fun initializeTapToFocusAndPinchToZoom(camera: Camera) {
ThreadUtil.assertMainThread()
previewView.setOnTouchListener { v: View?, event: MotionEvent ->
val isSingleTouch = event.pointerCount == 1
val isUpEvent = event.action == MotionEvent.ACTION_UP
val notALongPress = (event.eventTime - event.downTime < ViewConfiguration.getLongPressTimeout())
if (isSingleTouch && isUpEvent && notALongPress) {
focusAndMeterOnPoint(camera, event.x, event.y)
v?.performClick()
return@setOnTouchListener true
}
return@setOnTouchListener scaleGestureDetector.onTouchEvent(event)
}
}
@MainThread
private fun focusAndMeterOnPoint(camera: Camera, x: Float, y: Float) {
ThreadUtil.assertMainThread()
val meteringPointFactory = DisplayOrientedMeteringPointFactory(previewView.display, camera.cameraInfo, previewView.width.toFloat(), previewView.height.toFloat())
val afPoint = meteringPointFactory.createPoint(x, y, AF_SIZE)
val aePoint = meteringPointFactory.createPoint(x, y, AE_SIZE)
val action = FocusMeteringAction.Builder(afPoint, FocusMeteringAction.FLAG_AF)
.addPoint(aePoint, FocusMeteringAction.FLAG_AE)
.build()
focusIndicator.x = x - (focusIndicator.width / 2)
focusIndicator.y = y - (focusIndicator.height / 2)
focusIndicator.visible = true
tapToFocusEvents += 1
Futures.addCallback(
camera.cameraControl.startFocusAndMetering(action),
object : FutureCallback<FocusMeteringResult> {
override fun onSuccess(result: FocusMeteringResult?) {
Log.d(TAG, "Tap to focus was successful? ${result?.isFocusSuccessful}")
tapToFocusEvents -= 1
if (tapToFocusEvents <= 0) {
ViewUtil.fadeOut(focusIndicator, 80)
}
}
override fun onFailure(t: Throwable) {
Log.d(TAG, "Tap to focus could not be completed due to an exception.", t)
tapToFocusEvents -= 1
if (tapToFocusEvents <= 0) {
ViewUtil.fadeOut(focusIndicator, 80)
}
}
},
ContextCompat.getMainExecutor(context)
)
}
@MainThread
private fun onPinchToZoom(pinchToZoomScale: Float) {
val zoomState = getZoomState().getValue() ?: return
var clampedRatio: Float = zoomState.zoomRatio * if (pinchToZoomScale > 1f) {
1.0f + (pinchToZoomScale - 1.0f) * 2
} else {
1.0f - (1.0f - pinchToZoomScale) * 2
}
clampedRatio = min(
max(clampedRatio.toDouble(), zoomState.minZoomRatio.toDouble()),
zoomState.maxZoomRatio.toDouble()
).toFloat()
setZoomRatio(clampedRatio)
}
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)
}
inner class PinchToZoomGestureListener : ScaleGestureDetector.OnScaleGestureListener {
override fun onScale(detector: ScaleGestureDetector): Boolean {
onPinchToZoom(detector.scaleFactor)
return true
}
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
override fun onScaleEnd(detector: ScaleGestureDetector) = Unit
}
fun interface InitializationListener {
fun onInitialized(cameraProvider: ProcessCameraProvider)
}
}

View file

@ -45,7 +45,6 @@ import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
@ -92,7 +91,7 @@ class MediaSelectionActivity :
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
setContentView(R.layout.media_selection_activity)
if (RemoteConfig.customCameraXController) {
if (false) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
@ -163,6 +162,7 @@ class MediaSelectionActivity :
TransitionManager.beginDelayedTransition(textStoryToggle, AutoTransition().setDuration(200))
cameraSelectedConstraintSet.applyTo(textStoryToggle)
}
R.id.textStoryPostCreationFragment -> {
textStoryToggle.visible = canDisplayStorySwitch()
@ -170,6 +170,7 @@ class MediaSelectionActivity :
TransitionManager.beginDelayedTransition(textStoryToggle, AutoTransition().setDuration(200))
textSelectedConstraintSet.applyTo(textStoryToggle)
}
else -> textStoryToggle.visible = false
}
}

View file

@ -8,7 +8,6 @@ import android.os.IBinder
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import kotlinx.collections.immutable.toImmutableSet
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R

View file

@ -1031,15 +1031,6 @@ object RemoteConfig {
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false)
}
/** Whether or not to use the custom CameraX controller class */
@JvmStatic
@get:JvmName("customCameraXController")
val customCameraXController: Boolean by remoteBoolean(
key = "android.cameraXCustomController",
defaultValue = false,
hotSwappable = true
)
/** Whether unauthenticated chat web socket is backed by libsignal-net */
@JvmStatic
@get:JvmName("libSignalWebSocketEnabled")