Signal-Android/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java

479 wiersze
18 KiB
Java

package org.thoughtcrime.securesms.mediasend;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.Display;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageButton;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.cardview.widget.CardView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.view.ViewKt;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.signal.core.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.ByteArrayOutputStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* Camera capture implemented with the legacy camera API's. Should only be used if sdk < 21.
*/
public class Camera1Fragment extends LoggingFragment implements CameraFragment,
TextureView.SurfaceTextureListener,
Camera1Controller.EventListener
{
private static final String TAG = Log.tag(Camera1Fragment.class);
private TextureView cameraPreview;
private ViewGroup controlsContainer;
private ImageButton flipButton;
private View captureButton;
private Camera1Controller camera;
private Controller controller;
private OrderEnforcer<Stage> orderEnforcer;
private Camera1Controller.Properties properties;
private RotationListener rotationListener;
private Disposable rotationListenerDisposable;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private boolean isThumbAvailable;
private boolean isMediaSelected;
public static Camera1Fragment newInstance() {
return new Camera1Fragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActivity() instanceof Controller) {
this.controller = (Controller) getActivity();
} else if (getParentFragment() instanceof Controller) {
this.controller = (Controller) getParentFragment();
}
if (controller == null) {
throw new IllegalStateException("Parent must implement controller interface.");
}
WindowManager windowManager = ServiceUtil.getWindowManager(getActivity());
Display display = windowManager.getDefaultDisplay();
Point displaySize = new Point();
display.getSize(displaySize);
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.camera_fragment, container, false);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
rotationListener = new RotationListener(requireContext());
cameraPreview = view.findViewById(R.id.camera_preview);
controlsContainer = view.findViewById(R.id.camera_controls_container);
View cameraParent = view.findViewById(R.id.camera_preview_parent);
onOrientationChanged();
cameraPreview.setSurfaceTextureListener(this);
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
// Let's assume portrait for now, so 9:16
float aspectRatio = CameraFragment.getAspectRatioForOrientation(Configuration.ORIENTATION_PORTRAIT);
float width = right - left;
float height = Math.min((1f / aspectRatio) * width, bottom - top);
ViewGroup.LayoutParams params = cameraParent.getLayoutParams();
// If there's a mismatch...
if (params.height != (int) height) {
params.width = (int) width;
params.height = (int) height;
cameraParent.setLayoutParams(params);
}
});
}
@Override
public void onResume() {
super.onResume();
camera.initialize();
if (cameraPreview.isAvailable()) {
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
}
if (properties != null) {
orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
}
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
camera.linkSurface(cameraPreview.getSurfaceTexture());
});
rotationListenerDisposable = rotationListener.getObservable()
.distinctUntilChanged()
.filter(rotation -> rotation != RotationListener.Rotation.ROTATION_180)
.subscribe(rotation -> {
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
camera.setScreenRotation(rotation.getSurfaceRotation());
});
});
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
@Override
public void onPause() {
super.onPause();
rotationListenerDisposable.dispose();
camera.release();
orderEnforcer.reset();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mostRecentItemDisposable.dispose();
}
@Override
public void onDestroy() {
super.onDestroy();
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
@Override
public void fadeOutControls(@NonNull Runnable onEndAction) {
controlsContainer.setEnabled(false);
controlsContainer.animate()
.setInterpolator(MediaAnimations.getInterpolator())
.setDuration(250)
.alpha(0f)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
controlsContainer.setEnabled(true);
onEndAction.run();
}
});
}
@Override
public void fadeInControls() {
controlsContainer.setEnabled(false);
controlsContainer.animate()
.setInterpolator(MediaAnimations.getInterpolator())
.setDuration(250)
.alpha(1f)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
controlsContainer.setEnabled(true);
}
});
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.d(TAG, "onSurfaceTextureAvailable");
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
@Override
public void onPropertiesAvailable(@NonNull Camera1Controller.Properties properties) {
Log.d(TAG, "Got camera properties: " + properties);
this.properties = properties;
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
}
@Override
public void onCameraUnavailable() {
controller.onCameraError();
}
private void presentRecentItemThumbnail(@Nullable Media media) {
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
if (media != null) {
thumbnail.setVisibility(View.VISIBLE);
Glide.with(this)
.load(new DecryptableUri(media.getUri()))
.centerCrop()
.into(thumbnail);
} else {
thumbnail.setVisibility(View.GONE);
thumbnail.setImageResource(0);
}
isThumbAvailable = media != null;
updateGalleryVisibility();
}
@Override
public void presentHud(int selectedMediaCount) {
MediaCountIndicatorButton countButton = controlsContainer.findViewById(R.id.camera_review_button);
View cameraGalleryContainer = controlsContainer.findViewById(R.id.camera_gallery_button_background);
if (selectedMediaCount > 0) {
countButton.setVisibility(View.VISIBLE);
countButton.setCount(selectedMediaCount);
cameraGalleryContainer.setVisibility(View.GONE);
} else {
countButton.setVisibility(View.GONE);
cameraGalleryContainer.setVisibility(View.VISIBLE);
}
isMediaSelected = selectedMediaCount > 0;
updateGalleryVisibility();
}
private void updateGalleryVisibility() {
View cameraGalleryContainer = controlsContainer.findViewById(R.id.camera_gallery_button_background);
if (isMediaSelected || !isThumbAvailable) {
cameraGalleryContainer.setVisibility(View.GONE);
} else {
cameraGalleryContainer.setVisibility(View.VISIBLE);
}
}
private void initControls() {
flipButton = requireView().findViewById(R.id.camera_flip_button);
captureButton = requireView().findViewById(R.id.camera_capture_button);
View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
View countButton = requireView().findViewById(R.id.camera_review_button);
mostRecentItemDisposable.dispose();
mostRecentItemDisposable = controller.getMostRecentMediaItem()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(item -> presentRecentItemThumbnail(item.orElse(null)));
initializeViewFinderAndControlsPositioning();
captureButton.setOnClickListener(v -> {
captureButton.setEnabled(false);
onCaptureClicked();
});
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, () -> {
if (properties.getCameraCount() > 1) {
flipButton.setVisibility(properties.getCameraCount() > 1 ? View.VISIBLE : View.GONE);
flipButton.setOnClickListener(v -> {
int newCameraId = camera.flip();
TextSecurePreferences.setDirectCaptureCameraId(getContext(), newCameraId);
Animation animation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(200);
animation.setInterpolator(new DecelerateInterpolator());
flipButton.startAnimation(animation);
});
} else {
flipButton.setVisibility(View.GONE);
}
});
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
}
private void initializeViewFinderAndControlsPositioning() {
CardView cameraCard = requireView().findViewById(R.id.camera_preview_parent);
View controls = requireView().findViewById(R.id.camera_controls_container);
CameraDisplay cameraDisplay = CameraDisplay.getDisplay(requireActivity());
if (!cameraDisplay.getRoundViewFinderCorners()) {
cameraCard.setRadius(0f);
}
ViewUtil.setBottomMargin(controls, cameraDisplay.getCameraCaptureMarginBottom(getResources()));
if (cameraDisplay.getCameraViewportGravity() == CameraDisplay.CameraViewportGravity.CENTER) {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone((ConstraintLayout) requireView());
constraintSet.connect(R.id.camera_preview_parent, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
constraintSet.applyTo((ConstraintLayout) requireView());
} else {
ViewUtil.setBottomMargin(cameraCard, cameraDisplay.getCameraViewportMarginBottom());
}
}
private void onCaptureClicked() {
orderEnforcer.reset();
Stopwatch fastCaptureTimer = new Stopwatch("Capture");
camera.capture((jpegData, frontFacing) -> {
fastCaptureTimer.split("captured");
Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
: new CenterCrop();
GlideApp.with(this)
.asBitmap()
.load(jpegData)
.transform(transformation)
.override(cameraPreview.getWidth(), cameraPreview.getHeight())
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
fastCaptureTimer.split("transform");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
resource.compress(Bitmap.CompressFormat.JPEG, 80, stream);
fastCaptureTimer.split("compressed");
byte[] data = stream.toByteArray();
fastCaptureTimer.split("bytes");
fastCaptureTimer.stop(TAG);
controller.onImageCaptured(data, resource.getWidth(), resource.getHeight());
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
controller.onCameraError();
}
});
});
}
private PointF getScaleTransform(float viewWidth, float viewHeight, int cameraWidth, int cameraHeight) {
float camWidth = isPortrait() ? Math.min(cameraWidth, cameraHeight) : Math.max(cameraWidth, cameraHeight);
float camHeight = isPortrait() ? Math.max(cameraWidth, cameraHeight) : Math.min(cameraWidth, cameraHeight);
float scaleX = 1;
float scaleY = 1;
if ((camWidth / viewWidth) > (camHeight / viewHeight)) {
float targetWidth = viewHeight * (camWidth / camHeight);
scaleX = targetWidth / viewWidth;
} else {
float targetHeight = viewWidth * (camHeight / camWidth);
scaleY = targetHeight / viewHeight;
}
return new PointF(scaleX, scaleY);
}
private void onOrientationChanged() {
int layout = R.layout.camera_controls_portrait;
controlsContainer.removeAllViews();
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
initControls();
}
private void updatePreviewScale() {
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
Matrix matrix = new Matrix();
float camWidth = isPortrait() ? Math.min(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.max(cameraPreview.getWidth(), cameraPreview.getHeight());
float camHeight = isPortrait() ? Math.max(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.min(cameraPreview.getWidth(), cameraPreview.getHeight());
matrix.setScale(scale.x, scale.y);
matrix.postTranslate((camWidth - (camWidth * scale.x)) / 2, (camHeight - (camHeight * scale.y)) / 2);
cameraPreview.setTransform(matrix);
}
private boolean isPortrait() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}
private final GestureDetector.OnGestureListener flipGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
flipButton.performClick();
return true;
}
};
private enum Stage {
SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
}
}