/***
Copyright (c) 2013-2014 CommonsWare, LLC
Portions Copyright (C) 2007 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.commonsware.cwac.camera;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import java.io.IOException;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
public class CameraView extends ViewGroup implements AutoFocusCallback {
static final String TAG="CWAC-Camera";
private PreviewStrategy previewStrategy;
private Camera.Size previewSize;
private Camera camera=null;
private boolean inPreview=false;
private CameraHost host=null;
private OnOrientationChange onOrientationChange=null;
private int displayOrientation=-1;
private int outputOrientation=-1;
private int cameraId=-1;
private MediaRecorder recorder=null;
private Camera.Parameters previewParams=null;
private boolean isDetectingFaces=false;
private boolean isAutoFocusing=false;
private int lastPictureOrientation=-1;
public CameraView(Context context) {
super(context);
onOrientationChange=new OnOrientationChange(context);
}
public CameraView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CameraView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
onOrientationChange=new OnOrientationChange(context);
if (context instanceof CameraHostProvider) {
setHost(((CameraHostProvider)context).getCameraHost());
}
else {
throw new IllegalArgumentException("To use the two- or "
+ "three-parameter constructors on CameraView, "
+ "your activity needs to implement the "
+ "CameraHostProvider interface");
}
}
public CameraHost getHost() {
return(host);
}
// must call this after constructor, before onResume()
public void setHost(CameraHost host) {
this.host=host;
if (host.getDeviceProfile().useTextureView()) {
previewStrategy=new TexturePreviewStrategy(this);
}
else {
previewStrategy=new SurfacePreviewStrategy(this);
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onResume() {
addView(previewStrategy.getWidget());
if (camera == null) {
cameraId=getHost().getCameraId();
if (cameraId >= 0) {
try {
camera=Camera.open(cameraId);
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
onOrientationChange.enable();
}
setCameraDisplayOrientation();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& getHost() instanceof Camera.FaceDetectionListener) {
camera.setFaceDetectionListener((Camera.FaceDetectionListener)getHost());
}
}
catch (Exception e) {
getHost().onCameraFail(FailureReason.UNKNOWN);
}
}
else {
getHost().onCameraFail(FailureReason.NO_CAMERAS_REPORTED);
}
}
}
public Camera getCamera() {
return camera;
}
public void onPause() {
if (camera != null) {
previewDestroyed();
removeView(previewStrategy.getWidget());
}
lastPictureOrientation=-1;
}
// based on CameraPreview.java from ApiDemos
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int width=
resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
final int height=
resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(width, height);
if (width > 0 && height > 0) {
if (camera != null) {
Camera.Size newSize=null;
try {
if (getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY) {
// Camera.Size deviceHint=
// host.getDeviceProfile()
// .getPreferredPreviewSizeForVideo(getDisplayOrientation(),
// width,
// height,
// camera.getParameters());
newSize=
getHost().getPreferredPreviewSizeForVideo(getDisplayOrientation(),
width,
height,
camera.getParameters(),
null);
// if (newSize != null) {
// android.util.Log.wtf("CameraView",
// String.format("getPreferredPreviewSizeForVideo: %d x %d",
// newSize.width,
// newSize.height));
// }
}
if (newSize == null || newSize.width * newSize.height < 65536) {
newSize=
getHost().getPreviewSize(getDisplayOrientation(),
width, height,
camera.getParameters());
}
}
catch (Exception e) {
android.util.Log.e(getClass().getSimpleName(),
"Could not work with camera parameters?",
e);
// TODO get this out to library clients
}
if (newSize != null) {
if (previewSize == null) {
previewSize=newSize;
}
else if (previewSize.width != newSize.width
|| previewSize.height != newSize.height) {
if (inPreview) {
stopPreview();
}
previewSize=newSize;
initPreview(width, height, false);
}
}
}
}
}
// based on CameraPreview.java from ApiDemos
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed && getChildCount() > 0) {
final View child=getChildAt(0);
final int width=r - l;
final int height=b - t;
int previewWidth=width;
int previewHeight=height;
// handle orientation
if (previewSize != null) {
if (getDisplayOrientation() == 90
|| getDisplayOrientation() == 270) {
previewWidth=previewSize.height;
previewHeight=previewSize.width;
}
else {
previewWidth=previewSize.width;
previewHeight=previewSize.height;
}
}
boolean useFirstStrategy=
(width * previewHeight > height * previewWidth);
boolean useFullBleed=getHost().useFullBleedPreview();
if ((useFirstStrategy && !useFullBleed)
|| (!useFirstStrategy && useFullBleed)) {
final int scaledChildWidth=
previewWidth * height / previewHeight;
child.layout((width - scaledChildWidth) / 2, 0,
(width + scaledChildWidth) / 2, height);
}
else {
final int scaledChildHeight=
previewHeight * width / previewWidth;
child.layout(0, (height - scaledChildHeight) / 2, width,
(height + scaledChildHeight) / 2);
}
}
}
public int getDisplayOrientation() {
return(displayOrientation);
}
public void lockToLandscape(boolean enable) {
if (enable) {
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
onOrientationChange.enable();
}
else {
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
onOrientationChange.disable();
}
}
public void restartPreview() {
if (!inPreview) {
startPreview();
}
}
public void takePicture(boolean needBitmap, boolean needByteArray) {
PictureTransaction xact=new PictureTransaction(getHost());
takePicture(xact.needBitmap(needBitmap)
.needByteArray(needByteArray));
}
public void takePicture(final PictureTransaction xact) {
if (inPreview) {
if (isAutoFocusing) {
throw new IllegalStateException(
"Camera cannot take a picture while auto-focusing");
}
else {
previewParams=camera.getParameters();
Camera.Parameters pictureParams=camera.getParameters();
Camera.Size pictureSize=
xact.host.getPictureSize(xact, pictureParams);
pictureParams.setPictureSize(pictureSize.width,
pictureSize.height);
pictureParams.setPictureFormat(ImageFormat.JPEG);
if (xact.flashMode != null) {
pictureParams.setFlashMode(xact.flashMode);
}
if (!onOrientationChange.isEnabled()) {
setCameraPictureOrientation(pictureParams);
}
camera.setParameters(xact.host.adjustPictureParameters(xact,
pictureParams));
xact.cameraView=this;
postDelayed(new Runnable() {
@Override
public void run() {
try {
camera.takePicture(xact, null,
new PictureTransactionCallback(xact));
}
catch (Exception e) {
android.util.Log.e(getClass().getSimpleName(),
"Exception taking a picture", e);
// TODO get this out to library clients
}
}
}, xact.host.getDeviceProfile().getPictureDelay());
inPreview=false;
}
}
else {
throw new IllegalStateException(
"Preview mode must have started before you can take a picture");
}
}
public boolean isRecording() {
return(recorder != null);
}
public void record() throws Exception {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
throw new UnsupportedOperationException(
"Video recording supported only on API Level 11+");
}
if (displayOrientation != 0 && displayOrientation != 180) {
throw new UnsupportedOperationException(
"Video recording supported only in landscape");
}
Camera.Parameters pictureParams=camera.getParameters();
setCameraPictureOrientation(pictureParams);
camera.setParameters(pictureParams);
stopPreview();
camera.unlock();
try {
recorder=new MediaRecorder();
recorder.setCamera(camera);
getHost().configureRecorderAudio(cameraId, recorder);
recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
getHost().configureRecorderProfile(cameraId, recorder);
getHost().configureRecorderOutput(cameraId, recorder);
recorder.setOrientationHint(outputOrientation);
previewStrategy.attach(recorder);
recorder.prepare();
recorder.start();
}
catch (IOException e) {
recorder.release();
recorder=null;
throw e;
}
}
public void stopRecording() throws IOException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
throw new UnsupportedOperationException(
"Video recording supported only on API Level 11+");
}
MediaRecorder tempRecorder=recorder;
recorder=null;
tempRecorder.stop();
tempRecorder.release();
camera.reconnect();
startPreview();
}
public void autoFocus() {
if (inPreview) {
camera.autoFocus(this);
isAutoFocusing=true;
}
}
public void cancelAutoFocus() {
camera.cancelAutoFocus();
}
public boolean isAutoFocusAvailable() {
return(inPreview);
}
@Override
public void onAutoFocus(boolean success, Camera camera) {
isAutoFocusing=false;
if (getHost() instanceof AutoFocusCallback) {
getHost().onAutoFocus(success, camera);
}
}
public String getFlashMode() {
return(camera.getParameters().getFlashMode());
}
public void setFlashMode(String mode) {
if (camera != null) {
Camera.Parameters params=camera.getParameters();
params.setFlashMode(mode);
camera.setParameters(params);
}
}
public ZoomTransaction zoomTo(int level) {
if (camera == null) {
throw new IllegalStateException(
"Yes, we have no camera, we have no camera today");
}
else {
Camera.Parameters params=camera.getParameters();
if (level >= 0 && level <= params.getMaxZoom()) {
return(new ZoomTransaction(camera, level));
}
else {
throw new IllegalArgumentException(
String.format("Invalid zoom level: %d",
level));
}
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void startFaceDetection() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& camera != null && !isDetectingFaces
&& camera.getParameters().getMaxNumDetectedFaces() > 0) {
camera.startFaceDetection();
isDetectingFaces=true;
}
}
public void stopFaceDetection() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& camera != null && isDetectingFaces) {
try {
camera.stopFaceDetection();
}
catch (Exception e) {
// TODO get this out to hosting app
}
isDetectingFaces=false;
}
}
public boolean doesZoomReallyWork() {
CameraInfo info=new CameraInfo();
Camera.getCameraInfo(getHost().getCameraId(), info);
return(getHost().getDeviceProfile().doesZoomActuallyWork(info.facing == CameraInfo.CAMERA_FACING_FRONT));
}
void previewCreated() {
if (camera != null) {
try {
previewStrategy.attach(camera);
}
catch (IOException e) {
getHost().handleException(e);
}
}
}
void previewDestroyed() {
if (camera != null) {
previewStopped();
camera.release();
camera=null;
}
}
void previewReset(int width, int height) {
previewStopped();
initPreview(width, height);
}
private void previewStopped() {
if (inPreview) {
stopPreview();
}
}
public void initPreview(int w, int h) {
initPreview(w, h, true);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void initPreview(int w, int h, boolean firstRun) {
if (camera != null) {
Camera.Parameters parameters=camera.getParameters();
parameters.setPreviewSize(previewSize.width, previewSize.height);
//Since we are doing stills only!
/*
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
parameters.setRecordingHint(getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY);
}
*/
requestLayout();
camera.setParameters(getHost().adjustPreviewParameters(parameters));
startPreview();
}
}
//Implement the previewCallback
private final class RawPreviewCallback implements Camera.PreviewCallback {
public void onPreviewFrame(byte [] rawData, Camera camera) {
getHost().onPreviewFrame(rawData, camera);
}
};
private void startPreview() {
camera.setPreviewCallback(new RawPreviewCallback());
camera.startPreview();
inPreview=true;
getHost().autoFocusAvailable();
}
private void stopPreview() {
inPreview=false;
getHost().autoFocusUnavailable();
camera.setPreviewCallback(null);
camera.stopPreview();
}
// based on
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
// and http://stackoverflow.com/a/10383164/115145
private void setCameraDisplayOrientation() {
CameraInfo info=new CameraInfo();
int rotation=
getActivity().getWindowManager().getDefaultDisplay()
.getRotation();
int degrees=0;
DisplayMetrics dm=new DisplayMetrics();
Camera.getCameraInfo(cameraId, info);
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
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;
}
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
displayOrientation=(info.orientation + degrees) % 360;
displayOrientation=(360 - displayOrientation) % 360;
}
else {
displayOrientation=(info.orientation - degrees + 360) % 360;
}
boolean wasInPreview=inPreview;
if (inPreview) {
stopPreview();
}
camera.setDisplayOrientation(displayOrientation);
if (wasInPreview) {
startPreview();
}
}
private void setCameraPictureOrientation(Camera.Parameters params) {
CameraInfo info=new CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
outputOrientation=
getCameraPictureRotation(getActivity().getWindowManager()
.getDefaultDisplay()
.getOrientation());
}
else if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
outputOrientation=(360 - displayOrientation) % 360;
}
else {
outputOrientation=displayOrientation;
}
if (lastPictureOrientation != outputOrientation) {
params.setRotation(outputOrientation);
lastPictureOrientation=outputOrientation;
}
}
// based on:
// http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int)
private int getCameraPictureRotation(int orientation) {
CameraInfo info=new CameraInfo();
Camera.getCameraInfo(cameraId, info);
int rotation=0;
orientation=(orientation + 45) / 90 * 90;
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
rotation=(info.orientation - orientation + 360) % 360;
}
else { // back-facing camera
rotation=(info.orientation + orientation) % 360;
}
return(rotation);
}
Activity getActivity() {
return((Activity)getContext());
}
private class OnOrientationChange extends OrientationEventListener {
private boolean isEnabled=false;
public OnOrientationChange(Context context) {
super(context);
disable();
}
@Override
public void onOrientationChanged(int orientation) {
if (camera != null && orientation != ORIENTATION_UNKNOWN) {
int newOutputOrientation=getCameraPictureRotation(orientation);
if (newOutputOrientation != outputOrientation) {
outputOrientation=newOutputOrientation;
Camera.Parameters params=camera.getParameters();
params.setRotation(outputOrientation);
camera.setParameters(params);
lastPictureOrientation=outputOrientation;
}
}
}
@Override
public void enable() {
isEnabled=true;
super.enable();
}
@Override
public void disable() {
isEnabled=false;
super.disable();
}
boolean isEnabled() {
return(isEnabled);
}
}
private class PictureTransactionCallback implements
Camera.PictureCallback {
PictureTransaction xact=null;
PictureTransactionCallback(PictureTransaction xact) {
this.xact=xact;
}
@Override
public void onPictureTaken(byte[] data, Camera camera) {
camera.setParameters(previewParams);
if (data != null) {
new ImageCleanupTask(getContext(), data, cameraId, xact).start();
}
if (!xact.useSingleShotMode()) {
startPreview();
}
}
}
}