/* * 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; } }