Apply video recording permissions checks and error handling.

This commit is contained in:
Alex Hart 2019-10-18 12:01:01 -03:00 committed by Greyson Parrelli
parent 2bc3a4417f
commit 5d03e3d516
5 changed files with 79 additions and 14 deletions

View file

@ -206,6 +206,9 @@
<string name="ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera">To capture photos and video, allow Signal access to the camera.</string> <string name="ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera">To capture photos and video, allow Signal access to the camera.</string>
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">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\".</string> <string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">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\".</string>
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Signal needs Camera permissions to take photos or video</string> <string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Signal needs Camera permissions to take photos or video</string>
<string name="ConversationActivity_enable_the_microphone_permission_to_capture_videos_with_sound">Enable the microphone permission to capture videos with sound.</string>
<string name="ConversationActivity_signal_needs_the_recording_permissions_to_capture_video">Signal needs microphone permissions to record videos, but they have been denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\".</string>
<string name="ConversationActivity_signal_needs_recording_permissions_to_capture_video">Signal needs microphone permissions to record videos.</string>
<string name="ConversationActivity_quoted_contact_message">%1$s %2$s</string> <string name="ConversationActivity_quoted_contact_message">%1$s %2$s</string>
<string name="ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app">Signal cannot send SMS/MMS messages because it is not your default SMS app. Would you like to change this in your Android settings?</string> <string name="ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app">Signal cannot send SMS/MMS messages because it is not your default SMS app. Would you like to change this in your Android settings?</string>

View file

@ -24,6 +24,7 @@ public interface CameraFragment {
void onCameraError(); void onCameraError();
void onImageCaptured(@NonNull byte[] data, int width, int height); void onImageCaptured(@NonNull byte[] data, int width, int height);
void onVideoCaptured(@NonNull FileDescriptor fd); void onVideoCaptured(@NonNull FileDescriptor fd);
void onVideoCaptureError();
void onGalleryClicked(); void onGalleryClicked();
int getDisplayRotation(); int getDisplayRotation();
void onCameraCountButtonClicked(); void onCameraCountButtonClicked();

View file

@ -237,6 +237,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
camera.setCaptureMode(CameraXView.CaptureMode.MIXED); camera.setCaptureMode(CameraXView.CaptureMode.MIXED);
captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper( captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper(
this,
captureButton, captureButton,
camera, camera,
videoFileDescriptor, videoFileDescriptor,
@ -255,7 +256,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
@Override @Override
public void onVideoError(@Nullable Throwable cause) { public void onVideoError(@Nullable Throwable cause) {
showAndEnableControlsAfterVideoRecording(captureButton, flashButton, flipButton, inAnimation); showAndEnableControlsAfterVideoRecording(captureButton, flashButton, flipButton, inAnimation);
controller.onCameraError(); controller.onVideoCaptureError();
} }
} }
)); ));

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import android.Manifest;
import android.animation.Animator; import android.animation.Animator;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
@ -8,16 +9,21 @@ import android.util.Log;
import android.util.Size; import android.util.Size;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.LinearInterpolator; import android.view.animation.LinearInterpolator;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
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.CameraXView; import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
import org.thoughtcrime.securesms.mediasend.camerax.VideoCapture; import org.thoughtcrime.securesms.mediasend.camerax.VideoCapture;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor; import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.video.VideoUtil; import org.thoughtcrime.securesms.video.VideoUtil;
import java.io.FileDescriptor; 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 String VIDEO_DEBUG_LABEL = "video-capture";
private static final long VIDEO_SIZE = 10 * 1024 * 1024; private static final long VIDEO_SIZE = 10 * 1024 * 1024;
private final @NonNull Fragment fragment;
private final @NonNull CameraXView camera; private final @NonNull CameraXView camera;
private final @NonNull Callback callback; private final @NonNull Callback callback;
private final @NonNull MemoryFileDescriptor memoryFileDescriptor; private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
private boolean isRecording;
private ValueAnimator cameraMetricsAnimator;
private final ValueAnimator updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f) private final ValueAnimator updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f)
.setDuration(VideoUtil.VIDEO_MAX_LENGTH_S * 1000); .setDuration(VideoUtil.VIDEO_MAX_LENGTH_S * 1000);
@ -55,14 +64,17 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
@Nullable Throwable cause) @Nullable Throwable cause)
{ {
callback.onVideoError(cause); callback.onVideoError(cause);
Util.runOnMain(() -> resetCameraSizing());
} }
}; };
CameraXVideoCaptureHelper(@NonNull CameraButtonView captureButton, CameraXVideoCaptureHelper(@NonNull Fragment fragment,
@NonNull CameraButtonView captureButton,
@NonNull CameraXView camera, @NonNull CameraXView camera,
@NonNull MemoryFileDescriptor memoryFileDescriptor, @NonNull MemoryFileDescriptor memoryFileDescriptor,
@NonNull Callback callback) @NonNull Callback callback)
{ {
this.fragment = fragment;
this.camera = camera; this.camera = camera;
this.memoryFileDescriptor = memoryFileDescriptor; this.memoryFileDescriptor = memoryFileDescriptor;
this.callback = callback; this.callback = callback;
@ -72,7 +84,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
updateProgressAnimator.addListener(new AnimationCompleteListener() { updateProgressAnimator.addListener(new AnimationCompleteListener() {
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
onVideoCaptureComplete(); if (isRecording) onVideoCaptureComplete();
} }
}); });
} }
@ -81,22 +93,43 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
public void onVideoCaptureStarted() { public void onVideoCaptureStarted() {
Log.d(TAG, "onVideoCaptureStarted"); Log.d(TAG, "onVideoCaptureStarted");
this.camera.setZoomLevel(0f); if (canRecordAudio()) {
callback.onVideoRecordStarted(); isRecording = true;
shrinkCaptureArea(() -> { beginCameraRecording();
camera.startRecording(memoryFileDescriptor.getFileDescriptor(), videoSavedListener); } else {
updateProgressAnimator.start(); 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 screenSize = getScreenSize();
Size videoRecordingSize = VideoUtil.getVideoRecordingSize(); Size videoRecordingSize = VideoUtil.getVideoRecordingSize();
float scale = getSurfaceScaleForRecording(); float scale = getSurfaceScaleForRecording();
float targetWidthForAnimation = videoRecordingSize.getWidth() * scale; float targetWidthForAnimation = videoRecordingSize.getWidth() * scale;
float scaleX = targetWidthForAnimation / screenSize.getWidth(); float scaleX = targetWidthForAnimation / screenSize.getWidth();
final ValueAnimator cameraMetricsAnimator;
if (scaleX == 1f) { if (scaleX == 1f) {
float targetHeightForAnimation = videoRecordingSize.getHeight() * scale; float targetHeightForAnimation = videoRecordingSize.getHeight() * scale;
cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getHeight(), targetHeightForAnimation); cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getHeight(), targetHeightForAnimation);
@ -110,8 +143,9 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
cameraMetricsAnimator.addListener(new AnimationCompleteListener() { cameraMetricsAnimator.addListener(new AnimationCompleteListener() {
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
if (!isRecording) return;
scaleCameraViewToMatchRecordingSizeAndAspectRatio(); scaleCameraViewToMatchRecordingSizeAndAspectRatio();
onCaptureAreaShrank.run();
} }
}); });
cameraMetricsAnimator.addUpdateListener(animation -> { 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()); 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 @Override
public void onVideoCaptureComplete() { public void onVideoCaptureComplete() {
isRecording = false;
if (!canRecordAudio()) return;
Log.d(TAG, "onVideoCaptureComplete"); Log.d(TAG, "onVideoCaptureComplete");
updateProgressAnimator.cancel();
camera.stopRecording(); camera.stopRecording();
if (cameraMetricsAnimator != null && cameraMetricsAnimator.isRunning()) {
cameraMetricsAnimator.reverse();
}
updateProgressAnimator.cancel();
} }
@Override @Override

View file

@ -10,6 +10,7 @@ import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -36,7 +37,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.TransportOptions; import org.thoughtcrime.securesms.TransportOptions;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.SendButton; 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.Function3;
import org.thoughtcrime.securesms.util.IOFunction; import org.thoughtcrime.securesms.util.IOFunction;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -384,6 +385,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
finish(); finish();
} }
@Override
public void onVideoCaptureError() {
Vibrator vibrator = ServiceUtil.getVibrator(this);
vibrator.vibrate(50);
}
@Override @Override
public void onImageCaptured(@NonNull byte[] data, int width, int height) { public void onImageCaptured(@NonNull byte[] data, int width, int height) {
Log.i(TAG, "Camera image captured."); Log.i(TAG, "Camera image captured.");