Add blocklist for mixed-mode capture.

This commit is contained in:
Alex Hart 2022-10-03 12:26:55 -03:00 committed by Greyson Parrelli
parent afedbf40e3
commit 79b3b9190a
4 changed files with 115 additions and 19 deletions

View file

@ -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();

View file

@ -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

View file

@ -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
}
}
}
}

View file

@ -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);