Commit 78cd4841 authored by Adam Langley's avatar Adam Langley Committed by Commit Bot

Rotate camera preview correctly.

This change causes the camera preview in the QR-scanning dialog to be
rotated correctly. It also moves opening the camera (which is a slow
operation) off the UI thread, thus keeping StrictMode happy.

BUG=1002262

Change-Id: I9a7ae2ead10703fee38764a0c8f76597ef6212cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2097302
Commit-Queue: Adam Langley <agl@chromium.org>
Reviewed-by: default avatarMartin Kreichgauer <martinkr@google.com>
Auto-Submit: Adam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/master@{#749431}
parent e8d576a5
......@@ -6,28 +6,64 @@ package org.chromium.chrome.browser.webauth.authenticator;
import android.content.Context;
import android.hardware.Camera;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.io.IOException;
/**
* Provides a SurfaceView and adapts it for use as a camera preview target
* so that the current camera image can be displayed.
*
* TODO: locking and unlocking the screen seems to stop the camera preview because, on unlock,
* multiple of these Views end up getting created and only one wins the race to the camera.
*/
class CameraView extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "CameraView";
private final Camera.PreviewCallback mCallback;
private final Display mDisplay;
/**
* Holds a reference to the camera. Only referenced from the UI thread.
*/
private Camera mCamera;
/**
* Contains the number of degrees that the image from the selected camera
* will be rotated. Only referenced from the UI thread.
*/
private int mCameraRotation;
/**
* True if a background thread is trying to open the camera. Only referenced from the UI thread.
*/
private boolean mAmOpeningCamera;
/**
True if this View is currently detached. If this occurs while the camera is being opened
then it needs to immediately be closed again. Only referenced from the UI thread.
*/
private boolean mDetached;
private SurfaceHolder mHolder;
public CameraView(Context context, Camera.PreviewCallback callback) {
public CameraView(Context context, Display display, Camera.PreviewCallback callback) {
super(context);
mCallback = callback;
mDisplay = display;
}
/**
* Called to indicate that the callback that was passed to the constructor has finished
* processing and thus is free to receive another camera frame.
*/
public void rearmCallback() {
ThreadUtils.assertOnUiThread();
if (mCamera != null) {
mCamera.setOneShotPreviewCallback(mCallback);
}
......@@ -35,16 +71,19 @@ class CameraView extends SurfaceView implements SurfaceHolder.Callback {
@Override
protected void onAttachedToWindow() {
ThreadUtils.assertOnUiThread();
super.onAttachedToWindow();
mDetached = false;
getHolder().addCallback(this);
// TODO: Camera.open is slow and shouldn't be called on the UI
// thread.
mCamera = Camera.open();
}
@Override
protected void onDetachedFromWindow() {
ThreadUtils.assertOnUiThread();
super.onDetachedFromWindow();
mDetached = true;
getHolder().removeCallback(this);
if (mCamera != null) {
mCamera.release();
......@@ -52,47 +91,143 @@ class CameraView extends SurfaceView implements SurfaceHolder.Callback {
}
}
private void startCamera() {
private void openCamera() {
// We want to find the first, rear-facing camera. This is what
// Camera.open() gives us, but then we don't get the camera ID and we
// need that to get the rotation amount. Thus the need to iterate
// over the cameras to find the right one.
final int numCameras = Camera.getNumberOfCameras();
if (numCameras == 0) {
// TODO: indicate in UI when QR scanning fails.
return;
}
Camera.CameraInfo info = new Camera.CameraInfo();
boolean found = false;
int cameraId;
for (cameraId = 0; cameraId < numCameras; cameraId++) {
Camera.getCameraInfo(cameraId, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
found = true;
break;
}
}
if (!found) {
// No rear facing cameras available. Just use the first camera.
cameraId = 0;
Camera.getCameraInfo(cameraId, info);
}
Camera camera;
try {
mCamera.setPreviewDisplay(getHolder());
camera = Camera.open(cameraId);
} catch (RuntimeException e) {
Log.w(TAG, "Failed to open camera", e);
// TODO: indicate in UI when QR scanning fails.
return;
}
// This logic is based on
// https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
// But the sample code there appears to be wrong in practice.
int rotation;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
rotation = info.orientation;
} else {
rotation = (360 - info.orientation) % 360;
}
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> startCamera(camera, rotation));
}
private void startCamera(Camera camera, int cameraRotation) {
ThreadUtils.assertOnUiThread();
mAmOpeningCamera = false;
if (mDetached) {
// View was detached while the camera was being opened.
camera.release();
return;
}
mCamera = camera;
mCameraRotation = cameraRotation;
if (mHolder == null) {
// Surface was lost while the camera was being opened.
return;
}
try {
mCamera.setPreviewDisplay(mHolder);
// Use a one-shot callback so that callbacks don't happen faster
// they're processed.
mCamera.setOneShotPreviewCallback(mCallback);
Camera.Parameters parameters = mCamera.getParameters();
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
mCamera.setParameters(parameters);
// TODO: the display orientation should be configured as
// described in
// https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
int displayRotation = 0;
// getRotation returns the opposite of the rotation of the physical
// display. (I.e. it returns the rotation that needs to be applied
// in order to correct for the rotation of the screen.) Thus 90/270
// are swapped.
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0:
displayRotation = 0;
break;
case Surface.ROTATION_90:
displayRotation = 270;
break;
case Surface.ROTATION_180:
displayRotation = 180;
break;
case Surface.ROTATION_270:
displayRotation = 90;
break;
}
mCamera.setDisplayOrientation((mCameraRotation + displayRotation) % 360);
mCamera.startPreview();
} catch (IOException e) {
Log.w(TAG, "Exception while starting camera", e);
}
}
private void stopCamera() {
if (mCamera == null) {
return;
}
mCamera.setOneShotPreviewCallback(null);
mCamera.stopPreview();
}
/** SurfaceHolder.Callback implementation. */
@Override
public void surfaceCreated(SurfaceHolder holder) {
startCamera();
ThreadUtils.assertOnUiThread();
mHolder = holder;
if (mAmOpeningCamera) {
return;
}
if (mCamera == null) {
mAmOpeningCamera = true;
PostTask.postTask(TaskTraits.USER_VISIBLE_MAY_BLOCK, this::openCamera);
} else {
startCamera(mCamera, mCameraRotation);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopCamera();
ThreadUtils.assertOnUiThread();
mHolder = null;
if (mCamera != null) {
mCamera.setOneShotPreviewCallback(null);
mCamera.stopPreview();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
stopCamera();
startCamera();
surfaceDestroyed(holder);
surfaceCreated(holder);
}
}
......@@ -57,7 +57,8 @@ public class QRScanDialog extends DialogFragment implements Camera.PreviewCallba
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mCameraView = new CameraView(getContext(), this);
mCameraView = new CameraView(
getContext(), getActivity().getWindowManager().getDefaultDisplay(), this);
return mCameraView;
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment