Add blocklist for mixed-mode capture.
This commit is contained in:
parent
afedbf40e3
commit
79b3b9190a
4 changed files with 115 additions and 19 deletions
|
@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXFlashToggleView;
|
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.CameraXUtil;
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
|
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
|
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
|
||||||
|
@ -87,6 +88,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||||
private MemoryFileDescriptor videoFileDescriptor;
|
private MemoryFileDescriptor videoFileDescriptor;
|
||||||
private LifecycleCameraController cameraController;
|
private LifecycleCameraController cameraController;
|
||||||
private Disposable mostRecentItemDisposable = Disposable.disposed();
|
private Disposable mostRecentItemDisposable = Disposable.disposed();
|
||||||
|
private CameraXModePolicy cameraXModePolicy;
|
||||||
|
|
||||||
private boolean isThumbAvailable;
|
private boolean isThumbAvailable;
|
||||||
private boolean isMediaSelected;
|
private boolean isMediaSelected;
|
||||||
|
@ -136,13 +138,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||||
|
|
||||||
this.previewView = view.findViewById(R.id.camerax_camera);
|
this.previewView = view.findViewById(R.id.camerax_camera);
|
||||||
this.controlsContainer = view.findViewById(R.id.camerax_controls_container);
|
this.controlsContainer = view.findViewById(R.id.camerax_controls_container);
|
||||||
|
this.cameraXModePolicy = CameraXModePolicy.acquire(requireContext(),
|
||||||
|
controller.getMediaConstraints(),
|
||||||
|
requireArguments().getBoolean(IS_VIDEO_ENABLED, true));
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName());
|
||||||
|
|
||||||
cameraController = new LifecycleCameraController(requireContext());
|
cameraController = new LifecycleCameraController(requireContext());
|
||||||
cameraController.bindToLifecycle(getViewLifecycleOwner());
|
cameraController.bindToLifecycle(getViewLifecycleOwner());
|
||||||
cameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
cameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
||||||
cameraController.setTapToFocusEnabled(true);
|
cameraController.setTapToFocusEnabled(true);
|
||||||
cameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
cameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||||
cameraController.setEnabledUseCases(getSupportedUseCases());
|
cameraXModePolicy.initialize(cameraController);
|
||||||
|
|
||||||
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
||||||
previewView.setController(cameraController);
|
previewView.setController(cameraController);
|
||||||
|
@ -335,7 +342,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||||
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||||
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
|
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
|
||||||
|
|
||||||
if (isVideoRecordingSupported(requireContext())) {
|
if (Build.VERSION.SDK_INT >= 26 && cameraXModePolicy.isVideoSupported()) {
|
||||||
try {
|
try {
|
||||||
closeVideoFileDescriptor();
|
closeVideoFileDescriptor();
|
||||||
videoFileDescriptor = CameraXVideoCaptureHelper.createFileDescriptor(requireContext());
|
videoFileDescriptor = CameraXVideoCaptureHelper.createFileDescriptor(requireContext());
|
||||||
|
@ -356,6 +363,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||||
cameraController,
|
cameraController,
|
||||||
previewView,
|
previewView,
|
||||||
videoFileDescriptor,
|
videoFileDescriptor,
|
||||||
|
cameraXModePolicy,
|
||||||
maxDuration,
|
maxDuration,
|
||||||
new CameraXVideoCaptureHelper.Callback() {
|
new CameraXVideoCaptureHelper.Callback() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -389,23 +397,6 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CameraController.UseCases
|
|
||||||
private int getSupportedUseCases() {
|
|
||||||
if (isVideoRecordingSupported(requireContext())) {
|
|
||||||
return CameraController.IMAGE_CAPTURE | CameraController.VIDEO_CAPTURE;
|
|
||||||
} else {
|
|
||||||
return CameraController.IMAGE_CAPTURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isVideoRecordingSupported(@NonNull Context context) {
|
|
||||||
return Build.VERSION.SDK_INT >= 26 &&
|
|
||||||
requireArguments().getBoolean(IS_VIDEO_ENABLED, true) &&
|
|
||||||
MediaConstraints.isVideoTranscodeAvailable() &&
|
|
||||||
CameraXUtil.isMixedModeSupported(context) &&
|
|
||||||
VideoUtil.getMaxVideoRecordDurationInSeconds(context, controller.getMediaConstraints()) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayVideoRecordingTooltipIfNecessary(CameraButtonView captureButton) {
|
private void displayVideoRecordingTooltipIfNecessary(CameraButtonView captureButton) {
|
||||||
if (shouldDisplayVideoRecordingTooltip()) {
|
if (shouldDisplayVideoRecordingTooltip()) {
|
||||||
int displayRotation = requireActivity().getWindowManager().getDefaultDisplay().getRotation();
|
int displayRotation = requireActivity().getWindowManager().getDefaultDisplay().getRotation();
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.bumptech.glide.util.Executors;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.util.Debouncer;
|
import org.thoughtcrime.securesms.util.Debouncer;
|
||||||
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
||||||
|
@ -51,6 +52,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||||
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
|
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
|
||||||
private final @NonNull ValueAnimator updateProgressAnimator;
|
private final @NonNull ValueAnimator updateProgressAnimator;
|
||||||
private final @NonNull Debouncer debouncer;
|
private final @NonNull Debouncer debouncer;
|
||||||
|
private final @NonNull CameraXModePolicy cameraXModePolicy;
|
||||||
|
|
||||||
private ValueAnimator cameraMetricsAnimator;
|
private ValueAnimator cameraMetricsAnimator;
|
||||||
|
|
||||||
|
@ -81,6 +83,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||||
@NonNull CameraController cameraController,
|
@NonNull CameraController cameraController,
|
||||||
@NonNull PreviewView previewView,
|
@NonNull PreviewView previewView,
|
||||||
@NonNull MemoryFileDescriptor memoryFileDescriptor,
|
@NonNull MemoryFileDescriptor memoryFileDescriptor,
|
||||||
|
@NonNull CameraXModePolicy cameraXModePolicy,
|
||||||
int maxVideoDurationSec,
|
int maxVideoDurationSec,
|
||||||
@NonNull Callback callback)
|
@NonNull Callback callback)
|
||||||
{
|
{
|
||||||
|
@ -91,6 +94,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
|
this.updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
|
||||||
this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
|
this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
|
||||||
|
this.cameraXModePolicy = cameraXModePolicy;
|
||||||
|
|
||||||
updateProgressAnimator.setInterpolator(new LinearInterpolator());
|
updateProgressAnimator.setInterpolator(new LinearInterpolator());
|
||||||
updateProgressAnimator.addUpdateListener(anim -> captureButton.setProgress(anim.getAnimatedFraction()));
|
updateProgressAnimator.addUpdateListener(anim -> captureButton.setProgress(anim.getAnimatedFraction()));
|
||||||
|
@ -123,6 +127,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private void beginCameraRecording() {
|
private void beginCameraRecording() {
|
||||||
|
cameraXModePolicy.setToVideo(cameraController);
|
||||||
this.cameraController.setZoomRatio(Objects.requireNonNull(this.cameraController.getZoomState().getValue()).getMinZoomRatio());
|
this.cameraController.setZoomRatio(Objects.requireNonNull(this.cameraController.getZoomState().getValue()).getMinZoomRatio());
|
||||||
callback.onVideoRecordStarted();
|
callback.onVideoRecordStarted();
|
||||||
shrinkCaptureArea();
|
shrinkCaptureArea();
|
||||||
|
@ -196,6 +201,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||||
|
|
||||||
updateProgressAnimator.cancel();
|
updateProgressAnimator.cancel();
|
||||||
debouncer.clear();
|
debouncer.clear();
|
||||||
|
cameraXModePolicy.setToImage(cameraController);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package org.thoughtcrime.securesms.mediasend.camerax
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.camera.view.CameraController
|
||||||
|
import androidx.camera.view.video.ExperimentalVideo
|
||||||
|
import org.signal.core.util.asListContains
|
||||||
|
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
import org.thoughtcrime.securesms.video.VideoUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes device capabilities
|
||||||
|
*/
|
||||||
|
@RequiresApi(21)
|
||||||
|
@ExperimentalVideo
|
||||||
|
sealed class CameraXModePolicy {
|
||||||
|
|
||||||
|
abstract val isVideoSupported: Boolean
|
||||||
|
|
||||||
|
abstract fun initialize(cameraController: CameraController)
|
||||||
|
|
||||||
|
open fun setToImage(cameraController: CameraController) = Unit
|
||||||
|
|
||||||
|
open fun setToVideo(cameraController: CameraController) = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device supports having Image and Video enabled at the same time
|
||||||
|
*/
|
||||||
|
object Mixed : CameraXModePolicy() {
|
||||||
|
|
||||||
|
override val isVideoSupported: Boolean = true
|
||||||
|
|
||||||
|
override fun initialize(cameraController: CameraController) {
|
||||||
|
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device supports image and video, but only one mode at a time.
|
||||||
|
*/
|
||||||
|
object Single : CameraXModePolicy() {
|
||||||
|
|
||||||
|
override val isVideoSupported: Boolean = true
|
||||||
|
|
||||||
|
override fun initialize(cameraController: CameraController) {
|
||||||
|
setToImage(cameraController)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setToImage(cameraController: CameraController) {
|
||||||
|
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setToVideo(cameraController: CameraController) {
|
||||||
|
cameraController.setEnabledUseCases(CameraController.VIDEO_CAPTURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device supports taking images only.
|
||||||
|
*/
|
||||||
|
object ImageOnly : CameraXModePolicy() {
|
||||||
|
|
||||||
|
override val isVideoSupported: Boolean = false
|
||||||
|
|
||||||
|
override fun initialize(cameraController: CameraController) {
|
||||||
|
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun acquire(context: Context, mediaConstraints: MediaConstraints, isVideoEnabled: Boolean): CameraXModePolicy {
|
||||||
|
val isVideoSupported = Build.VERSION.SDK_INT >= 26 &&
|
||||||
|
isVideoEnabled &&
|
||||||
|
MediaConstraints.isVideoTranscodeAvailable() &&
|
||||||
|
VideoUtil.getMaxVideoRecordDurationInSeconds(context, mediaConstraints) > 0
|
||||||
|
|
||||||
|
val isMixedModeSupported = isVideoSupported &&
|
||||||
|
Build.VERSION.SDK_INT >= 26 &&
|
||||||
|
CameraXUtil.isMixedModeSupported(context) &&
|
||||||
|
!FeatureFlags.cameraXMixedModelBlocklist().asListContains(Build.MODEL)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isMixedModeSupported -> Mixed
|
||||||
|
isVideoSupported -> Single
|
||||||
|
else -> ImageOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,6 +97,7 @@ public final class FeatureFlags {
|
||||||
private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList";
|
private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList";
|
||||||
private static final String TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList";
|
private static final String TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList";
|
||||||
private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList";
|
private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList";
|
||||||
|
private static final String CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList";
|
||||||
private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2";
|
private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2";
|
||||||
private static final String CDS_V2_LOAD_TEST = "android.cdsV2LoadTest";
|
private static final String CDS_V2_LOAD_TEST = "android.cdsV2LoadTest";
|
||||||
private static final String SMS_EXPORTER = "android.sms.exporter";
|
private static final String SMS_EXPORTER = "android.sms.exporter";
|
||||||
|
@ -153,6 +154,7 @@ public final class FeatureFlags {
|
||||||
TELECOM_MANUFACTURER_ALLOWLIST,
|
TELECOM_MANUFACTURER_ALLOWLIST,
|
||||||
TELECOM_MODEL_BLOCKLIST,
|
TELECOM_MODEL_BLOCKLIST,
|
||||||
CAMERAX_MODEL_BLOCKLIST,
|
CAMERAX_MODEL_BLOCKLIST,
|
||||||
|
CAMERAX_MIXED_MODEL_BLOCKLIST,
|
||||||
RECIPIENT_MERGE_V2,
|
RECIPIENT_MERGE_V2,
|
||||||
CDS_V2_LOAD_TEST,
|
CDS_V2_LOAD_TEST,
|
||||||
SMS_EXPORTER,
|
SMS_EXPORTER,
|
||||||
|
@ -492,6 +494,11 @@ public final class FeatureFlags {
|
||||||
return getString(CAMERAX_MODEL_BLOCKLIST, "");
|
return getString(CAMERAX_MODEL_BLOCKLIST, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A comma-separated list of manufacturers that should *not* use CameraX mixed mode. */
|
||||||
|
public static @NonNull String cameraXMixedModelBlocklist() {
|
||||||
|
return getString(CAMERAX_MIXED_MODEL_BLOCKLIST, "");
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether or not hardware AEC should be used for calling on devices older than API 29. */
|
/** Whether or not hardware AEC should be used for calling on devices older than API 29. */
|
||||||
public static boolean useHardwareAecIfOlderThanApi29() {
|
public static boolean useHardwareAecIfOlderThanApi29() {
|
||||||
return getBoolean(USE_HARDWARE_AEC_IF_OLD, false);
|
return getBoolean(USE_HARDWARE_AEC_IF_OLD, false);
|
||||||
|
|
Loading…
Add table
Reference in a new issue