Clean up unused custom camera controller.
This commit is contained in:
parent
66278a0eac
commit
8d38f6f5e7
11 changed files with 65 additions and 880 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue