diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea15c2cbd8..f1ece04bba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -704,6 +704,7 @@ android:theme="@style/TextSecure.DarkNoActionBar" android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" android:launchMode="singleTop" + android:screenOrientation="portrait" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:exported="false"/> diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 01b6aef9b0..080d9c3e64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -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); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java index fd516e2861..28351a505a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index b387698931..93fce43023 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXController.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXController.kt new file mode 100644 index 0000000000..a84eae6756 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXController.kt @@ -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): 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 + + @MainThread + fun getZoomState(): LiveData + + @MainThread + fun setCameraSelector(selector: CameraSelector) + + @MainThread + fun getCameraSelector(): CameraSelector + + @MainThread + fun hasCamera(selectedCamera: CameraSelector): Boolean + + @MainThread + fun addInitializationCompletedListener(executor: Executor, onComplete: () -> Unit) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt index cad9cb9bed..23a71f8d80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt @@ -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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/PlatformCameraController.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/PlatformCameraController.kt new file mode 100644 index 0000000000..0231d98ed0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/PlatformCameraController.kt @@ -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): 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 { + return delegate.setZoomRatio(ratio) + } + + override fun getZoomState(): LiveData { + 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) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SignalCameraController.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SignalCameraController.kt index 5ada5c6291..a99c2a8b0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SignalCameraController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SignalCameraController.kt @@ -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): Recording { + override fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, executor: Executor, videoSavedListener: Consumer): 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 { + override fun setZoomRatio(ratio: Float): ListenableFuture { ThreadUtil.assertMainThread() return cameraProperty.cameraControl.setZoomRatio(ratio) } @MainThread - fun getZoomState(): LiveData { + override fun getZoomState(): LiveData { 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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index a16619d96c..84529d23e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index e8e332e793..f656e87db9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -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 getMemoryValues() { return new TreeMap<>(REMOTE_VALUES);