mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-28 12:44:34 +01:00
Some phones, notably the Pixel 3, had some problems with scaling after taking photos. This fixes it by using the takePicture API instead of pulling the bitmap from the TextureView. Fixes #8292
310 lines
12 KiB
Java
310 lines
12 KiB
Java
package org.thoughtcrime.securesms.camera;
|
|
|
|
import android.annotation.SuppressLint;
|
|
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.hardware.Camera;
|
|
import android.os.Bundle;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.v4.app.Fragment;
|
|
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.AnimationUtils;
|
|
import android.widget.Button;
|
|
import android.widget.ImageButton;
|
|
|
|
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.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener,
|
|
Camera1Controller.EventListener
|
|
{
|
|
|
|
private static final String TAG = Camera1Fragment.class.getSimpleName();
|
|
|
|
private TextureView cameraPreview;
|
|
private ViewGroup controlsContainer;
|
|
private ImageButton flipButton;
|
|
private Button captureButton;
|
|
private Camera1Controller camera;
|
|
private Controller controller;
|
|
private OrderEnforcer<Stage> orderEnforcer;
|
|
private Camera1Controller.Properties properties;
|
|
|
|
public static Camera1Fragment newInstance() {
|
|
return new Camera1Fragment();
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
if (!(getActivity() instanceof Controller)) {
|
|
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
|
}
|
|
|
|
WindowManager windowManager = ServiceUtil.getWindowManager(getActivity());
|
|
Display display = windowManager.getDefaultDisplay();
|
|
Point displaySize = new Point();
|
|
|
|
display.getSize(displaySize);
|
|
|
|
controller = (Controller) getActivity();
|
|
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);
|
|
|
|
cameraPreview = view.findViewById(R.id.camera_preview);
|
|
controlsContainer = view.findViewById(R.id.camera_controls_container);
|
|
|
|
onOrientationChanged(getResources().getConfiguration().orientation);
|
|
|
|
cameraPreview.setSurfaceTextureListener(this);
|
|
|
|
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
|
|
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
camera.initialize();
|
|
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
|
|
camera.linkSurface(cameraPreview.getSurfaceTexture());
|
|
camera.setScreenRotation(controller.getDisplayRotation());
|
|
});
|
|
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
super.onPause();
|
|
camera.release();
|
|
}
|
|
|
|
@Override
|
|
public void onConfigurationChanged(Configuration newConfig) {
|
|
super.onConfigurationChanged(newConfig);
|
|
onOrientationChanged(newConfig.orientation);
|
|
}
|
|
|
|
@Override
|
|
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
|
|
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
|
|
}
|
|
|
|
@Override
|
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
|
|
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> camera.setScreenRotation(controller.getDisplayRotation()));
|
|
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();
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
private void initControls() {
|
|
flipButton = getView().findViewById(R.id.camera_flip_button);
|
|
captureButton = getView().findViewById(R.id.camera_capture_button);
|
|
|
|
captureButton.setOnTouchListener((v, event) -> {
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
Animation shrinkAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_shrink);
|
|
shrinkAnimation.setFillAfter(true);
|
|
shrinkAnimation.setFillEnabled(true);
|
|
captureButton.startAnimation(shrinkAnimation);
|
|
onCaptureClicked();
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
case MotionEvent.ACTION_OUTSIDE:
|
|
Animation growAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_grow);
|
|
growAnimation.setFillAfter(true);
|
|
growAnimation.setFillEnabled(true);
|
|
captureButton.startAnimation(growAnimation);
|
|
captureButton.setEnabled(false);
|
|
break;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, () -> {
|
|
if (properties.getCameraCount() > 1) {
|
|
flipButton.setVisibility(properties.getCameraCount() > 1 ? View.VISIBLE : View.GONE);
|
|
flipButton.setImageResource(TextSecurePreferences.getDirectCaptureCameraId(getContext()) == Camera.CameraInfo.CAMERA_FACING_BACK ? R.drawable.ic_camera_front
|
|
: R.drawable.ic_camera_rear);
|
|
flipButton.setOnClickListener(v -> {
|
|
int newCameraId = camera.flip();
|
|
flipButton.setImageResource(newCameraId == Camera.CameraInfo.CAMERA_FACING_BACK ? R.drawable.ic_camera_front
|
|
: R.drawable.ic_camera_rear);
|
|
|
|
TextSecurePreferences.setDirectCaptureCameraId(getContext(), newCameraId);
|
|
});
|
|
} else {
|
|
flipButton.setVisibility(View.GONE);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
@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 orientation) {
|
|
int layout = orientation == Configuration.ORIENTATION_PORTRAIT ? R.layout.camera_controls_portrait
|
|
: R.layout.camera_controls_landscape;
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
public interface Controller {
|
|
void onCameraError();
|
|
void onImageCaptured(@NonNull byte[] data);
|
|
int getDisplayRotation();
|
|
}
|
|
|
|
private enum Stage {
|
|
SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
|
|
}
|
|
}
|