Fix camera scaling issues on some phones.

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
This commit is contained in:
Greyson Parrelli 2018-10-22 01:06:40 -07:00
parent 76054a9e33
commit 91db26437d
3 changed files with 160 additions and 60 deletions

View File

@ -7,7 +7,6 @@ import android.view.Surface;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@ -16,6 +15,9 @@ public class Camera1Controller {
private static final String TAG = Camera1Controller.class.getSimpleName(); private static final String TAG = Camera1Controller.class.getSimpleName();
private final int screenWidth;
private final int screenHeight;
private Camera camera; private Camera camera;
private int cameraId; private int cameraId;
private OrderEnforcer<Stage> enforcer; private OrderEnforcer<Stage> enforcer;
@ -23,10 +25,12 @@ public class Camera1Controller {
private SurfaceTexture previewSurface; private SurfaceTexture previewSurface;
private int screenRotation; private int screenRotation;
public Camera1Controller(int preferredDirection, @NonNull EventListener eventListener) { public Camera1Controller(int preferredDirection, int screenWidth, int screenHeight, @NonNull EventListener eventListener) {
this.eventListener = eventListener; this.eventListener = eventListener;
this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED); this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED);
this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK; this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
} }
public void initialize() { public void initialize() {
@ -50,10 +54,17 @@ public class Camera1Controller {
} }
Camera.Parameters params = camera.getParameters(); Camera.Parameters params = camera.getParameters();
Camera.Size maxSize = getMaxSupportedPreviewSize(camera); Camera.Size previewSize = getClosestSize(camera.getParameters().getSupportedPreviewSizes(), screenWidth, screenHeight);
Camera.Size pictureSize = getClosestSize(camera.getParameters().getSupportedPictureSizes(), screenWidth, screenHeight);
final List<String> focusModes = params.getSupportedFocusModes(); final List<String> focusModes = params.getSupportedFocusModes();
params.setPreviewSize(maxSize.width, maxSize.height); Log.d(TAG, "Preview size: " + previewSize.width + "x" + previewSize.height + " Picture size: " + pictureSize.width + "x" + pictureSize.height);
params.setPreviewSize(previewSize.width, previewSize.height);
params.setPictureSize(pictureSize.width, pictureSize.height);
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
params.setColorEffect(Camera.Parameters.EFFECT_NONE);
params.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
@ -61,6 +72,7 @@ public class Camera1Controller {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
} }
camera.setParameters(params); camera.setParameters(params);
enforcer.markCompleted(Stage.INITIALIZED); enforcer.markCompleted(Stage.INITIALIZED);
@ -96,6 +108,14 @@ public class Camera1Controller {
}); });
} }
public void capture(@NonNull CaptureCallback callback) {
enforcer.run(Stage.PREVIEW_STARTED, () -> {
camera.takePicture(null, null, null, (data, camera) -> {
callback.onCaptureAvailable(data, cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
});
});
}
public int flip() { public int flip() {
Log.d(TAG, "flip()"); Log.d(TAG, "flip()");
SurfaceTexture surfaceTexture = previewSurface; SurfaceTexture surfaceTexture = previewSurface;
@ -115,13 +135,15 @@ public class Camera1Controller {
Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing"); Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing");
this.screenRotation = screenRotation; this.screenRotation = screenRotation;
int rotation = getCameraRotationForScreen(screenRotation); int previewRotation = getPreviewRotation(screenRotation);
camera.setDisplayOrientation(rotation); int outputRotation = getOutputRotation(screenRotation);
Log.d(TAG, "Set camera rotation to: " + rotation); Log.d(TAG, "Preview rotation: " + previewRotation + " Output rotation: " + outputRotation);
camera.setDisplayOrientation(previewRotation);
Camera.Parameters params = camera.getParameters(); Camera.Parameters params = camera.getParameters();
params.setRotation(rotation); params.setRotation(outputRotation);
camera.setParameters(params); camera.setParameters(params);
}); });
} }
@ -136,33 +158,58 @@ public class Camera1Controller {
return new Properties(Camera.getNumberOfCameras(), previewSize.width, previewSize.height); return new Properties(Camera.getNumberOfCameras(), previewSize.width, previewSize.height);
} }
private Camera.Size getMaxSupportedPreviewSize(Camera camera) { private Camera.Size getClosestSize(List<Camera.Size> sizes, int width, int height) {
List<Camera.Size> cameraSizes = camera.getParameters().getSupportedPreviewSizes(); Collections.sort(sizes, ASC_SIZE_COMPARATOR);
Collections.sort(cameraSizes, DESC_SIZE_COMPARATOR);
return cameraSizes.get(0); int i = 0;
while (i < sizes.size() && (sizes.get(i).width * sizes.get(i).height) < (width * height)) {
i++;
} }
private int getCameraRotationForScreen(int screenRotation) { return sizes.get(Math.min(i, sizes.size() - 1));
int degrees = 0;
switch (screenRotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
} }
private int getOutputRotation(int displayRotationCode) {
int degrees = convertRotationToDegrees(displayRotationCode);
Camera.CameraInfo info = new Camera.CameraInfo(); Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info); Camera.getCameraInfo(cameraId, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return (360 - ((info.orientation + degrees) % 360)) % 360; return (info.orientation + degrees) % 360;
} else { } else {
return (info.orientation - degrees + 360) % 360; return (info.orientation - degrees + 360) % 360;
} }
} }
private final Comparator<Camera.Size> DESC_SIZE_COMPARATOR = (o1, o2) -> Integer.compare(o2.width * o2.height, o1.width * o1.height); private int getPreviewRotation(int displayRotationCode) {
int degrees = convertRotationToDegrees(displayRotationCode);
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360;
} else {
result = (info.orientation - degrees + 360) % 360;
}
return result;
}
private int convertRotationToDegrees(int screenRotation) {
switch (screenRotation) {
case Surface.ROTATION_0: return 0;
case Surface.ROTATION_90: return 90;
case Surface.ROTATION_180: return 180;
case Surface.ROTATION_270: return 270;
}
return 0;
}
private final Comparator<Camera.Size> ASC_SIZE_COMPARATOR = (o1, o2) -> Integer.compare(o1.width * o1.height, o2.width * o2.height);
private enum Stage { private enum Stage {
INITIALIZED, PREVIEW_STARTED INITIALIZED, PREVIEW_STARTED
@ -194,7 +241,7 @@ public class Camera1Controller {
@Override @Override
public String toString() { public String toString() {
return "cameraCount: " + camera + " previewWidth: " + previewWidth + " previewHeight: " + previewHeight; return "cameraCount: " + cameraCount + " previewWidth: " + previewWidth + " previewHeight: " + previewHeight;
} }
} }
@ -202,4 +249,8 @@ public class Camera1Controller {
void onPropertiesAvailable(@NonNull Properties properties); void onPropertiesAvailable(@NonNull Properties properties);
void onCameraUnavailable(); void onCameraUnavailable();
} }
interface CaptureCallback {
void onCaptureAvailable(@NonNull byte[] jpegData, boolean frontFacing);
}
} }

View File

@ -1,35 +1,43 @@
package org.thoughtcrime.securesms.camera; package org.thoughtcrime.securesms.camera;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.graphics.drawable.Drawable;
import android.hardware.Camera; import android.hardware.Camera;
import android.media.MediaActionSound;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.Display;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.TextureView; import android.view.TextureView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; 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.R;
import org.thoughtcrime.securesms.logging.Log; 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.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -59,8 +67,14 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
throw new IllegalStateException("Parent activity must implement the Controller interface."); 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(); controller = (Controller) getActivity();
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), this); camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE); orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
} }
@ -190,43 +204,41 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
private void onCaptureClicked() { private void onCaptureClicked() {
orderEnforcer.reset(); orderEnforcer.reset();
Stopwatch fastCaptureTimer = new Stopwatch("Fast Capture"); Stopwatch fastCaptureTimer = new Stopwatch("Capture");
Bitmap preview = cameraPreview.getBitmap(); camera.capture((jpegData, frontFacing) -> {
fastCaptureTimer.split("captured"); fastCaptureTimer.split("captured");
LifecycleBoundTask.run(getLifecycle(), () -> { Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
Bitmap full = preview; : new CenterCrop();
if (Build.VERSION.SDK_INT < 28) {
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
Matrix matrix = new Matrix();
matrix.setScale(scale.x, scale.y); GlideApp.with(this)
.asBitmap()
int adjWidth = (int) (cameraPreview.getWidth() / scale.x); .load(jpegData)
int adjHeight = (int) (cameraPreview.getHeight() / scale.y); .transform(transformation)
.override(cameraPreview.getWidth(), cameraPreview.getHeight())
full = Bitmap.createBitmap(preview, 0, 0, adjWidth, adjHeight, matrix, true); .into(new SimpleTarget<Bitmap>() {
} @Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
fastCaptureTimer.split("transformed"); fastCaptureTimer.split("transform");
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
full.compress(Bitmap.CompressFormat.JPEG, 80, stream); resource.compress(Bitmap.CompressFormat.JPEG, 80, stream);
fastCaptureTimer.split("compressed"); fastCaptureTimer.split("compressed");
byte[] data = stream.toByteArray(); byte[] data = stream.toByteArray();
fastCaptureTimer.split("bytes"); fastCaptureTimer.split("bytes");
fastCaptureTimer.stop(TAG); fastCaptureTimer.stop(TAG);
return data;
}, data -> {
if (data != null) {
controller.onImageCaptured(data); controller.onImageCaptured(data);
} else { }
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
controller.onCameraError(); controller.onCameraError();
} }
}); });
});
} }
private PointF getScaleTransform(float viewWidth, float viewHeight, int cameraWidth, int cameraHeight) { private PointF getScaleTransform(float viewWidth, float viewHeight, int cameraWidth, int cameraHeight) {
@ -260,7 +272,11 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight()); PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
Matrix matrix = new Matrix(); 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.setScale(scale.x, scale.y);
matrix.postTranslate((camWidth - (camWidth * scale.x)) / 2, (camHeight - (camHeight * scale.y)) / 2);
cameraPreview.setTransform(matrix); cameraPreview.setTransform(matrix);
} }

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.camera;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import java.security.MessageDigest;
public class FlipTransformation extends BitmapTransformation {
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
Bitmap output = pool.get(toTransform.getWidth(), toTransform.getHeight(), toTransform.getConfig());
Canvas canvas = new Canvas(output);
Matrix matrix = new Matrix();
matrix.setScale(-1, 1);
matrix.postTranslate(toTransform.getWidth(), 0);
canvas.drawBitmap(toTransform, matrix, null);
return output;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(FlipTransformation.class.getSimpleName().getBytes());
}
}