package com.prey.barcodereader.ui.camera;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.os.Build;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresPermission;
import android.support.annotation.StringDef;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import com.google.android.gms.common.images.Size;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.Frame;
import com.prey.PreyLogger;
import java.io.IOException;
import java.lang.Thread.State;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings("deprecation")
public class CameraSource {
@SuppressLint("InlinedApi")
public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK;
@SuppressLint("InlinedApi")
public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT;
private static final int DUMMY_TEXTURE_NAME = 100;
private static final float ASPECT_RATIO_TOLERANCE = 0.01f;
@StringDef({
Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
Camera.Parameters.FOCUS_MODE_AUTO,
Camera.Parameters.FOCUS_MODE_EDOF,
Camera.Parameters.FOCUS_MODE_FIXED,
Camera.Parameters.FOCUS_MODE_INFINITY,
Camera.Parameters.FOCUS_MODE_MACRO
})
@Retention(RetentionPolicy.SOURCE)
private @interface FocusMode {}
@StringDef({
Camera.Parameters.FLASH_MODE_ON,
Camera.Parameters.FLASH_MODE_OFF,
Camera.Parameters.FLASH_MODE_AUTO,
Camera.Parameters.FLASH_MODE_RED_EYE,
Camera.Parameters.FLASH_MODE_TORCH
})
@Retention(RetentionPolicy.SOURCE)
private @interface FlashMode {}
private Context mContext;
private final Object mCameraLock = new Object();
private Camera mCamera;
private int mFacing = CAMERA_FACING_BACK;
private int mRotation;
private Size mPreviewSize;
private float mRequestedFps = 30.0f;
private int mRequestedPreviewWidth = 1024;
private int mRequestedPreviewHeight = 768;
private String mFocusMode = null;
private String mFlashMode = null;
private SurfaceView mDummySurfaceView;
private SurfaceTexture mDummySurfaceTexture;
private Thread mProcessingThread;
private FrameProcessingRunnable mFrameProcessor;
private Map<byte[], ByteBuffer> mBytesToByteBuffer = new HashMap<>();
public static class Builder {
private final Detector<?> mDetector;
private CameraSource mCameraSource = new CameraSource();
public Builder(Context context, Detector<?> detector) {
if (context == null) {
throw new IllegalArgumentException("No context supplied.");
}
if (detector == null) {
throw new IllegalArgumentException("No detector supplied.");
}
mDetector = detector;
mCameraSource.mContext = context;
}
public Builder setRequestedFps(float fps) {
if (fps <= 0) {
throw new IllegalArgumentException("Invalid fps: " + fps);
}
mCameraSource.mRequestedFps = fps;
return this;
}
public Builder setFocusMode(@FocusMode String mode) {
mCameraSource.mFocusMode = mode;
return this;
}
public Builder setFlashMode(@FlashMode String mode) {
mCameraSource.mFlashMode = mode;
return this;
}
public Builder setRequestedPreviewSize(int width, int height) {
final int MAX = 1000000;
if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) {
throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height);
}
mCameraSource.mRequestedPreviewWidth = width;
mCameraSource.mRequestedPreviewHeight = height;
return this;
}
public Builder setFacing(int facing) {
if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) {
throw new IllegalArgumentException("Invalid camera: " + facing);
}
mCameraSource.mFacing = facing;
return this;
}
public CameraSource build() {
mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector);
return mCameraSource;
}
}
public interface ShutterCallback {
void onShutter();
}
public interface PictureCallback {
void onPictureTaken(byte[] data);
}
public interface AutoFocusCallback {
void onAutoFocus(boolean success);
}
public interface AutoFocusMoveCallback {
void onAutoFocusMoving(boolean start);
}
public void release() {
synchronized (mCameraLock) {
stop();
mFrameProcessor.release();
}
}
@RequiresPermission(Manifest.permission.CAMERA)
public CameraSource start() throws IOException {
synchronized (mCameraLock) {
if (mCamera != null) {
return this;
}
mCamera = createCamera();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME);
mCamera.setPreviewTexture(mDummySurfaceTexture);
} else {
mDummySurfaceView = new SurfaceView(mContext);
mCamera.setPreviewDisplay(mDummySurfaceView.getHolder());
}
mCamera.startPreview();
mProcessingThread = new Thread(mFrameProcessor);
mFrameProcessor.setActive(true);
mProcessingThread.start();
}
return this;
}
@RequiresPermission(Manifest.permission.CAMERA)
public CameraSource start(SurfaceHolder surfaceHolder) throws IOException {
synchronized (mCameraLock) {
if (mCamera != null) {
return this;
}
mCamera = createCamera();
mCamera.setPreviewDisplay(surfaceHolder);
mCamera.startPreview();
mProcessingThread = new Thread(mFrameProcessor);
mFrameProcessor.setActive(true);
mProcessingThread.start();
}
return this;
}
public void stop() {
synchronized (mCameraLock) {
mFrameProcessor.setActive(false);
if (mProcessingThread != null) {
try {
mProcessingThread.join();
} catch (InterruptedException e) {
PreyLogger.e("Frame processing thread interrupted on release.",e);
}
mProcessingThread = null;
}
mBytesToByteBuffer.clear();
if (mCamera != null) {
mCamera.stopPreview();
mCamera.setPreviewCallbackWithBuffer(null);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mCamera.setPreviewTexture(null);
} else {
mCamera.setPreviewDisplay(null);
}
} catch (Exception e) {
PreyLogger.e("Failed to clear camera preview: " + e.getMessage(),e);
}
mCamera.release();
mCamera = null;
}
}
}
public Size getPreviewSize() {
return mPreviewSize;
}
public int getCameraFacing() {
return mFacing;
}
public int doZoom(float scale) {
synchronized (mCameraLock) {
if (mCamera == null) {
return 0;
}
int currentZoom = 0;
int maxZoom;
Camera.Parameters parameters = mCamera.getParameters();
if (!parameters.isZoomSupported()) {
PreyLogger.d("Zoom is not supported on this device");
return currentZoom;
}
maxZoom = parameters.getMaxZoom();
currentZoom = parameters.getZoom() + 1;
float newZoom;
if (scale > 1) {
newZoom = currentZoom + scale * (maxZoom / 10);
} else {
newZoom = currentZoom * scale;
}
currentZoom = Math.round(newZoom) - 1;
if (currentZoom < 0) {
currentZoom = 0;
} else if (currentZoom > maxZoom) {
currentZoom = maxZoom;
}
parameters.setZoom(currentZoom);
mCamera.setParameters(parameters);
return currentZoom;
}
}
public void takePicture(ShutterCallback shutter, PictureCallback jpeg) {
synchronized (mCameraLock) {
if (mCamera != null) {
PictureStartCallback startCallback = new PictureStartCallback();
startCallback.mDelegate = shutter;
PictureDoneCallback doneCallback = new PictureDoneCallback();
doneCallback.mDelegate = jpeg;
mCamera.takePicture(startCallback, null, null, doneCallback);
}
}
}
@Nullable
@FocusMode
public String getFocusMode() {
return mFocusMode;
}
public boolean setFocusMode(@FocusMode String mode) {
synchronized (mCameraLock) {
if (mCamera != null && mode != null) {
Camera.Parameters parameters = mCamera.getParameters();
if (parameters.getSupportedFocusModes().contains(mode)) {
parameters.setFocusMode(mode);
mCamera.setParameters(parameters);
mFocusMode = mode;
return true;
}
}
return false;
}
}
@Nullable
@FlashMode
public String getFlashMode() {
return mFlashMode;
}
public boolean setFlashMode(@FlashMode String mode) {
synchronized (mCameraLock) {
if (mCamera != null && mode != null) {
Camera.Parameters parameters = mCamera.getParameters();
if (parameters.getSupportedFlashModes().contains(mode)) {
parameters.setFlashMode(mode);
mCamera.setParameters(parameters);
mFlashMode = mode;
return true;
}
}
return false;
}
}
public void autoFocus(@Nullable AutoFocusCallback cb) {
synchronized (mCameraLock) {
if (mCamera != null) {
CameraAutoFocusCallback autoFocusCallback = null;
if (cb != null) {
autoFocusCallback = new CameraAutoFocusCallback();
autoFocusCallback.mDelegate = cb;
}
mCamera.autoFocus(autoFocusCallback);
}
}
}
public void cancelAutoFocus() {
synchronized (mCameraLock) {
if (mCamera != null) {
mCamera.cancelAutoFocus();
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return false;
}
synchronized (mCameraLock) {
if (mCamera != null) {
CameraAutoFocusMoveCallback autoFocusMoveCallback = null;
if (cb != null) {
autoFocusMoveCallback = new CameraAutoFocusMoveCallback();
autoFocusMoveCallback.mDelegate = cb;
}
mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback);
}
}
return true;
}
private CameraSource() {
}
private class PictureStartCallback implements Camera.ShutterCallback {
private ShutterCallback mDelegate;
@Override
public void onShutter() {
if (mDelegate != null) {
mDelegate.onShutter();
}
}
}
private class PictureDoneCallback implements Camera.PictureCallback {
private PictureCallback mDelegate;
@Override
public void onPictureTaken(byte[] data, Camera camera) {
if (mDelegate != null) {
mDelegate.onPictureTaken(data);
}
synchronized (mCameraLock) {
if (mCamera != null) {
mCamera.startPreview();
}
}
}
}
private class CameraAutoFocusCallback implements Camera.AutoFocusCallback {
private AutoFocusCallback mDelegate;
@Override
public void onAutoFocus(boolean success, Camera camera) {
if (mDelegate != null) {
mDelegate.onAutoFocus(success);
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback {
private AutoFocusMoveCallback mDelegate;
@Override
public void onAutoFocusMoving(boolean start, Camera camera) {
if (mDelegate != null) {
mDelegate.onAutoFocusMoving(start);
}
}
}
@SuppressLint("InlinedApi")
private Camera createCamera() {
int requestedCameraId = getIdForRequestedCamera(mFacing);
if (requestedCameraId == -1) {
throw new RuntimeException("Could not find requested camera.");
}
Camera camera = Camera.open(requestedCameraId);
SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight);
if (sizePair == null) {
throw new RuntimeException("Could not find suitable preview size.");
}
Size pictureSize = sizePair.pictureSize();
mPreviewSize = sizePair.previewSize();
int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps);
if (previewFpsRange == null) {
throw new RuntimeException("Could not find suitable preview frames per second range.");
}
Camera.Parameters parameters = camera.getParameters();
if (pictureSize != null) {
parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
}
parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
parameters.setPreviewFpsRange(
previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
parameters.setPreviewFormat(ImageFormat.NV21);
setRotation(camera, parameters, requestedCameraId);
if (mFocusMode != null) {
if (parameters.getSupportedFocusModes().contains(
mFocusMode)) {
parameters.setFocusMode(mFocusMode);
} else {
PreyLogger.i("Camera focus mode: " + mFocusMode + " is not supported on this device.");
}
}
mFocusMode = parameters.getFocusMode();
if (mFlashMode != null) {
if (parameters.getSupportedFlashModes().contains(
mFlashMode)) {
parameters.setFlashMode(mFlashMode);
} else {
PreyLogger.i("Camera flash mode: " + mFlashMode + " is not supported on this device.");
}
}
mFlashMode = parameters.getFlashMode();
camera.setParameters(parameters);
camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback());
camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
return camera;
}
private static int getIdForRequestedCamera(int facing) {
CameraInfo cameraInfo = new CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == facing) {
return i;
}
}
return -1;
}
private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) {
List<SizePair> validPreviewSizes = generateValidPreviewSizeList(camera);
SizePair selectedPair = null;
int minDiff = Integer.MAX_VALUE;
for (SizePair sizePair : validPreviewSizes) {
Size size = sizePair.previewSize();
int diff = Math.abs(size.getWidth() - desiredWidth) +
Math.abs(size.getHeight() - desiredHeight);
if (diff < minDiff) {
selectedPair = sizePair;
minDiff = diff;
}
}
return selectedPair;
}
private static class SizePair {
private Size mPreview;
private Size mPicture;
public SizePair(Camera.Size previewSize,
Camera.Size pictureSize) {
mPreview = new Size(previewSize.width, previewSize.height);
if (pictureSize != null) {
mPicture = new Size(pictureSize.width, pictureSize.height);
}
}
public Size previewSize() {
return mPreview;
}
@SuppressWarnings("unused")
public Size pictureSize() {
return mPicture;
}
}
private static List<SizePair> generateValidPreviewSizeList(Camera camera) {
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> supportedPreviewSizes =
parameters.getSupportedPreviewSizes();
List<Camera.Size> supportedPictureSizes =
parameters.getSupportedPictureSizes();
List<SizePair> validPreviewSizes = new ArrayList<>();
for (Camera.Size previewSize : supportedPreviewSizes) {
float previewAspectRatio = (float) previewSize.width / (float) previewSize.height;
for (Camera.Size pictureSize : supportedPictureSizes) {
float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height;
if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) {
validPreviewSizes.add(new SizePair(previewSize, pictureSize));
break;
}
}
}
if (validPreviewSizes.size() == 0) {
PreyLogger.d("No preview sizes have a corresponding same-aspect-ratio picture size");
for (Camera.Size previewSize : supportedPreviewSizes) {
validPreviewSizes.add(new SizePair(previewSize, null));
}
}
return validPreviewSizes;
}
private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) {
int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f);
int[] selectedFpsRange = null;
int minDiff = Integer.MAX_VALUE;
List<int[]> previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange();
for (int[] range : previewFpsRangeList) {
int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
int diff = Math.abs(deltaMin) + Math.abs(deltaMax);
if (diff < minDiff) {
selectedFpsRange = range;
minDiff = diff;
}
}
return selectedFpsRange;
}
private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) {
WindowManager windowManager =
(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
int degrees = 0;
int rotation = windowManager.getDefaultDisplay().getRotation();
switch (rotation) {
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;
default:
PreyLogger.d("Bad rotation value: " + rotation);
}
CameraInfo cameraInfo = new CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
int angle;
int displayAngle;
if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
angle = (cameraInfo.orientation + degrees) % 360;
displayAngle = (360 - angle); // compensate for it being mirrored
} else { // back-facing
angle = (cameraInfo.orientation - degrees + 360) % 360;
displayAngle = angle;
}
mRotation = angle / 90;
camera.setDisplayOrientation(displayAngle);
parameters.setRotation(angle);
}
private byte[] createPreviewBuffer(Size previewSize) {
int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21);
long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel;
int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1;
byte[] byteArray = new byte[bufferSize];
ByteBuffer buffer = ByteBuffer.wrap(byteArray);
if (!buffer.hasArray() || (buffer.array() != byteArray)) {
throw new IllegalStateException("Failed to create valid buffer for camera source.");
}
mBytesToByteBuffer.put(byteArray, buffer);
return byteArray;
}
private class CameraPreviewCallback implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
mFrameProcessor.setNextFrame(data, camera);
}
}
private class FrameProcessingRunnable implements Runnable {
private Detector<?> mDetector;
private long mStartTimeMillis = SystemClock.elapsedRealtime();
private final Object mLock = new Object();
private boolean mActive = true;
private long mPendingTimeMillis;
private int mPendingFrameId = 0;
private ByteBuffer mPendingFrameData;
FrameProcessingRunnable(Detector<?> detector) {
mDetector = detector;
}
@SuppressLint("Assert")
void release() {
assert (mProcessingThread.getState() == State.TERMINATED);
mDetector.release();
mDetector = null;
}
void setActive(boolean active) {
synchronized (mLock) {
mActive = active;
mLock.notifyAll();
}
}
void setNextFrame(byte[] data, Camera camera) {
synchronized (mLock) {
if (mPendingFrameData != null) {
camera.addCallbackBuffer(mPendingFrameData.array());
mPendingFrameData = null;
}
if (!mBytesToByteBuffer.containsKey(data)) {
PreyLogger.d(
"Skipping frame. Could not find ByteBuffer associated with the image " +
"data from the camera.");
return;
}
mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis;
mPendingFrameId++;
mPendingFrameData = mBytesToByteBuffer.get(data);
mLock.notifyAll();
}
}
@Override
public void run() {
Frame outputFrame;
ByteBuffer data;
while (true) {
synchronized (mLock) {
while (mActive && (mPendingFrameData == null)) {
try {
mLock.wait();
} catch (InterruptedException e) {
PreyLogger.e("Frame processing loop terminated.", e);
return;
}
}
if (!mActive) {
return;
}
outputFrame = new Frame.Builder()
.setImageData(mPendingFrameData, mPreviewSize.getWidth(),
mPreviewSize.getHeight(), ImageFormat.NV21)
.setId(mPendingFrameId)
.setTimestampMillis(mPendingTimeMillis)
.setRotation(mRotation)
.build();
data = mPendingFrameData;
mPendingFrameData = null;
}
try {
mDetector.receiveFrame(outputFrame);
} catch (Throwable t) {
PreyLogger.e("Exception thrown from receiver.", t);
} finally {
mCamera.addCallbackBuffer(data.array());
}
}
}
}
}