mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 02:08:40 +00:00
Put custom controller behind feature flag.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user