That fuck shit the fascists are using
1package org.tm.archive.mediasend;
2
3import android.animation.Animator;
4import android.annotation.SuppressLint;
5import android.content.pm.ActivityInfo;
6import android.content.res.Configuration;
7import android.graphics.Bitmap;
8import android.graphics.Color;
9import android.graphics.Matrix;
10import android.graphics.Point;
11import android.graphics.PointF;
12import android.graphics.SurfaceTexture;
13import android.graphics.drawable.Drawable;
14import android.hardware.Camera;
15import android.os.Bundle;
16import android.view.Display;
17import android.view.GestureDetector;
18import android.view.LayoutInflater;
19import android.view.MotionEvent;
20import android.view.TextureView;
21import android.view.View;
22import android.view.ViewGroup;
23import android.view.WindowManager;
24import android.view.animation.Animation;
25import android.view.animation.DecelerateInterpolator;
26import android.view.animation.RotateAnimation;
27import android.widget.ImageButton;
28import android.widget.ImageView;
29
30import androidx.annotation.NonNull;
31import androidx.annotation.Nullable;
32import androidx.constraintlayout.widget.ConstraintLayout;
33import androidx.constraintlayout.widget.ConstraintSet;
34
35import com.bumptech.glide.Glide;
36import com.bumptech.glide.load.MultiTransformation;
37import com.bumptech.glide.load.Transformation;
38import com.bumptech.glide.load.resource.bitmap.CenterCrop;
39import com.bumptech.glide.request.target.SimpleTarget;
40import com.bumptech.glide.request.transition.Transition;
41import com.google.android.material.card.MaterialCardView;
42
43import org.signal.core.util.Stopwatch;
44import org.signal.core.util.logging.Log;
45import org.tm.archive.LoggingFragment;
46import org.tm.archive.R;
47import org.tm.archive.animation.AnimationCompleteListener;
48import org.tm.archive.mediasend.camerax.CameraXModelBlocklist;
49import org.tm.archive.mediasend.v2.MediaAnimations;
50import org.tm.archive.mediasend.v2.MediaCountIndicatorButton;
51import org.tm.archive.mms.DecryptableStreamUriLoader.DecryptableUri;
52import org.tm.archive.util.ServiceUtil;
53import org.tm.archive.util.TextSecurePreferences;
54import org.tm.archive.util.ViewUtil;
55
56import java.io.ByteArrayOutputStream;
57
58import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
59import io.reactivex.rxjava3.disposables.Disposable;
60
61/**
62 * Camera capture implemented with the legacy camera API's. Should only be used if a device is on the {@link CameraXModelBlocklist}.
63 */
64public class Camera1Fragment extends LoggingFragment implements CameraFragment,
65 TextureView.SurfaceTextureListener,
66 Camera1Controller.EventListener
67{
68
69 private static final String TAG = Log.tag(Camera1Fragment.class);
70
71 private TextureView cameraPreview;
72 private ViewGroup controlsContainer;
73 private ImageButton flipButton;
74 private View captureButton;
75 private Camera1Controller camera;
76 private Controller controller;
77 private OrderEnforcer<Stage> orderEnforcer;
78 private Camera1Controller.Properties properties;
79 private RotationListener rotationListener;
80 private Disposable rotationListenerDisposable;
81 private Disposable mostRecentItemDisposable = Disposable.disposed();
82 private CameraScreenBrightnessController cameraScreenBrightnessController;
83 private boolean isMediaSelected;
84
85 public static Camera1Fragment newInstance() {
86 return new Camera1Fragment();
87 }
88
89 @Override
90 public void onCreate(@Nullable Bundle savedInstanceState) {
91 super.onCreate(savedInstanceState);
92
93 if (getActivity() instanceof Controller) {
94 this.controller = (Controller) getActivity();
95 } else if (getParentFragment() instanceof Controller) {
96 this.controller = (Controller) getParentFragment();
97 }
98
99 if (controller == null) {
100 throw new IllegalStateException("Parent must implement controller interface.");
101 }
102
103 WindowManager windowManager = ServiceUtil.getWindowManager(getActivity());
104 Display display = windowManager.getDefaultDisplay();
105 Point displaySize = new Point();
106
107 display.getSize(displaySize);
108
109 camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
110 orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
111 }
112
113 @Nullable
114 @Override
115 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
116 return inflater.inflate(R.layout.camera_fragment, container, false);
117 }
118
119 @SuppressLint("ClickableViewAccessibility")
120 @Override
121 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
122 super.onViewCreated(view, savedInstanceState);
123 requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
124 cameraScreenBrightnessController = new CameraScreenBrightnessController(requireActivity().getWindow(), new CameraStateProvider(camera));
125 getViewLifecycleOwner().getLifecycle().addObserver(cameraScreenBrightnessController);
126
127 rotationListener = new RotationListener(requireContext());
128 cameraPreview = view.findViewById(R.id.camera_preview);
129 controlsContainer = view.findViewById(R.id.camera_controls_container);
130
131 View cameraParent = view.findViewById(R.id.camera_preview_parent);
132
133 onOrientationChanged();
134
135 cameraPreview.setSurfaceTextureListener(this);
136
137 GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
138 cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
139
140 view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
141 // Let's assume portrait for now, so 9:16
142 float aspectRatio = CameraFragment.getAspectRatioForOrientation(Configuration.ORIENTATION_PORTRAIT);
143 float width = right - left;
144 float height = Math.min((1f / aspectRatio) * width, bottom - top);
145
146 ViewGroup.LayoutParams params = cameraParent.getLayoutParams();
147
148 // If there's a mismatch...
149 if (params.height != (int) height) {
150 params.width = (int) width;
151 params.height = (int) height;
152
153 cameraParent.setLayoutParams(params);
154 }
155 });
156 }
157
158 @Override
159 public void onResume() {
160 super.onResume();
161 camera.initialize();
162
163 if (cameraPreview.isAvailable()) {
164 orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
165 }
166
167 if (properties != null) {
168 orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
169 }
170
171 orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
172 camera.linkSurface(cameraPreview.getSurfaceTexture());
173 });
174
175 rotationListenerDisposable = rotationListener.getObservable()
176 .distinctUntilChanged()
177 .filter(rotation -> rotation != RotationListener.Rotation.ROTATION_180)
178 .subscribe(rotation -> {
179 orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
180 camera.setScreenRotation(rotation.getSurfaceRotation());
181 });
182 });
183
184 orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
185 requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
186 }
187
188 @Override
189 public void onPause() {
190 super.onPause();
191 rotationListenerDisposable.dispose();
192 camera.release();
193 orderEnforcer.reset();
194 }
195
196 @Override
197 public void onDestroyView() {
198 super.onDestroyView();
199 mostRecentItemDisposable.dispose();
200 }
201
202 @Override
203 public void onDestroy() {
204 super.onDestroy();
205 requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
206 }
207
208 @Override
209 public void fadeOutControls(@NonNull Runnable onEndAction) {
210 controlsContainer.setEnabled(false);
211 controlsContainer.animate()
212 .setInterpolator(MediaAnimations.getInterpolator())
213 .setDuration(250)
214 .alpha(0f)
215 .setListener(new AnimationCompleteListener() {
216 @Override
217 public void onAnimationEnd(Animator animation) {
218 controlsContainer.setEnabled(true);
219 onEndAction.run();
220 }
221 });
222 }
223
224 @Override
225 public void fadeInControls() {
226 controlsContainer.setEnabled(false);
227 controlsContainer.animate()
228 .setInterpolator(MediaAnimations.getInterpolator())
229 .setDuration(250)
230 .alpha(1f)
231 .setListener(new AnimationCompleteListener() {
232 @Override
233 public void onAnimationEnd(Animator animation) {
234 controlsContainer.setEnabled(true);
235 }
236 });
237 }
238
239 @Override
240 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
241 Log.d(TAG, "onSurfaceTextureAvailable");
242 orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
243 }
244
245 @Override
246 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
247 orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
248 }
249
250 @Override
251 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
252 return false;
253 }
254
255 @Override
256 public void onSurfaceTextureUpdated(SurfaceTexture surface) {
257 }
258
259 @Override
260 public void onPropertiesAvailable(@NonNull Camera1Controller.Properties properties) {
261 Log.d(TAG, "Got camera properties: " + properties);
262 this.properties = properties;
263 orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
264 orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
265 }
266
267 @Override
268 public void onCameraUnavailable() {
269 controller.onCameraError();
270 }
271
272 private void presentRecentItemThumbnail(@Nullable Media media) {
273 View thumbBackground = controlsContainer.findViewById(R.id.camera_gallery_button_background);
274 ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
275
276 if (media != null) {
277 thumbBackground.setBackgroundResource(R.drawable.circle_tintable);
278 thumbnail.clearColorFilter();
279 thumbnail.setScaleType(ImageView.ScaleType.FIT_CENTER);
280 Glide.with(this)
281 .load(new DecryptableUri(media.getUri()))
282 .centerCrop()
283 .into(thumbnail);
284 } else {
285 thumbBackground.setBackgroundResource(R.drawable.media_selection_camera_switch_background);
286 thumbnail.setImageResource(R.drawable.symbol_album_tilt_24);
287 thumbnail.setColorFilter(Color.WHITE);
288 thumbnail.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
289 }
290 }
291
292 @Override
293 public void presentHud(int selectedMediaCount) {
294 MediaCountIndicatorButton countButton = controlsContainer.findViewById(R.id.camera_review_button);
295 View cameraGalleryContainer = controlsContainer.findViewById(R.id.camera_gallery_button_background);
296
297 if (selectedMediaCount > 0) {
298 countButton.setVisibility(View.VISIBLE);
299 countButton.setCount(selectedMediaCount);
300 cameraGalleryContainer.setVisibility(View.GONE);
301 } else {
302 countButton.setVisibility(View.GONE);
303 cameraGalleryContainer.setVisibility(View.VISIBLE);
304 }
305
306 isMediaSelected = selectedMediaCount > 0;
307 updateGalleryVisibility();
308 }
309
310 private void updateGalleryVisibility() {
311 View cameraGalleryContainer = controlsContainer.findViewById(R.id.camera_gallery_button_background);
312
313 if (isMediaSelected) {
314 cameraGalleryContainer.setVisibility(View.GONE);
315 } else {
316 cameraGalleryContainer.setVisibility(View.VISIBLE);
317 }
318 }
319
320 private void initControls() {
321 flipButton = requireView().findViewById(R.id.camera_flip_button);
322 captureButton = requireView().findViewById(R.id.camera_capture_button);
323
324 View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
325 View countButton = requireView().findViewById(R.id.camera_review_button);
326
327 mostRecentItemDisposable.dispose();
328 mostRecentItemDisposable = controller.getMostRecentMediaItem()
329 .observeOn(AndroidSchedulers.mainThread())
330 .subscribe(item -> presentRecentItemThumbnail(item.orElse(null)));
331
332 initializeViewFinderAndControlsPositioning();
333
334 captureButton.setOnClickListener(v -> {
335 captureButton.setEnabled(false);
336 onCaptureClicked();
337 });
338
339 captureButton.setOnLongClickListener(unused -> {
340 CameraFragment.toastVideoRecordingNotAvailable(requireContext());
341 return true;
342 });
343
344 orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, () -> {
345 if (properties.getCameraCount() > 1) {
346 flipButton.setVisibility(properties.getCameraCount() > 1 ? View.VISIBLE : View.GONE);
347 flipButton.setOnClickListener(v -> {
348 int newCameraId = camera.flip();
349 TextSecurePreferences.setDirectCaptureCameraId(getContext(), newCameraId);
350
351 Animation animation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
352 animation.setDuration(200);
353 animation.setInterpolator(new DecelerateInterpolator());
354 flipButton.startAnimation(animation);
355 cameraScreenBrightnessController.onCameraDirectionChanged(newCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
356 });
357 } else {
358 flipButton.setVisibility(View.GONE);
359 }
360 });
361
362 galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
363 countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
364 }
365
366 private void initializeViewFinderAndControlsPositioning() {
367 MaterialCardView cameraCard = requireView().findViewById(R.id.camera_preview_parent);
368 View controls = requireView().findViewById(R.id.camera_controls_container);
369 CameraDisplay cameraDisplay = CameraDisplay.getDisplay(requireActivity());
370
371 if (!cameraDisplay.getRoundViewFinderCorners()) {
372 cameraCard.setRadius(0f);
373 }
374
375 ViewUtil.setBottomMargin(controls, cameraDisplay.getCameraCaptureMarginBottom(getResources()));
376
377 if (cameraDisplay.getCameraViewportGravity() == CameraDisplay.CameraViewportGravity.CENTER) {
378 ConstraintSet constraintSet = new ConstraintSet();
379 constraintSet.clone((ConstraintLayout) requireView());
380 constraintSet.connect(R.id.camera_preview_parent, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
381 constraintSet.applyTo((ConstraintLayout) requireView());
382 } else {
383 ViewUtil.setBottomMargin(cameraCard, cameraDisplay.getCameraViewportMarginBottom());
384 }
385 }
386
387 private void onCaptureClicked() {
388 orderEnforcer.reset();
389
390 Stopwatch fastCaptureTimer = new Stopwatch("Capture");
391
392 camera.capture((jpegData, frontFacing) -> {
393 fastCaptureTimer.split("captured");
394
395 Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
396 : new CenterCrop();
397
398 Glide.with(this)
399 .asBitmap()
400 .load(jpegData)
401 .transform(transformation)
402 .override(cameraPreview.getWidth(), cameraPreview.getHeight())
403 .into(new SimpleTarget<Bitmap>() {
404 @Override
405 public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
406 fastCaptureTimer.split("transform");
407
408 ByteArrayOutputStream stream = new ByteArrayOutputStream();
409 resource.compress(Bitmap.CompressFormat.JPEG, 80, stream);
410 fastCaptureTimer.split("compressed");
411
412 byte[] data = stream.toByteArray();
413 fastCaptureTimer.split("bytes");
414 fastCaptureTimer.stop(TAG);
415
416 controller.onImageCaptured(data, resource.getWidth(), resource.getHeight());
417 }
418
419 @Override
420 public void onLoadFailed(@Nullable Drawable errorDrawable) {
421 controller.onCameraError();
422 }
423 });
424 });
425 }
426
427 private PointF getScaleTransform(float viewWidth, float viewHeight, int cameraWidth, int cameraHeight) {
428 float camWidth = isPortrait() ? Math.min(cameraWidth, cameraHeight) : Math.max(cameraWidth, cameraHeight);
429 float camHeight = isPortrait() ? Math.max(cameraWidth, cameraHeight) : Math.min(cameraWidth, cameraHeight);
430
431 float scaleX = 1;
432 float scaleY = 1;
433
434 if ((camWidth / viewWidth) > (camHeight / viewHeight)) {
435 float targetWidth = viewHeight * (camWidth / camHeight);
436 scaleX = targetWidth / viewWidth;
437 } else {
438 float targetHeight = viewWidth * (camHeight / camWidth);
439 scaleY = targetHeight / viewHeight;
440 }
441
442 return new PointF(scaleX, scaleY);
443 }
444
445 private void onOrientationChanged() {
446 int layout = R.layout.camera_controls_portrait;
447
448 controlsContainer.removeAllViews();
449 controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
450 initControls();
451 }
452
453 private void updatePreviewScale() {
454 PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
455 Matrix matrix = new Matrix();
456
457 float camWidth = isPortrait() ? Math.min(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.max(cameraPreview.getWidth(), cameraPreview.getHeight());
458 float camHeight = isPortrait() ? Math.max(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.min(cameraPreview.getWidth(), cameraPreview.getHeight());
459
460 matrix.setScale(scale.x, scale.y);
461 matrix.postTranslate((camWidth - (camWidth * scale.x)) / 2, (camHeight - (camHeight * scale.y)) / 2);
462 cameraPreview.setTransform(matrix);
463 }
464
465 private boolean isPortrait() {
466 return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
467 }
468
469 private final GestureDetector.OnGestureListener flipGestureListener = new GestureDetector.SimpleOnGestureListener() {
470 @Override
471 public boolean onDown(MotionEvent e) {
472 return true;
473 }
474
475 @Override
476 public boolean onDoubleTap(MotionEvent e) {
477 flipButton.performClick();
478 return true;
479 }
480 };
481
482 private enum Stage {
483 SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
484 }
485
486 private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
487
488 private final Camera1Controller camera1Controller;
489
490 private CameraStateProvider(Camera1Controller camera1Controller) {
491 this.camera1Controller = camera1Controller;
492 }
493
494 @Override
495 public boolean isFrontFacingCameraSelected() {
496 return camera1Controller.isCameraFacingFront();
497 }
498
499 @Override
500 public boolean isFlashEnabled() {
501 return false;
502 }
503 }
504}