/* * * Copyright (c) Microsoft. All rights reserved. * Licensed under the MIT license. * * Project Oxford: http://ProjectOxford.ai * * Project Oxford Mimicker Alarm Github: * https://github.com/Microsoft/ProjectOxford-Apps-MimickerAlarm * * Copyright (c) Microsoft Corporation * All rights reserved. * * MIT License: * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ package com.microsoft.mimickeralarm.mimics; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.YuvImage; import android.hardware.Camera; import android.os.AsyncTask; import android.os.Handler; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import com.microsoft.mimickeralarm.utilities.Logger; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Class that creates a camera preview for either the front or back camera * since the image captured is not really for artistic purposes or for keeping, * this camera values speed over quality. It chooses a decent resolution, not the max, and * upon capture, immediately returns the last preview frame displayed as opposed to the actual * image captured by the camera * * To use this, pass in * a CapturedImageCallbackAsync to process the image returned, * an aspect ratio to use. The class will find the camera setting that best fits this aspect ratio, * a camera facing (Front or Back) * * Public methods: * initPreview (initialize the camera and the preview surface), * start (call initPreview before), * stop, * onCapture (set the CapturedImageCallbackAsync), * onFocus (focus the camera at a certain x, y position) */ @SuppressWarnings("deprecation") public class CameraPreview implements SurfaceHolder.Callback { private static final String LOGTAG = "CameraPreview"; private static final int MAX_SIZE = 1080; private static final double ASPECT_RATIO_EPSILON = 0.02; private SurfaceView mPreviewView; private Camera mCamera; private int mCameraFacing; private int mCameraRotation; private double mCameraAspectRatio; private Boolean mIsFlashSupported; // we only want torch mode. Boolean type so we can cache the result private boolean mFlashState; private FlashStateCallback mFlashStateCallback; private CapturedImageCallbackAsync mCapturedCapturedImageCallbackAsync; private CameraInitializedCallback mCameraInitializedCallback; private Camera.PreviewCallback mCaptureCallback = new Camera.PreviewCallback() { public void onPreviewFrame(byte[] data, Camera camera) { camera.stopPreview(); if (mIsFlashSupported) { // Delay turning off flash for 0.5s to allow camera to capture image final Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { changeFlashState(false); if (mFlashStateCallback != null) { mFlashStateCallback.execute(false); } } }, 500); } new processCaptureImage().execute(data, camera); } }; public interface CapturedImageCallbackAsync { void execute(Bitmap bitmap); } public interface CameraInitializedCallback { void execute(boolean success); } public interface FlashStateCallback { void execute(boolean state); } private OnCameraPreviewException mOnException; public CameraPreview(SurfaceView surfaceView, OnCameraPreviewException onException, CameraInitializedCallback onCameraInitialized, double aspectRatio, int facing) { mCameraAspectRatio = aspectRatio; mCameraFacing = facing; mPreviewView = surfaceView; mCameraRotation = 0; mIsFlashSupported = null; mFlashState = false; final SurfaceHolder surfaceHolder = mPreviewView.getHolder(); surfaceHolder.addCallback(this); mOnException = onException; mCameraInitializedCallback = onCameraInitialized; } @Override public void surfaceCreated(SurfaceHolder holder) {} @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { (new OpenCameraTask()).execute(); } @Override public void surfaceDestroyed(SurfaceHolder holder) {} private void initPreview(){ if (mCamera == null) { mCamera = getNewCamera(); } try { if (mCamera != null) { mCamera.setPreviewDisplay(mPreviewView.getHolder()); } } catch (IOException e) { Logger.trackException(e); } } public void stop() { if (mCamera != null) { changeFlashState(false); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } public void start() throws MimicException{ if (mCamera != null) { mCamera.startPreview(); } else{ MimicException e = new MimicException("failed to open camera"); Logger.trackException(e); throw e; } } public void onCapture(CapturedImageCallbackAsync callback, FlashStateCallback flashCallback) { mCapturedCapturedImageCallbackAsync = callback; mFlashStateCallback = flashCallback; if (mCamera != null) { mCamera.setOneShotPreviewCallback(mCaptureCallback); } } public void onFocus(int x, int y) { RectF focusRectF = new RectF(x - 10, y - 10, x + 10, y + 10); Matrix rotateMatrix = new Matrix(); rotateMatrix.postRotate(-mCameraRotation); rotateMatrix.mapRect(focusRectF); Rect focusRect = new Rect(); focusRectF.round(focusRect); if (mCamera == null){ return; } try { Camera.Parameters parameters = mCamera.getParameters(); if (parameters == null) { return; } if (parameters.getMaxNumFocusAreas() > 0) { List<Camera.Area> focusAreas = new ArrayList<>(); focusAreas.add(new Camera.Area(focusRect, 1000)); parameters.setFocusAreas(focusAreas); mCamera.cancelAutoFocus(); mCamera.setParameters(parameters); mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { } }); } } catch (Exception e) { Logger.trackException(e); } } public boolean isFlashSupported() { if (mIsFlashSupported != null) { return mIsFlashSupported; } // use mIsFlashSupported to cache; mIsFlashSupported = false; // Currently let's limit flash to back camera only if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { return false; } if (mCamera == null) { return false; } Camera.Parameters parameters = mCamera.getParameters(); if (parameters == null) { return false; } List<String> flashModes = parameters.getSupportedFlashModes(); if (flashModes != null && !flashModes.isEmpty()){ if (flashModes.contains(Camera.Parameters.FLASH_MODE_TORCH)) { mIsFlashSupported = true; } } return mIsFlashSupported; } public boolean changeFlashState(boolean turnOn) { if (mFlashState == turnOn) { return true; } if (mCamera == null) { return false; } if (!isFlashSupported()) { return false; } Camera.Parameters parameters = mCamera.getParameters(); if (parameters == null) { return false; } try { if (turnOn) { parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); } else { parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); } mCamera.setParameters(parameters); } catch (Exception e) { Logger.trackException(e); return false; } mFlashState = turnOn; return true; } public boolean getFlashState() { return mFlashState; } private Camera getNewCamera() { Camera cam = null; Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); int cameraCount = Camera.getNumberOfCameras(); // Find the camera that's facing the right way for (int i = 0; i < cameraCount; i++) { Camera.getCameraInfo(i, cameraInfo); if (cameraInfo.facing == mCameraFacing) { try { cam = Camera.open(i); break; } catch (RuntimeException ex) { Log.e(LOGTAG, "err opening camera", ex); Logger.trackException(ex); } } } // Configure the camera with right resolution, aspect ratio and focus if (cam != null) { try { Camera.Parameters params = cam.getParameters(); // find a camera configuration of the same size as the phone screen. List<Camera.Size> supportedSizes = params.getSupportedPreviewSizes(); Camera.Size bestSize = supportedSizes.get(0); for (Camera.Size size : supportedSizes) { if (size.width > MAX_SIZE && size.height > MAX_SIZE) { continue; } if (Math.abs(((double) size.width / (double) size.height) - mCameraAspectRatio) < ASPECT_RATIO_EPSILON) { if (size.width >= bestSize.width) bestSize = size; } } params.setPreviewSize(bestSize.width, bestSize.height); // if available set the autofocus on List<String> supportedFocusModes = params.getSupportedFocusModes(); for (String mode : supportedFocusModes) { if (mode.equals(Camera.Parameters.FOCUS_MODE_AUTO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); break; } } mCameraRotation = cameraInfo.orientation; // compensate for the front camera mirror if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { mCameraRotation = (360 - mCameraRotation) % 360; } params.setRotation(mCameraRotation); cam.setParameters(params); cam.setDisplayOrientation(mCameraRotation); } catch (RuntimeException ex) { Log.e(LOGTAG, "err configuring camera", ex); Logger.trackException(ex); } } return cam; } private class OpenCameraTask extends AsyncTask<Object, String, Boolean> { @Override protected Boolean doInBackground(Object... params) { if(mCamera == null) { initPreview(); } boolean success = false; try { start(); success = true; } catch (MimicException ex) { Logger.trackException(ex); } catch (Exception ex) { Log.e(LOGTAG, "err starting camera preview", ex); Logger.trackException(ex); } return success; } @Override protected void onPostExecute(Boolean success) { super.onPostExecute(success); if (!success) { if (mOnException != null) { mOnException.execute(); } } else { if (mCameraInitializedCallback != null) { mCameraInitializedCallback.execute(true); } } } } private class processCaptureImage extends AsyncTask<Object, String, Boolean> { @Override // Decode the image data and rotate it to the proper orientation. // then run the callback, if any, on the image to do post processing protected Boolean doInBackground(Object... params) { byte[] data = (byte[]) params[0]; Camera camera = (Camera) params[1]; Camera.Parameters parameters = camera.getParameters(); int format = parameters.getPreviewFormat(); //YUV formats require more conversion if (format == ImageFormat.NV21 || format == ImageFormat.YUY2 || format == ImageFormat.NV16) { int w = parameters.getPreviewSize().width; int h = parameters.getPreviewSize().height; // Get the YuV image YuvImage yuv_image = new YuvImage(data, format, w, h, null); // Convert YuV to Jpeg Rect rect = new Rect(0, 0, w, h); ByteArrayOutputStream output_stream = new ByteArrayOutputStream(); yuv_image.compressToJpeg(rect, 100, output_stream); byte[] imageBytes = output_stream.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); Matrix transform = new Matrix(); if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { transform.preScale(-1, 1); } transform.postRotate(mCameraRotation); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), transform, true); if (mCapturedCapturedImageCallbackAsync != null) { mCapturedCapturedImageCallbackAsync.execute(bitmap); } } return null; } } public interface OnCameraPreviewException{ void execute(); } }