/***
* Copyright (c) 2015 CommonsWare, LLC
* <p>
* 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
* <p>
* 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.cam2;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.view.View;
import com.commonsware.cwac.cam2.plugin.FlashModePlugin;
import com.commonsware.cwac.cam2.plugin.FocusModePlugin;
import com.commonsware.cwac.cam2.plugin.OrientationPlugin;
import com.commonsware.cwac.cam2.plugin.SizeAndFormatPlugin;
import com.commonsware.cwac.cam2.util.Size;
import com.commonsware.cwac.cam2.util.Utils;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Queue;
/**
* Controller for camera-related functions, designed to be used
* by CameraFragment or the equivalent.
*/
public class CameraController implements CameraView.StateCallback {
private final boolean allowChangeFlashMode;
private CameraEngine engine;
private CameraSession session;
private List<CameraDescriptor> cameras=null;
private int currentCamera=0;
private final HashMap<CameraDescriptor, CameraView> previews=
new HashMap<CameraDescriptor, CameraView>();
private Queue<CameraView> availablePreviews=null;
private boolean switchPending=false;
private boolean isVideoRecording=false;
private final FocusMode focusMode;
private final boolean isVideo;
private FlashModePlugin flashModePlugin;
private int zoomLevel=0;
private int quality=0;
private final ResultReceiver onError;
public CameraController(FocusMode focusMode,
ResultReceiver onError,
boolean allowChangeFlashMode,
boolean isVideo) {
this.onError=onError;
this.focusMode=focusMode==null ?
FocusMode.CONTINUOUS : focusMode;
this.isVideo=isVideo;
this.allowChangeFlashMode=allowChangeFlashMode;
}
/**
* @return the engine being used by this fragment to access
* the camera(s) on the device
*/
public CameraEngine getEngine() {
return (engine);
}
/**
* Setter for the engine. Must be called before onCreateView()
* is called, preferably shortly after constructing the
* fragment.
*
* @param engine the engine to be used by this fragment to access
* the camera(s) on the device
*/
public void setEngine(CameraEngine engine,
CameraSelectionCriteria criteria) {
this.engine=engine;
AbstractCameraActivity.BUS.register(this);
engine.loadCameraDescriptors(criteria);
}
public int getNumberOfCameras() {
return (cameras==null ? 0 : cameras.size());
}
/**
* Call this from onStart() of an activity or fragment, or from
* an equivalent point in time. If the CameraView is ready,
* the preview should begin; otherwise, the preview will
* begin after the CameraView is ready.
*/
public void start() {
if (cameras!=null) {
CameraDescriptor camera=cameras.get(currentCamera);
CameraView cv=getPreview(camera);
if (cv.isAvailable()) {
open();
}
}
}
/**
* Call this from onStop() of an activity or fragment, or
* from an equivalent point in time, to indicate that you want
* the camera preview to stop.
*/
public void stop() throws Exception {
if (session!=null) {
stopVideoRecording(true);
CameraSession temp=session;
session=null;
engine.close(temp);
// session.destroy(); -- moved into engines
}
}
/**
* Call this from onDestroy() of an activity or fragment,
* or from an equivalent point in time, to tear down the
* entire controller. A fresh controller should
* be created if you want to use the camera again in the future.
*/
public void destroy() {
AbstractCameraActivity.BUS.post(
new ControllerDestroyedEvent(this));
AbstractCameraActivity.BUS.unregister(this);
}
/**
* Call to switch to the next camera in sequence. Most
* devices have only two cameras, and so calling this will
* switch the preview and pictures to the camera other than
* the one presently being used.
*/
public void switchCamera() throws Exception {
if (session!=null) {
getPreview(session.getDescriptor()).setVisibility(
View.INVISIBLE);
switchPending=true;
stop();
}
}
/**
* Supplies CameraView objects for each camera. After this,
* we can open() the camera.
*
* @param cameraViews a list of CameraViews
*/
public void setCameraViews(Queue<CameraView> cameraViews) {
availablePreviews=cameraViews;
previews.clear();
for (CameraView cv : cameraViews) {
cv.setStateCallback(this);
}
open(); // in case visible CameraView is already ready
}
/**
* Public because Java interfaces are intrinsically public.
* This method is not part of the class' API and should not
* be used by third-party developers.
*
* @param cv the CameraView that is now ready
*/
@Override
public void onReady(CameraView cv) {
if (cameras!=null) {
open();
}
}
/**
* Public because Java interfaces are intrinsically public.
* This method is not part of the class' API and should not
* be used by third-party developers.
*
* @param cv the CameraView that is now destroyed
*/
@Override
public void onDestroyed(CameraView cv) throws Exception {
stop();
}
/**
* Takes a picture, in accordance with the details supplied
* in the PictureTransaction. Subscribe to the
* PictureTakenEvent to get the results of the picture.
*
* @param xact a PictureTransaction describing what should be taken
*/
public void takePicture(PictureTransaction xact) {
if (session!=null) {
AbstractCameraActivity.BUS.post(new PictureCaptureStartEvent());
engine.takePicture(session, xact);
}
}
public void recordVideo(VideoTransaction xact) throws Exception {
if (session!=null) {
engine.recordVideo(session, xact);
isVideoRecording=true;
}
}
public void stopVideoRecording(boolean abandon) throws Exception {
if (session!=null && isVideoRecording) {
try {
engine.stopVideoRecording(session, abandon);
}
finally {
isVideoRecording=false;
}
}
}
public void setQuality(int quality) {
this.quality=quality;
}
public boolean canToggleFlashMode() {
return (allowChangeFlashMode &&
engine.supportsDynamicFlashModes() &&
engine.hasMoreThanOneEligibleFlashMode());
}
public FlashMode getCurrentFlashMode() {
return (session.getCurrentFlashMode());
}
public boolean supportsZoom() {
return (engine.supportsZoom(session));
}
public int getCurrentCamera() {
return (currentCamera);
}
public void setCurrentCamera(int currentCamera) {
this.currentCamera=currentCamera;
}
public boolean changeZoom(int delta) {
zoomLevel+=delta;
return (handleZoom());
}
public boolean setZoom(int zoomLevel) {
this.zoomLevel=zoomLevel;
return (handleZoom());
}
private boolean handleZoom() {
if (zoomLevel<0) {
zoomLevel=0;
}
else if (zoomLevel>100) {
zoomLevel=100;
}
return (engine.zoomTo(session, zoomLevel));
}
private CameraView getPreview(CameraDescriptor camera) {
CameraView result=previews.get(camera);
if (result==null && availablePreviews!=null) {
result=availablePreviews.remove();
previews.put(camera, result);
}
return (result);
}
private int getNextCameraIndex() {
int next=currentCamera+1;
if (next==cameras.size()) {
next=0;
}
return (next);
}
private void open() {
if (session==null) {
Size previewSize=null;
CameraDescriptor camera=cameras.get(currentCamera);
CameraView cv=getPreview(camera);
Size pictureSize;
if (quality>0) {
pictureSize=Utils.getLargestPictureSize(camera);
}
else {
pictureSize=Utils.getSmallestPictureSize(camera);
}
if (camera!=null && cv.getWidth()>0 && cv.getHeight()>0) {
previewSize=Utils.chooseOptimalSize(camera.getPreviewSizes(),
cv.getWidth(), cv.getHeight(), pictureSize);
}
SurfaceTexture texture=cv.getSurfaceTexture();
if (previewSize!=null && texture!=null) {
if (Build.VERSION.SDK_INT>=
Build.VERSION_CODES.JELLY_BEAN_MR1) {
texture.setDefaultBufferSize(previewSize.getWidth(),
previewSize.getHeight());
}
flashModePlugin=new FlashModePlugin();
session=engine
.buildSession(cv.getContext(), camera)
.addPlugin(new SizeAndFormatPlugin(previewSize,
pictureSize, ImageFormat.JPEG))
.addPlugin(new OrientationPlugin(cv.getContext()))
.addPlugin(
new FocusModePlugin(cv.getContext(), focusMode, isVideo))
.addPlugin(flashModePlugin)
.build();
session.setPreviewSize(previewSize);
engine.open(session, texture);
}
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode=ThreadMode.MAIN)
public void onEventMainThread(
CameraEngine.CameraDescriptorsEvent event) {
if (event.exception!=null) {
postError(ErrorConstants.ERROR_LIST_CAMERAS, event.exception);
}
if (event.descriptors.size()>0) {
cameras=event.descriptors;
AbstractCameraActivity.BUS.post(
new ControllerReadyEvent(this, cameras.size()));
}
else {
AbstractCameraActivity.BUS.post(new NoSuchCameraEvent());
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode=ThreadMode.MAIN)
public void onEventMainThread(CameraEngine.OpenedEvent event) {
if (event.exception!=null) {
// handled at fragment level
}
else {
CameraDescriptor camera=cameras.get(currentCamera);
CameraView cv=getPreview(camera);
if (cv!=null) {
boolean shouldSwapPreviewDimensions=
cv
.getContext()
.getResources()
.getConfiguration().orientation==
Configuration.ORIENTATION_PORTRAIT;
Size virtualPreviewSize=session.getPreviewSize();
if (shouldSwapPreviewDimensions) {
virtualPreviewSize=
new Size(session.getPreviewSize().getHeight(),
session.getPreviewSize().getWidth());
}
cv.setPreviewSize(virtualPreviewSize);
}
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode=ThreadMode.MAIN)
public void onEventMainThread(CameraEngine.ClosedEvent event) {
if (event.exception!=null) {
postError(ErrorConstants.ERROR_CLOSE_CAMERA, event.exception);
AbstractCameraActivity.BUS.post(new NoSuchCameraEvent());
}
else {
if (switchPending) {
switchPending=false;
currentCamera=getNextCameraIndex();
getPreview(cameras.get(currentCamera))
.setVisibility(View.VISIBLE);
open();
}
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode=ThreadMode.MAIN)
public void onEventMainThread(
CameraEngine.OrientationChangedEvent event) {
if (engine!=null) {
engine.handleOrientationChange(session, event);
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode=ThreadMode.MAIN)
public void onEventMainThread(CameraEngine.DeepImpactEvent event) {
postError(ErrorConstants.ERROR_MISC, event.exception);
}
public void postError(int resultCode, Throwable e) {
if (onError!=null) {
Bundle resultData=new Bundle();
StringWriter sw=new StringWriter();
e.printStackTrace(new PrintWriter(sw));
resultData
.putString(ErrorConstants.RESULT_STACK_TRACE,
sw.toString());
onError.send(resultCode, resultData);
}
}
/**
* Raised if there are no available cameras on this
* device. Consider using uses-feature elements in the
* manifest, so your app only runs on devices that have
* a camera, if you need a camera.
*/
public static class NoSuchCameraEvent {
}
/**
* Event raised when the controller has its cameras
* and is ready for use. Clients should then turn
* around and call setCameraViews() to complete the process
* and start showing the first preview.
*/
public static class ControllerReadyEvent {
final private int cameraCount;
final private CameraController ctlr;
private ControllerReadyEvent(CameraController ctlr,
int cameraCount) {
this.cameraCount=cameraCount;
this.ctlr=ctlr;
}
public int getNumberOfCameras() {
return (cameraCount);
}
public boolean isEventForController(CameraController ctlr) {
return (this.ctlr==ctlr);
}
}
/**
* Event raised when the controller has its cameras
* and is ready for use. Clients should then turn
* around and call setCameraViews() to complete the process
* and start showing the first preview.
*/
public static class ControllerDestroyedEvent {
private final CameraController ctlr;
ControllerDestroyedEvent(CameraController ctlr) {
this.ctlr=ctlr;
}
public CameraController getDestroyedController() {
return (ctlr);
}
}
public static class PictureCaptureStartEvent {
}
}