From 5d03e3d5168eab7c6d113ec5633d39ad5cad3065 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 18 Oct 2019 12:01:01 -0300 Subject: [PATCH] Apply video recording permissions checks and error handling. --- res/values/strings.xml | 3 + .../securesms/mediasend/CameraFragment.java | 1 + .../securesms/mediasend/CameraXFragment.java | 3 +- .../mediasend/CameraXVideoCaptureHelper.java | 77 ++++++++++++++++--- .../mediasend/MediaSendActivity.java | 9 ++- 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 190176fc4f..8e35295cb1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -206,6 +206,9 @@ To capture photos and video, allow Signal access to the camera. Signal needs the Camera permission to take photos or video, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\". Signal needs Camera permissions to take photos or video + Enable the microphone permission to capture videos with sound. + Signal needs microphone permissions to record videos, but they have been denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\". + Signal needs microphone permissions to record videos. %1$s %2$s Signal cannot send SMS/MMS messages because it is not your default SMS app. Would you like to change this in your Android settings? diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java b/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java index b1d43945e3..48c084dd64 100644 --- a/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java @@ -24,6 +24,7 @@ public interface CameraFragment { void onCameraError(); void onImageCaptured(@NonNull byte[] data, int width, int height); void onVideoCaptured(@NonNull FileDescriptor fd); + void onVideoCaptureError(); void onGalleryClicked(); int getDisplayRotation(); void onCameraCountButtonClicked(); diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 8cac0cef14..23fcaa609d 100644 --- a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -237,6 +237,7 @@ public class CameraXFragment extends Fragment implements CameraFragment { camera.setCaptureMode(CameraXView.CaptureMode.MIXED); captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper( + this, captureButton, camera, videoFileDescriptor, @@ -255,7 +256,7 @@ public class CameraXFragment extends Fragment implements CameraFragment { @Override public void onVideoError(@Nullable Throwable cause) { showAndEnableControlsAfterVideoRecording(captureButton, flashButton, flipButton, inAnimation); - controller.onCameraError(); + controller.onVideoCaptureError(); } } )); diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index 6492852750..841a0736b3 100644 --- a/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.mediasend; +import android.Manifest; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; @@ -8,16 +9,21 @@ import android.util.Log; import android.util.Size; import android.view.ViewGroup; import android.view.animation.LinearInterpolator; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.fragment.app.Fragment; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.mediasend.camerax.CameraXView; import org.thoughtcrime.securesms.mediasend.camerax.VideoCapture; +import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.video.VideoUtil; import java.io.FileDescriptor; @@ -30,10 +36,13 @@ 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 CameraXView camera; private final @NonNull Callback callback; private final @NonNull MemoryFileDescriptor memoryFileDescriptor; + private boolean isRecording; + private ValueAnimator cameraMetricsAnimator; private final ValueAnimator updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f) .setDuration(VideoUtil.VIDEO_MAX_LENGTH_S * 1000); @@ -55,14 +64,17 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener @Nullable Throwable cause) { callback.onVideoError(cause); + Util.runOnMain(() -> resetCameraSizing()); } }; - CameraXVideoCaptureHelper(@NonNull CameraButtonView captureButton, + CameraXVideoCaptureHelper(@NonNull Fragment fragment, + @NonNull CameraButtonView captureButton, @NonNull CameraXView camera, @NonNull MemoryFileDescriptor memoryFileDescriptor, @NonNull Callback callback) { + this.fragment = fragment; this.camera = camera; this.memoryFileDescriptor = memoryFileDescriptor; this.callback = callback; @@ -72,7 +84,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener updateProgressAnimator.addListener(new AnimationCompleteListener() { @Override public void onAnimationEnd(Animator animation) { - onVideoCaptureComplete(); + if (isRecording) onVideoCaptureComplete(); } }); } @@ -81,22 +93,43 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener public void onVideoCaptureStarted() { Log.d(TAG, "onVideoCaptureStarted"); - this.camera.setZoomLevel(0f); - callback.onVideoRecordStarted(); - shrinkCaptureArea(() -> { - camera.startRecording(memoryFileDescriptor.getFileDescriptor(), videoSavedListener); - updateProgressAnimator.start(); - }); + if (canRecordAudio()) { + isRecording = true; + beginCameraRecording(); + } else { + displayAudioRecordingPermissionsDialog(); + } } - private void shrinkCaptureArea(@NonNull Runnable onCaptureAreaShrank) { + private boolean canRecordAudio() { + return Permissions.hasAll(fragment.requireContext(), Manifest.permission.RECORD_AUDIO); + } + + private void displayAudioRecordingPermissionsDialog() { + Permissions.with(fragment) + .request(Manifest.permission.RECORD_AUDIO) + .ifNecessary() + .withRationaleDialog(fragment.getString(R.string.ConversationActivity_enable_the_microphone_permission_to_capture_videos_with_sound), R.drawable.ic_mic_solid_24) + .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video)) + .onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_recording_permissions_to_capture_video, Toast.LENGTH_LONG).show()) + .execute(); + } + + private void beginCameraRecording() { + this.camera.setZoomLevel(0f); + callback.onVideoRecordStarted(); + shrinkCaptureArea(); + camera.startRecording(memoryFileDescriptor.getFileDescriptor(), videoSavedListener); + updateProgressAnimator.start(); + } + + private void shrinkCaptureArea() { Size screenSize = getScreenSize(); Size videoRecordingSize = VideoUtil.getVideoRecordingSize(); float scale = getSurfaceScaleForRecording(); float targetWidthForAnimation = videoRecordingSize.getWidth() * scale; float scaleX = targetWidthForAnimation / screenSize.getWidth(); - final ValueAnimator cameraMetricsAnimator; if (scaleX == 1f) { float targetHeightForAnimation = videoRecordingSize.getHeight() * scale; cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getHeight(), targetHeightForAnimation); @@ -110,8 +143,9 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener cameraMetricsAnimator.addListener(new AnimationCompleteListener() { @Override public void onAnimationEnd(Animator animation) { + if (!isRecording) return; + scaleCameraViewToMatchRecordingSizeAndAspectRatio(); - onCaptureAreaShrank.run(); } }); cameraMetricsAnimator.addUpdateListener(animation -> { @@ -150,11 +184,30 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener return Math.min(screenSize.getHeight(), screenSize.getWidth()) / (float) Math.min(videoRecordingSize.getHeight(), videoRecordingSize.getWidth()); } + private void resetCameraSizing() { + ViewGroup.LayoutParams layoutParams = camera.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + + camera.setLayoutParams(layoutParams); + camera.setScaleX(1); + camera.setScaleY(1); + } + @Override public void onVideoCaptureComplete() { + isRecording = false; + if (!canRecordAudio()) return; + Log.d(TAG, "onVideoCaptureComplete"); - updateProgressAnimator.cancel(); camera.stopRecording(); + + + if (cameraMetricsAnimator != null && cameraMetricsAnimator.isRunning()) { + cameraMetricsAnimator.reverse(); + } + + updateProgressAnimator.cancel(); } @Override diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index 44be0569b8..e5b0081e9d 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -10,6 +10,7 @@ import android.graphics.Rect; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Vibrator; import android.text.Editable; import android.text.TextWatcher; import android.view.KeyEvent; @@ -36,7 +37,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.TransportOptions; -import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.SendButton; @@ -69,6 +69,7 @@ import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState; import org.thoughtcrime.securesms.util.Function3; import org.thoughtcrime.securesms.util.IOFunction; import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -384,6 +385,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple finish(); } + @Override + public void onVideoCaptureError() { + Vibrator vibrator = ServiceUtil.getVibrator(this); + vibrator.vibrate(50); + } + @Override public void onImageCaptured(@NonNull byte[] data, int width, int height) { Log.i(TAG, "Camera image captured.");