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()); } } } } }