That fuck shit the fascists are using
1package org.tm.archive.mediasend;
2
3import android.Manifest;
4import android.animation.ValueAnimator;
5import android.annotation.SuppressLint;
6import android.content.Context;
7import android.util.DisplayMetrics;
8import android.util.Size;
9import android.view.ViewGroup;
10import android.view.animation.LinearInterpolator;
11import android.widget.Toast;
12
13import androidx.annotation.NonNull;
14import androidx.annotation.Nullable;
15import androidx.annotation.RequiresApi;
16import androidx.camera.core.ZoomState;
17import androidx.camera.video.FileDescriptorOutputOptions;
18import androidx.camera.video.Recording;
19import androidx.camera.video.VideoRecordEvent;
20import androidx.camera.view.PreviewView;
21import androidx.camera.view.video.AudioConfig;
22import androidx.core.content.ContextCompat;
23import androidx.core.util.Consumer;
24import androidx.fragment.app.Fragment;
25
26import org.signal.core.util.logging.Log;
27import org.tm.archive.R;
28import org.tm.archive.mediasend.camerax.CameraXController;
29import org.tm.archive.mediasend.camerax.CameraXModePolicy;
30import org.tm.archive.permissions.Permissions;
31import org.tm.archive.util.ContextUtil;
32import org.tm.archive.util.Debouncer;
33import org.tm.archive.util.FeatureFlags;
34import org.tm.archive.util.MemoryFileDescriptor;
35import org.tm.archive.video.VideoUtil;
36
37import java.io.FileDescriptor;
38import java.io.IOException;
39import java.util.Objects;
40import java.util.concurrent.TimeUnit;
41
42@RequiresApi(26)
43class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener {
44
45 private static final String TAG = Log.tag(CameraXVideoCaptureHelper.class);
46 private static final String VIDEO_DEBUG_LABEL = "video-capture";
47 private static final long VIDEO_SIZE = 10 * 1024 * 1024;
48
49 private final @NonNull Fragment fragment;
50 private final @NonNull PreviewView previewView;
51 private final @NonNull CameraXController cameraController;
52 private final @NonNull Callback callback;
53 private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
54 private final @NonNull ValueAnimator updateProgressAnimator;
55 private final @NonNull Debouncer debouncer;
56 private final @NonNull CameraXModePolicy cameraXModePolicy;
57
58 private ValueAnimator cameraMetricsAnimator;
59
60 private @Nullable Recording activeRecording = null;
61
62 private final Consumer<VideoRecordEvent> videoSavedListener = new Consumer<>() {
63 @Override
64 public void accept(VideoRecordEvent videoRecordEvent) {
65 Log.d(TAG, "Received recording event: " + videoRecordEvent.getClass().getSimpleName());
66
67 if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
68 VideoRecordEvent.Finalize event = (VideoRecordEvent.Finalize) videoRecordEvent;
69
70 if (event.hasError()) {
71 Log.w(TAG, "Hit an error while recording! Error code: " + event.getError(), event.getCause());
72 debouncer.clear();
73 callback.onVideoError(event.getCause());
74 } else {
75 try {
76 debouncer.clear();
77 cameraController.setZoomRatio(getDefaultVideoZoomRatio());
78 memoryFileDescriptor.seek(0);
79 callback.onVideoSaved(memoryFileDescriptor.getFileDescriptor());
80 } catch (IOException e) {
81 callback.onVideoError(e);
82 }
83 }
84 }
85 }
86 };
87
88 CameraXVideoCaptureHelper(@NonNull Fragment fragment,
89 @NonNull CameraButtonView captureButton,
90 @NonNull CameraXController cameraController,
91 @NonNull PreviewView previewView,
92 @NonNull MemoryFileDescriptor memoryFileDescriptor,
93 @NonNull CameraXModePolicy cameraXModePolicy,
94 int maxVideoDurationSec,
95 @NonNull Callback callback)
96 {
97 this.fragment = fragment;
98 this.cameraController = cameraController;
99 this.previewView = previewView;
100 this.memoryFileDescriptor = memoryFileDescriptor;
101 this.callback = callback;
102
103 float animationScale = ContextUtil.getAnimationScale(fragment.requireContext());
104 long baseDuration = TimeUnit.SECONDS.toMillis(maxVideoDurationSec);
105 long scaledDuration = Math.round(animationScale > 0f ? (baseDuration * (1f / animationScale)) : baseDuration);
106
107 this.updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(scaledDuration);
108 this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
109 this.cameraXModePolicy = cameraXModePolicy;
110
111 updateProgressAnimator.setInterpolator(new LinearInterpolator());
112 updateProgressAnimator.addUpdateListener(anim -> {
113 captureButton.setProgress(anim.getAnimatedFraction());
114 });
115 }
116
117 @Override
118 public void onVideoCaptureStarted() {
119 Log.d(TAG, "onVideoCaptureStarted");
120
121 if (canRecordAudio()) {
122 beginCameraRecording();
123 } else {
124 displayAudioRecordingPermissionsDialog();
125 }
126 }
127
128 private boolean canRecordAudio() {
129 return Permissions.hasAll(fragment.requireContext(), Manifest.permission.RECORD_AUDIO);
130 }
131
132 private void displayAudioRecordingPermissionsDialog() {
133 Permissions.with(fragment)
134 .request(Manifest.permission.RECORD_AUDIO)
135 .ifNecessary()
136 .withRationaleDialog(fragment.getString(R.string.ConversationActivity_enable_the_microphone_permission_to_capture_videos_with_sound), R.drawable.ic_mic_solid_24)
137 .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video))
138 .onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_recording_permissions_to_capture_video, Toast.LENGTH_LONG).show())
139 .execute();
140 }
141
142 @SuppressLint("RestrictedApi")
143 private void beginCameraRecording() {
144 cameraXModePolicy.setToVideo(cameraController);
145 this.cameraController.setZoomRatio(getDefaultVideoZoomRatio());
146 callback.onVideoRecordStarted();
147 shrinkCaptureArea();
148
149 FileDescriptorOutputOptions outputOptions = new FileDescriptorOutputOptions.Builder(memoryFileDescriptor.getParcelFileDescriptor()).build();
150 AudioConfig audioConfig = AudioConfig.create(true);
151
152 activeRecording = cameraController.startRecording(outputOptions, audioConfig, ContextCompat.getMainExecutor(fragment.requireContext()), videoSavedListener);
153
154 updateProgressAnimator.start();
155 debouncer.publish(this::onVideoCaptureComplete);
156 }
157
158 private void shrinkCaptureArea() {
159 Size screenSize = getScreenSize();
160 Size videoRecordingSize = VideoUtil.getVideoRecordingSize();
161 float scale = getSurfaceScaleForRecording();
162 float targetWidthForAnimation = videoRecordingSize.getWidth() * scale;
163 float scaleX = targetWidthForAnimation / screenSize.getWidth();
164
165 if (scaleX == 1f) {
166 float targetHeightForAnimation = videoRecordingSize.getHeight() * scale;
167
168 if (screenSize.getHeight() == targetHeightForAnimation) {
169 return;
170 }
171
172 cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getHeight(), targetHeightForAnimation);
173 } else {
174
175 if (screenSize.getWidth() == targetWidthForAnimation) {
176 return;
177 }
178
179 cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getWidth(), targetWidthForAnimation);
180 }
181
182 ViewGroup.LayoutParams params = previewView.getLayoutParams();
183 cameraMetricsAnimator.setInterpolator(new LinearInterpolator());
184 cameraMetricsAnimator.setDuration(200);
185 cameraMetricsAnimator.addUpdateListener(animation -> {
186 if (scaleX == 1f) {
187 params.height = Math.round((float) animation.getAnimatedValue());
188 } else {
189 params.width = Math.round((float) animation.getAnimatedValue());
190 }
191 previewView.setLayoutParams(params);
192 });
193 cameraMetricsAnimator.start();
194 }
195
196 private Size getScreenSize() {
197 DisplayMetrics metrics = previewView.getResources().getDisplayMetrics();
198 return new Size(metrics.widthPixels, metrics.heightPixels);
199 }
200
201 private float getSurfaceScaleForRecording() {
202 Size videoRecordingSize = VideoUtil.getVideoRecordingSize();
203 Size screenSize = getScreenSize();
204 return Math.min(screenSize.getHeight(), screenSize.getWidth()) / (float) Math.min(videoRecordingSize.getHeight(), videoRecordingSize.getWidth());
205 }
206
207 @Override
208 public void onVideoCaptureComplete() {
209 if (!canRecordAudio()) {
210 Log.w(TAG, "Can't record audio!");
211 return;
212 }
213
214 if (activeRecording == null) {
215 Log.w(TAG, "No active recording!");
216 return;
217 }
218
219 Log.d(TAG, "onVideoCaptureComplete");
220 activeRecording.close();
221 activeRecording = null;
222
223 if (cameraMetricsAnimator != null && cameraMetricsAnimator.isRunning()) {
224 cameraMetricsAnimator.reverse();
225 }
226
227 updateProgressAnimator.cancel();
228 debouncer.clear();
229 cameraXModePolicy.setToImage(cameraController);
230 }
231
232 @Override
233 public void onZoomIncremented(float increment) {
234 ZoomState zoomState = Objects.requireNonNull(cameraController.getZoomState().getValue());
235 float range = zoomState.getMaxZoomRatio() - getDefaultVideoZoomRatio();
236 cameraController.setZoomRatio((range * increment) + getDefaultVideoZoomRatio());
237 }
238
239 @Override
240 protected void finalize() throws Throwable {
241 if (activeRecording != null) {
242 Log.w(TAG, "Dangling recording left open in finalize()! Attempting to close.");
243 activeRecording.close();
244 }
245
246 super.finalize();
247 }
248
249 static MemoryFileDescriptor createFileDescriptor(@NonNull Context context) throws MemoryFileDescriptor.MemoryFileException {
250 return MemoryFileDescriptor.newMemoryFileDescriptor(
251 context,
252 VIDEO_DEBUG_LABEL,
253 VIDEO_SIZE,
254 true//**TM_SA**//
255 );
256 }
257
258 public float getDefaultVideoZoomRatio() {
259 if (FeatureFlags.startVideoRecordAt1x()) {
260 return 1f;
261 } else {
262 return Objects.requireNonNull(cameraController.getZoomState().getValue()).getMinZoomRatio();
263 }
264 }
265
266 interface Callback {
267 void onVideoRecordStarted();
268
269 void onVideoSaved(@NonNull FileDescriptor fd);
270
271 void onVideoError(@Nullable Throwable cause);
272 }
273}