Put custom controller behind feature flag.

This commit is contained in:
Nicholas Tinsley
2024-03-27 14:35:52 -04:00
parent 42450024fc
commit f126df2120
10 changed files with 304 additions and 90 deletions

View File

@@ -28,12 +28,9 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.camera.view.PreviewView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
@@ -50,14 +47,17 @@ 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.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -70,6 +70,7 @@ import java.util.concurrent.Executors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import kotlin.Unit;
/**
* Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be
@@ -86,11 +87,12 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private static final PreviewView.ScaleType PREVIEW_SCALE_TYPE = PreviewView.ScaleType.FILL_CENTER;
private PreviewView previewView;
private MaterialCardView cameraParent;
private ViewGroup controlsContainer;
private Controller controller;
private View selfieFlash;
private MemoryFileDescriptor videoFileDescriptor;
private SignalCameraController cameraController;
private CameraXController cameraController;
private CameraXOrientationListener orientationListener;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private CameraXModePolicy cameraXModePolicy;
@@ -121,6 +123,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -143,24 +146,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
return inflater.inflate(R.layout.camerax_fragment, container, false);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (cameraController != null) {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
orientationListener.enable();
} else {
orientationListener.disable();
cameraController.setImageRotation(0);
}
}
}
@SuppressLint("MissingPermission")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
ViewGroup cameraParent = view.findViewById(R.id.camerax_camera_parent);
this.cameraParent = view.findViewById(R.id.camerax_camera_parent);
this.previewView = view.findViewById(R.id.camerax_camera);
this.controlsContainer = view.findViewById(R.id.camerax_controls_container);
@@ -170,9 +159,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName());
View focusIndicator = view.findViewById(R.id.camerax_focus_indicator);
cameraController = new SignalCameraController(requireContext(), getViewLifecycleOwner(), previewView, focusIndicator);
previewView.setScaleType(PREVIEW_SCALE_TYPE);
if (FeatureFlags.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);
cameraScreenBrightnessController = new CameraScreenBrightnessController(
@@ -183,15 +181,13 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
previewView.setScaleType(PREVIEW_SCALE_TYPE);
onOrientationChanged();
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onViewCreated"));
if (FeatureFlags.customCameraXController()) {
cameraController.initializeAndBind(requireContext(), getViewLifecycleOwner());
}
if (requireArguments().getBoolean(IS_QR_SCAN_ENABLED, false)) {
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setResolutionSelector(new ResolutionSelector.Builder().setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build())
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(qrAnalysisExecutor, imageProxy -> {
cameraController.setImageAnalysisAnalyzer(qrAnalysisExecutor, imageProxy -> {
try {
String data = qrProcessor.getScannedData(imageProxy);
if (data != null) {
@@ -201,8 +197,6 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
imageProxy.close();
}
});
cameraController.addUseCase(imageAnalysis);
}
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -240,16 +234,13 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
public void onResume() {
super.onResume();
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onResume"));
if (FeatureFlags.customCameraXController()) {
cameraController.bindToLifecycle(getViewLifecycleOwner(), () -> Log.d(TAG, "Camera init complete from onResume"));
}
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -292,8 +283,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
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);
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);
@@ -394,7 +385,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
previewView.setScaleType(PREVIEW_SCALE_TYPE);
cameraController.addInitializationCompletedListener(cameraProvider -> initializeFlipButton(flipButton, flashButton));
cameraController.addInitializationCompletedListener(ContextCompat.getMainExecutor(requireContext()), () -> initializeFlipButton(flipButton, flashButton));
flashButton.setAutoFlashEnabled(cameraController.getImageCaptureFlashMode() >= ImageCapture.FLASH_MODE_AUTO);
flashButton.setFlash(cameraController.getImageCaptureFlashMode());
@@ -581,10 +572,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
}
@SuppressLint({ "MissingPermission" })
private void initializeFlipButton(@NonNull View flipButton, @NonNull CameraXFlashToggleView flashButton) {
private Unit initializeFlipButton(@NonNull View flipButton, @NonNull CameraXFlashToggleView flashButton) {
if (getContext() == null) {
Log.w(TAG, "initializeFlipButton called either before or after fragment was attached.");
return;
return Unit.INSTANCE;
}
getViewLifecycleOwner().getLifecycle().addObserver(cameraScreenBrightnessController);
@@ -621,13 +612,14 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
} else {
flipButton.setVisibility(View.GONE);
}
return Unit.INSTANCE;
}
private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
private final SignalCameraController cameraController;
private final CameraXController cameraController;
private CameraStateProvider(SignalCameraController cameraController) {
private CameraStateProvider(CameraXController cameraController) {
this.cameraController = cameraController;
}
@@ -651,7 +643,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
@Override
public void onOrientationChanged(int orientation) {
if (cameraController != null) {
cameraController.setImageRotation(orientation);
if (FeatureFlags.customCameraXController()) {
cameraController.setImageRotation(orientation);
}
}
}
}

View File

@@ -8,23 +8,24 @@ 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;
final class CameraXSelfieFlashHelper {
private static final float MAX_SCREEN_BRIGHTNESS = 1f;
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
private static final float MAX_SCREEN_BRIGHTNESS = 1f;
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
private final Window window;
private final SignalCameraController camera;
private final View selfieFlash;
private final Window window;
private final CameraXController camera;
private final View selfieFlash;
private float brightnessBeforeFlash;
private boolean inFlash;
private int flashMode = -1;
CameraXSelfieFlashHelper(@NonNull Window window,
@NonNull SignalCameraController camera,
@NonNull CameraXController camera,
@NonNull View selfieFlash)
{
this.window = window;

View File

@@ -19,14 +19,13 @@ import androidx.camera.video.Recording;
import androidx.camera.video.VideoRecordEvent;
import androidx.camera.view.PreviewView;
import androidx.camera.view.video.AudioConfig;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
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.CameraXController;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ContextUtil;
@@ -49,7 +48,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
private final @NonNull Fragment fragment;
private final @NonNull PreviewView previewView;
private final @NonNull SignalCameraController cameraController;
private final @NonNull CameraXController cameraController;
private final @NonNull Callback callback;
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
private final @NonNull ValueAnimator updateProgressAnimator;
@@ -88,7 +87,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
CameraXVideoCaptureHelper(@NonNull Fragment fragment,
@NonNull CameraButtonView captureButton,
@NonNull SignalCameraController cameraController,
@NonNull CameraXController cameraController,
@NonNull PreviewView previewView,
@NonNull MemoryFileDescriptor memoryFileDescriptor,
@NonNull CameraXModePolicy cameraXModePolicy,
@@ -150,7 +149,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
FileDescriptorOutputOptions outputOptions = new FileDescriptorOutputOptions.Builder(memoryFileDescriptor.getParcelFileDescriptor()).build();
AudioConfig audioConfig = AudioConfig.create(true);
activeRecording = cameraController.startRecording(outputOptions, audioConfig, videoSavedListener);
activeRecording = cameraController.startRecording(outputOptions, audioConfig, ContextCompat.getMainExecutor(fragment.requireContext()), videoSavedListener);
updateProgressAnimator.start();
debouncer.publish(this::onVideoCaptureComplete);

View File

@@ -0,0 +1,83 @@
/*
* 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 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

@@ -15,11 +15,11 @@ sealed class CameraXModePolicy {
abstract val isVideoSupported: Boolean
abstract fun initialize(cameraController: SignalCameraController)
abstract fun initialize(cameraController: CameraXController)
open fun setToImage(cameraController: SignalCameraController) = Unit
open fun setToImage(cameraController: CameraXController) = Unit
open fun setToVideo(cameraController: SignalCameraController) = Unit
open fun setToVideo(cameraController: CameraXController) = 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: SignalCameraController) {
override fun initialize(cameraController: CameraXController) {
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: SignalCameraController) {
override fun initialize(cameraController: CameraXController) {
setToImage(cameraController)
}
override fun setToImage(cameraController: SignalCameraController) {
override fun setToImage(cameraController: CameraXController) {
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
}
override fun setToVideo(cameraController: SignalCameraController) {
override fun setToVideo(cameraController: CameraXController) {
cameraController.setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
@@ -60,7 +60,7 @@ sealed class CameraXModePolicy {
override val isVideoSupported: Boolean = false
override fun initialize(cameraController: SignalCameraController) {
override fun initialize(cameraController: CameraXController) {
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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 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

@@ -21,6 +21,7 @@ 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
@@ -52,6 +53,7 @@ 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
@@ -69,7 +71,7 @@ class SignalCameraController(
private val lifecycleOwner: LifecycleOwner,
private val previewView: PreviewView,
private val focusIndicator: View
) {
) : CameraXController {
companion object {
val TAG = Log.tag(SignalCameraController::class.java)
@@ -108,8 +110,12 @@ class SignalCameraController(
private lateinit var extensionsManager: ExtensionsManager
private lateinit var cameraProperty: Camera
override fun initializeAndBind(context: Context, lifecycleOwner: LifecycleOwner) {
bindToLifecycle(lifecycleOwner) { Log.d(TAG, "Camera initialization and binding complete.") }
}
@RequiresPermission(Manifest.permission.CAMERA)
fun bindToLifecycle(onCameraBoundListener: Runnable) {
override fun bindToLifecycle(lifecycleOwner: LifecycleOwner, onCameraBoundListener: Runnable) {
ThreadUtil.assertMainThread()
if (this::cameraProvider.isInitialized && this::extensionsManager.isInitialized) {
bindToLifecycleInternal()
@@ -131,13 +137,13 @@ class SignalCameraController(
}
@MainThread
fun unbind() {
override fun unbind() {
ThreadUtil.assertMainThread()
cameraProvider.unbindAll()
}
@MainThread
private fun bindToLifecycleInternal() {
fun bindToLifecycleInternal() {
ThreadUtil.assertMainThread()
try {
if (!this::cameraProvider.isInitialized || !this::extensionsManager.isInitialized) {
@@ -170,10 +176,16 @@ class SignalCameraController(
}
@MainThread
fun addUseCase(useCase: UseCase) {
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()
customUseCases += useCase
imageAnalysis.setAnalyzer(executor, analyzer)
customUseCases += imageAnalysis
if (isRecording()) {
stopRecording()
@@ -183,7 +195,7 @@ class SignalCameraController(
}
@MainThread
fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
override fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
ThreadUtil.assertMainThread()
assertImageEnabled()
imageCaptureUseCase.takePicture(executor, callback)
@@ -191,7 +203,7 @@ class SignalCameraController(
@RequiresApi(26)
@MainThread
fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, videoSavedListener: Consumer<VideoRecordEvent>): Recording {
override fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, executor: Executor, videoSavedListener: Consumer<VideoRecordEvent>): Recording {
ThreadUtil.assertMainThread()
assertVideoEnabled()
@@ -216,7 +228,7 @@ class SignalCameraController(
}
@MainThread
fun setEnabledUseCases(useCaseFlags: Int) {
override fun setEnabledUseCases(useCaseFlags: Int) {
ThreadUtil.assertMainThread()
if (enabledUseCases == useCaseFlags) {
return
@@ -231,13 +243,13 @@ class SignalCameraController(
}
@MainThread
fun getImageCaptureFlashMode(): Int {
override fun getImageCaptureFlashMode(): Int {
ThreadUtil.assertMainThread()
return imageCaptureUseCase.flashMode
}
@MainThread
fun setPreviewTargetSize(size: Size) {
override fun setPreviewTargetSize(size: Size) {
ThreadUtil.assertMainThread()
if (size == previewTargetSize || previewTargetSize?.equals(size) == true) {
return
@@ -253,7 +265,7 @@ class SignalCameraController(
}
@MainThread
fun setImageCaptureTargetSize(size: Size) {
override fun setImageCaptureTargetSize(size: Size) {
ThreadUtil.assertMainThread()
if (size == imageCaptureTargetSize || imageCaptureTargetSize?.equals(size) == true) {
return
@@ -267,7 +279,7 @@ class SignalCameraController(
}
@MainThread
fun setImageRotation(rotation: Int) {
override fun setImageRotation(rotation: Int) {
ThreadUtil.assertMainThread()
val newRotation = UseCase.snapToSurfaceRotation(rotation.coerceIn(0, 359))
@@ -286,25 +298,25 @@ class SignalCameraController(
}
@MainThread
fun setImageCaptureFlashMode(flashMode: Int) {
override fun setImageCaptureFlashMode(flashMode: Int) {
ThreadUtil.assertMainThread()
imageCaptureUseCase.flashMode = flashMode
}
@MainThread
fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
ThreadUtil.assertMainThread()
return cameraProperty.cameraControl.setZoomRatio(ratio)
}
@MainThread
fun getZoomState(): LiveData<ZoomState> {
override fun getZoomState(): LiveData<ZoomState> {
ThreadUtil.assertMainThread()
return cameraProperty.cameraInfo.zoomState
}
@MainThread
fun setCameraSelector(selector: CameraSelector) {
override fun setCameraSelector(selector: CameraSelector) {
ThreadUtil.assertMainThread()
if (selector == cameraSelector) {
return
@@ -320,21 +332,20 @@ class SignalCameraController(
}
@MainThread
fun getCameraSelector(): CameraSelector {
override fun getCameraSelector(): CameraSelector {
ThreadUtil.assertMainThread()
return cameraSelector
}
@MainThread
fun hasCamera(selectedCamera: CameraSelector): Boolean {
override fun hasCamera(selectedCamera: CameraSelector): Boolean {
ThreadUtil.assertMainThread()
return cameraProvider.hasCamera(selectedCamera)
}
@MainThread
fun addInitializationCompletedListener(listener: InitializationListener) {
override fun addInitializationCompletedListener(executor: Executor, onComplete: () -> Unit) {
ThreadUtil.assertMainThread()
initializationCompleteListeners.add(listener)
initializationCompleteListeners.add(InitializationListener { onComplete() })
}
@MainThread
@@ -519,7 +530,7 @@ class SignalCameraController(
override fun onScaleEnd(detector: ScaleGestureDetector) = Unit
}
interface InitializationListener {
fun interface InitializationListener {
fun onInitialized(cameraProvider: ProcessCameraProvider)
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.mediasend.v2
import android.animation.ValueAnimator
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.os.Bundle
import android.view.KeyEvent
@@ -43,6 +44,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -90,6 +92,10 @@ class MediaSelectionActivity :
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
setContentView(R.layout.media_selection_activity)
if (FeatureFlags.customCameraXController()) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
FullscreenHelper.showSystemUI(window)
WindowUtil.setNavigationBarColor(this, 0x01000000)
WindowUtil.setStatusBarColor(window, Color.TRANSPARENT)

View File

@@ -128,6 +128,7 @@ public final class FeatureFlags {
private static final String LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds";
private static final String MESSAGE_BACKUPS = "android.messageBackups";
private static final String NICKNAMES = "android.nicknames";
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -207,7 +208,8 @@ public final class FeatureFlags {
CDSI_LIBSIGNAL_NET,
RX_MESSAGE_SEND,
LINKED_DEVICE_LIFESPAN_SECONDS,
NICKNAMES
NICKNAMES,
CAMERAX_CUSTOM_CONTROLLER
);
@VisibleForTesting
@@ -283,7 +285,8 @@ public final class FeatureFlags {
PREKEY_FORCE_REFRESH_INTERVAL,
CDSI_LIBSIGNAL_NET,
RX_MESSAGE_SEND,
LINKED_DEVICE_LIFESPAN_SECONDS
LINKED_DEVICE_LIFESPAN_SECONDS,
CAMERAX_CUSTOM_CONTROLLER
);
/**
@@ -747,6 +750,11 @@ public final class FeatureFlags {
return getBoolean(NICKNAMES, false);
}
/** Whether or not to use the custom CameraX controller class */
public static boolean customCameraXController() {
return getBoolean(CAMERAX_CUSTOM_CONTROLLER, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);