/*
* Artcodes recognises a different marker scheme that allows the
* creation of aesthetically pleasing, even beautiful, codes.
* Copyright (C) 2013-2016 The University of Nottingham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.horizon.artcodes.camera;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import java.io.IOException;
import java.util.List;
import uk.ac.horizon.artcodes.detect.Detector;
import uk.ac.horizon.artcodes.model.Experience;
import uk.ac.horizon.artcodes.scanner.R;
import uk.ac.horizon.artcodes.detect.DetectorSetting;
@SuppressWarnings("deprecation")
public class CameraView extends SurfaceView
{
private static final String THREAD_NAME = "Frame Processor";
private CameraInfo info;
private Camera camera;
private int facing = Camera.CameraInfo.CAMERA_FACING_BACK;
private Detector detector;
private HandlerThread cameraThread;
private SurfaceHolder surface;
private int surfaceWidth;
private int surfaceHeight;
private boolean deviceNeedsTapToFocus = false;
private final Object cameraFocusLock = new Object();
private boolean cameraIsFocused = false;
private Experience experience;
public CameraView(Context context)
{
super(context);
init();
}
public CameraView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public CameraView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CameraView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
{
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public void setDetector(Detector processor)
{
setDetector(processor, null);
}
public void setDetector(Detector processor, Experience experience)
{
this.experience = experience;
this.detector = processor;
if (detector != null)
{
if (camera == null)
{
createCamera();
}
if (camera != null)
{
camera.addCallbackBuffer(detector.createBuffer(info, surfaceWidth, surfaceHeight));
camera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback()
{
@Override
public void onPreviewFrame(final byte[] data, final Camera camera)
{
detector.setData(data);
if (deviceNeedsTapToFocus)
{
synchronized (cameraFocusLock)
{
while (!cameraIsFocused)
{
try
{
Log.i("Scanner", "Thread " + Thread.currentThread().getName() + " waiting on cameraFocusLock for camera focus");
cameraFocusLock.wait();
}
catch (InterruptedException ignored)
{
}
Log.i("Scanner", "Thread " + Thread.currentThread().getName() + " notified, cameraIsFocused="+cameraIsFocused);
}
}
}
camera.addCallbackBuffer(data);
}
});
}
if (Camera.getNumberOfCameras() > 1)
{
detector.getSettings().add(new DetectorSetting()
{
@Override
public void nextValue()
{
stopCamera();
facing = 1 - facing;
startCamera();
}
@Override
public int getIcon()
{
switch (facing)
{
case Camera.CameraInfo.CAMERA_FACING_BACK:
return R.drawable.ic_camera_rear_24dp;
case Camera.CameraInfo.CAMERA_FACING_FRONT:
return R.drawable.ic_camera_front_24dp;
}
return 0;
}
@Override
public int getText()
{
switch (facing)
{
case Camera.CameraInfo.CAMERA_FACING_BACK:
return R.string.camera_rear;
case Camera.CameraInfo.CAMERA_FACING_FRONT:
return R.string.camera_front;
}
return 0;
}
});
}
}
}
private void init()
{
getHolder().addCallback(new SurfaceHolder.Callback()
{
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
if (holder.getSurface() == null)
{
Log.i("scanner", "No surface?");
return;
}
surface = holder;
stopCamera();
surfaceWidth = width;
surfaceHeight = height;
startCamera();
}
@Override
public void surfaceCreated(SurfaceHolder holder)
{
}
@Override
public void surfaceDestroyed(SurfaceHolder holder)
{
stopCamera();
}
});
}
private void createCamera()
{
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
{
for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++)
{
try
{
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (info.facing == facing)
{
openCamera(cameraId);
return;
}
}
catch (RuntimeException e)
{
Log.e("Scanner", "Failed to open scanner " + cameraId + ": " + e.getLocalizedMessage(), e);
}
}
for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++)
{
try
{
openCamera(cameraId);
return;
}
catch (RuntimeException e)
{
Log.e("Scanner", "Failed to open scanner " + cameraId + ": " + e.getLocalizedMessage(), e);
}
}
}
}
private void openCamera(int cameraId)
{
Log.i("Scanner", "Device manufacturer: " + android.os.Build.MANUFACTURER + " model: " + android.os.Build.MODEL);
camera = Camera.open(cameraId);
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
Camera.Parameters parameters = camera.getParameters();
List<String> focusModes = parameters.getSupportedFocusModes();
if (!android.os.Build.MANUFACTURER.equalsIgnoreCase("SAMSUNG") &&
focusModes != null &&
!(this.experience!=null && this.experience.getRequestedAutoFocusMode()!=null && this.experience.getRequestedAutoFocusMode().equals("tapToFocus") && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) &&
focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO))
{
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
else if (focusModes != null && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO))
{
// if FOCUS_MODE_CONTINUOUS_VIDEO is not supported flag that manual auto-focus is needed every few seconds
Log.w("Scanner", "Camera requires manual focusing");
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
deviceNeedsTapToFocus = true;
}
float ratioOfSurface = (float) surfaceHeight / surfaceWidth;
if (ratioOfSurface < 1)
{
ratioOfSurface = 1 / ratioOfSurface;
}
Log.i("Scanner", "Surface size: " + surfaceWidth + "x" + surfaceHeight + " (Ratio: " + ratioOfSurface + ")");
Log.i("Scanner", "Format = " + parameters.getPictureFormat());
// Step 2: Find scanner preview that is best match for estimated surface ratio
final List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Camera.Size bestFitSoFar = null;
float ratioDifferenceOfBestFitSoFar = 0;
for (Camera.Size supportedSize : supportedPreviewSizes)
{
float ratio = (float) supportedSize.width / supportedSize.height;
float ratioDifference = Math.abs(ratio - ratioOfSurface);
//Log.i("Scanner", "Preview Size " + supportedSize.width + "x" + supportedSize.height + " (" + ratio + ")");
if (bestFitSoFar == null || ratioDifference < ratioDifferenceOfBestFitSoFar)
{
bestFitSoFar = supportedSize;
ratioDifferenceOfBestFitSoFar = ratioDifference;
}
}
if (bestFitSoFar != null)
{
// Would only be null if there are no supportedPreviewSizes
Log.i("Scanner", "Selected Preview Size: " + bestFitSoFar.width + "x" + bestFitSoFar.height + " (" + ((float) bestFitSoFar.width / (float) bestFitSoFar.height) + ")");
parameters.setPreviewSize(bestFitSoFar.width, bestFitSoFar.height);
camera.setParameters(parameters);
info = new CameraInfo(cameraInfo, parameters, getDeviceRotation());
camera.setDisplayOrientation(info.getRotation());
setDetector(detector, experience);
try
{
camera.setPreviewDisplay(surface);
}
catch (IOException e)
{
Log.w("Scanner", e.getMessage(), e);
}
camera.startPreview();
}
}
private int getDeviceRotation()
{
WindowManager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
int rotation = manager.getDefaultDisplay().getRotation();
switch (rotation)
{
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 void startCamera()
{
if (cameraThread == null)
{
cameraThread = new HandlerThread(THREAD_NAME);
cameraThread.start();
Handler cameraHandler = new Handler(cameraThread.getLooper());
cameraHandler.post(new Runnable()
{
@Override
public void run()
{
createCamera();
}
});
}
else
{
camera.startPreview();
}
}
private void stopCamera()
{
if (camera != null)
{
camera.stopPreview();
camera.setPreviewCallback(null);
camera.release();
if (cameraThread != null)
{
cameraThread.quit();
cameraThread = null;
}
camera = null;
}
}
/* Auto focus methods */
public boolean deviceNeedsTapToFocus()
{
return this.deviceNeedsTapToFocus;
}
public void focus(final Runnable callback)
{
synchronized (cameraFocusLock)
{
cameraIsFocused = false;
}
if (camera != null)
{
Log.i("Scanner", "Attempting to focus camera.");
camera.autoFocus(null);
}
// set a timeout rather than use the auto focus callback
// (because the auto focus callback never gets called on some devices)
Handler h = new Handler(Looper.getMainLooper());
h.postDelayed(new Runnable()
{
@Override
public void run()
{
Log.i("Scanner", "Firing auto focus timeout, notifying on cameraFocusLock.");
synchronized (cameraFocusLock)
{
cameraIsFocused = true;
cameraFocusLock.notifyAll();
}
callback.run();
}
}, 1000);
}
}