/*
* Copyright 2014 Bevbot LLC <info@bevbot.com>
*
* This file is part of the Kegtab package from the Kegbot project. For
* more information on Kegtab or Kegbot, see <http://kegbot.org/>.
*
* Kegtab is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, version 2.
*
* Kegtab 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with Kegtab. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kegbot.app.camera;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Fragment;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.ShutterCallback;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import org.kegbot.app.R;
import org.kegbot.app.config.AppConfiguration;
import org.kegbot.app.event.PictureDiscardedEvent;
import org.kegbot.app.event.PictureTakenEvent;
import org.kegbot.core.KegbotCore;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class CameraFragment extends Fragment {
private static final String TAG = CameraFragment.class.getSimpleName();
private static final long CAMERA_SETUP_DELAY_MILLIS = 200;
AppConfiguration mConfig;
private Preview mPreview;
private Camera mCamera;
private int mNumberOfCameras;
private int mRotation = 0;
private Button mPictureButton;
private Button mDiscardButton;
private Button mRetakeButton;
private ViewGroup mPostButtons;
private int mPictureSeconds = 0;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private SoundPool mSoundPool;
private int mCountdownBeepSoundId;
private int mCountdownBeepSoundLastId;
private boolean mPlaySounds = true;
private String mLastFilename = "";
private enum State {
INITIAL, IN_PROGRESS, TAKEN, COMPLETE, DISABLED;
}
private State mState = State.INITIAL;
// The first rear facing camera
int mDefaultCameraId;
private final Runnable PICTURE_COUNTDOWN_RUNNABLE = new Runnable() {
@Override
public void run() {
if (mPictureSeconds > 0) {
mPictureButton.setClickable(false);
mPictureButton.setText(mPictureSeconds + " ...");
mPictureSeconds -= 1;
if (mPlaySounds) {
mSoundPool.play(mCountdownBeepSoundId, 1, 1, 1, 0, 1);
}
mHandler.postDelayed(PICTURE_COUNTDOWN_RUNNABLE, 1000);
} else {
if (mPlaySounds) {
mSoundPool.play(mCountdownBeepSoundLastId, 1, 1, 1, 0, 1);
}
takePicture();
}
}
};
private final Runnable CAMERA_SETUP_RUNNABLE = new Runnable() {
@Override
public void run() {
doCameraSetup();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
KegbotCore core = KegbotCore.getInstance(getActivity());
mConfig = core.getConfiguration();
mNumberOfCameras = Camera.getNumberOfCameras();
// Find the ID of the default camera
CameraInfo cameraInfo = new CameraInfo();
for (int i = 0; i < mNumberOfCameras; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
mDefaultCameraId = i;
}
}
mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 100);
mCountdownBeepSoundId = mSoundPool.load(getActivity(), R.raw.countdown_beep, 1);
mCountdownBeepSoundLastId = mSoundPool.load(getActivity(), R.raw.countdown_beep_last, 1);
}
@Override
public void onDestroy() {
mSoundPool.release();
mSoundPool = null;
super.onDestroy();
}
public void setEnabled(boolean enabled) {
mPictureButton.setEnabled(enabled);
mRetakeButton.setEnabled(enabled);
mDiscardButton.setEnabled(enabled);
if (!enabled) {
cancelPendingPicture();
}
}
private void updateState(State newState) {
mState = newState;
switch (mState) {
case INITIAL:
mCamera.startPreview();
mPostButtons.setVisibility(View.GONE);
mPictureButton.setVisibility(View.VISIBLE);
mPictureButton.setClickable(true);
mPictureButton.setText("Take Picture");
setEnabled(true);
break;
case IN_PROGRESS:
mPostButtons.setVisibility(View.GONE);
mPictureButton.setVisibility(View.VISIBLE);
break;
case TAKEN:
//mPreview.stopCameraPreview();
//mCamera.stopPreview();
mPostButtons.setVisibility(View.VISIBLE);
mPictureButton.setVisibility(View.GONE);
break;
case COMPLETE:
mPostButtons.setVisibility(View.GONE);
mPictureButton.setVisibility(View.VISIBLE);
mPictureButton.setText("Pour Complete");
setEnabled(false);
break;
case DISABLED:
mPostButtons.setVisibility(View.GONE);
mPictureButton.setVisibility(View.VISIBLE);
setEnabled(false);
break;
}
}
public void takePicture() {
final ShutterCallback shutterCallback = new ShutterCallback() {
@Override
public void onShutter() {
Log.d(TAG, "camera shutter");
}
};
final PictureCallback jpegCallback = new PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Log.d(TAG, "camera jpeg: " + data);
new ImageSaveTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, data);
}
};
if (mCamera == null || mState == State.DISABLED) {
Log.d(TAG, "Not taking picture: disabled.");
}
doTakePicture(shutterCallback, null, jpegCallback);
}
private class ImageSaveTask extends AsyncTask<byte[], Void, String> {
@Override
protected String doInBackground(byte[]... params) {
byte[] data = params[0];
final int rotation = getDisplayOrientation();
final Bitmap bitmap = decodeAndRotateFromJpeg(data, rotation);
final File imageDir = getActivity().getCacheDir();
final Date pourDate = new Date(System.currentTimeMillis());
final SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US);
final String baseName = "pour-" + format.format(pourDate);
File imageFile = new File(imageDir, baseName + ".jpg");
int ext = 2;
while (imageFile.exists()) {
imageFile = new File(imageDir, baseName + "-" + (ext++) + ".jpg");
}
try {
FileOutputStream fos = new FileOutputStream(imageFile);
bitmap.compress(CompressFormat.JPEG, 100, fos);
fos.close();
} catch (IOException e) {
Log.w(TAG, "Could not save image.", e);
return null;
} finally {
bitmap.recycle();
}
final String savedImage = imageFile.getAbsolutePath();
Log.i(TAG, "Saved pour image: " + savedImage);
// Make file readable so LocalBackend can export it to the gallery.
imageFile.setReadable(true, false);
return savedImage;
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
mLastFilename = result;
updateState(State.TAKEN);
final Activity activity = getActivity();
if (activity != null) {
KegbotCore.getInstance(activity).postEvent(new PictureTakenEvent(result));
}
}
private Bitmap decodeAndRotateFromJpeg(byte[] data, int rotation) {
final Bitmap origBitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (rotation != 0) {
Log.w(TAG, "ImageSaveTask: rotation=" + rotation);
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
final Bitmap newBitmap = Bitmap.createBitmap(origBitmap, 0, 0, origBitmap.getWidth(),
origBitmap.getHeight(), matrix, true);
origBitmap.recycle();
return newBitmap;
}
return origBitmap;
}
}
private void doTakePicture(final ShutterCallback shutter, final PictureCallback raw,
final PictureCallback jpeg) {
if (mCamera == null || mState == State.DISABLED) {
Log.wtf(TAG, "doTakePicture called in disabled state.");
return;
}
if (Camera.Parameters.FOCUS_MODE_AUTO.equals(mCamera.getParameters().getFocusMode())) {
Log.d(TAG, "Taking picture with autofocus.");
mCamera.cancelAutoFocus();
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
mCamera.takePicture(shutter, raw, jpeg);
}
});
} else {
Log.d(TAG, "Taking picture without autofocus.");
mCamera.takePicture(shutter, raw, jpeg);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.camera_fragment_layout, container, false);
mPreview = (Preview) view.findViewById(R.id.cameraPreview);
mPictureButton = ((Button) view.findViewById(R.id.cameraTakePictureButton));
mPostButtons = (ViewGroup) view.findViewById(R.id.cameraPostPictureButtons);
mPictureButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
schedulePicture();
}
});
mDiscardButton = (Button) view.findViewById(R.id.cameraDiscardPictureButton);
mDiscardButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mState == State.DISABLED) {
// Paranoia; should not be reachable.
Log.d(TAG, "Skipping discard: disabled.");
return;
}
discardLastPicture();
mCamera.startPreview();
updateState(State.INITIAL);
}
});
mRetakeButton = (Button) view.findViewById(R.id.cameraTakeAnotherButton);
mRetakeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mState == State.DISABLED) {
// Paranoia; should not be reachable.
Log.d(TAG, "Skipping retake: disabled.");
return;
}
discardLastPicture();
mCamera.startPreview();
updateState(State.INITIAL);
schedulePicture();
}
});
return view;
}
public String getLastFilename() {
return mLastFilename;
}
private void discardLastPicture() {
KegbotCore.getInstance(getActivity()).postEvent(new PictureDiscardedEvent(mLastFilename));
mLastFilename = "";
//mPreview.startCameraPreview();
}
public void schedulePicture() {
mPictureSeconds = 3;
mHandler.removeCallbacks(PICTURE_COUNTDOWN_RUNNABLE);
mHandler.post(PICTURE_COUNTDOWN_RUNNABLE);
}
public void cancelPendingPicture() {
mHandler.removeCallbacks(PICTURE_COUNTDOWN_RUNNABLE);
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume()");
mPlaySounds = mConfig.getEnableCameraSounds();
mHandler.postDelayed(CAMERA_SETUP_RUNNABLE, CAMERA_SETUP_DELAY_MILLIS);
}
private void doCameraSetup() {
try {
mCamera = Camera.open(mDefaultCameraId);
} catch (Exception e) {
Log.w(TAG, "Error opening camera: %s" + e, e);
mCamera = null;
updateState(State.DISABLED);
return;
}
enableShutterSound(mPlaySounds, mDefaultCameraId, mCamera);
setCameraDisplayOrientation(getActivity(), mDefaultCameraId, mCamera);
mPreview.setCamera(mCamera);
updateState(State.INITIAL);
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause()");
cancelPendingPicture();
mHandler.removeCallbacks(CAMERA_SETUP_RUNNABLE);
if (mCamera != null) {
mPreview.setCamera(null);
mCamera.release();
mCamera = null;
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public void enableShutterSound(boolean enable, int cameraId, Camera camera) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (info.canDisableShutterSound) {
camera.enableShutterSound(enable);
}
}
}
public void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {
Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
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;
}
Log.d(TAG, "setCameraDisplayOrientation: degrees=" + degrees);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
mRotation = result;
camera.setDisplayOrientation(result);
}
public int getDisplayOrientation() {
return mRotation;
}
}