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.components.TooltipPopup;
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.v2.MediaAnimations;
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
@ -87,6 +88,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private MemoryFileDescriptor videoFileDescriptor;
private LifecycleCameraController cameraController;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private CameraXModePolicy cameraXModePolicy;
private boolean isThumbAvailable;
private boolean isMediaSelected;
@ -136,13 +138,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
this.previewView = view.findViewById(R.id.camerax_camera);
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.bindToLifecycle(getViewLifecycleOwner());
cameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
cameraController.setTapToFocusEnabled(true);
cameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode());
cameraController.setEnabledUseCases(getSupportedUseCases());
cameraXModePolicy.initialize(cameraController);
previewView.setScaleType(PREVIEW_SCALE_TYPE);
previewView.setController(cameraController);
@ -335,7 +342,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
if (isVideoRecordingSupported(requireContext())) {
if (Build.VERSION.SDK_INT >= 26 && cameraXModePolicy.isVideoSupported()) {
try {
closeVideoFileDescriptor();
videoFileDescriptor = CameraXVideoCaptureHelper.createFileDescriptor(requireContext());
@ -356,6 +363,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
cameraController,
previewView,
videoFileDescriptor,
cameraXModePolicy,
maxDuration,
new CameraXVideoCaptureHelper.Callback() {
@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) {
if (shouldDisplayVideoRecordingTooltip()) {
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.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
@ -51,6 +52,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
private final @NonNull ValueAnimator updateProgressAnimator;
private final @NonNull Debouncer debouncer;
private final @NonNull CameraXModePolicy cameraXModePolicy;
private ValueAnimator cameraMetricsAnimator;
@ -81,6 +83,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
@NonNull CameraController cameraController,
@NonNull PreviewView previewView,
@NonNull MemoryFileDescriptor memoryFileDescriptor,
@NonNull CameraXModePolicy cameraXModePolicy,
int maxVideoDurationSec,
@NonNull Callback callback)
{
@ -91,6 +94,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
this.callback = callback;
this.updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
this.cameraXModePolicy = cameraXModePolicy;
updateProgressAnimator.setInterpolator(new LinearInterpolator());
updateProgressAnimator.addUpdateListener(anim -> captureButton.setProgress(anim.getAnimatedFraction()));
@ -123,6 +127,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
@SuppressLint("RestrictedApi")
private void beginCameraRecording() {
cameraXModePolicy.setToVideo(cameraController);
this.cameraController.setZoomRatio(Objects.requireNonNull(this.cameraController.getZoomState().getValue()).getMinZoomRatio());
callback.onVideoRecordStarted();
shrinkCaptureArea();
@ -196,6 +201,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
updateProgressAnimator.cancel();
debouncer.clear();
cameraXModePolicy.setToImage(cameraController);
}
@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_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList";
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 CDS_V2_LOAD_TEST = "android.cdsV2LoadTest";
private static final String SMS_EXPORTER = "android.sms.exporter";
@ -153,6 +154,7 @@ public final class FeatureFlags {
TELECOM_MANUFACTURER_ALLOWLIST,
TELECOM_MODEL_BLOCKLIST,
CAMERAX_MODEL_BLOCKLIST,
CAMERAX_MIXED_MODEL_BLOCKLIST,
RECIPIENT_MERGE_V2,
CDS_V2_LOAD_TEST,
SMS_EXPORTER,
@ -492,6 +494,11 @@ public final class FeatureFlags {
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. */
public static boolean useHardwareAecIfOlderThanApi29() {
return getBoolean(USE_HARDWARE_AEC_IF_OLD, false);