/**
* Copyright (c) 2012 Vinayak Solutions Private Limited
* See the file license.txt for copying permission.
*/
package com.vinsol.expensetracker.cameraservice;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.Size;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.StatFs;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import com.flurry.android.FlurryAgent;
import com.vinsol.expensetracker.Constants;
import com.vinsol.expensetracker.ExpenseTrackerApplication;
import com.vinsol.expensetracker.R;
import com.vinsol.expensetracker.helpers.SharedPreferencesHelper;
import com.vinsol.expensetracker.utils.Log;
public class Camera extends Activity implements View.OnClickListener, ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback {
private static final int FIRST_TIME_INIT = 2;
private static final int NOT_FOUND = -1;
private static final int RESTART_PREVIEW = 3;
private static final int CLEAR_SCREEN_DELAY = 4;
private static final int SET_CAMERA_PARAMETERS_WHEN_IDLE = 5;
private static final String KEY_PICTURE_SIZE = "pref_camera_picturesize_key";
// The subset of parameters we need to update in setCameraParameters().
private static final int UPDATE_PARAM_INITIALIZE = 1;
private static final int UPDATE_PARAM_PREFERENCE = 4;
private static final int UPDATE_PARAM_ALL = -1;
private static final int ORIENTATION_HYSTERESIS = 5;
// When setCameraParametersWhenIdle() is called, we accumulate the subsets
// needed to be updated in mUpdateSet.
private int mUpdateSet;
// The brightness settings used when it is set to automatic in the system.
// The reason why it is set to 0.7 is just because 1.0 is too bright.
private static final float DEFAULT_CAMERA_BRIGHTNESS = 0.7f;
private static final int SCREEN_DELAY = 2 * 60 * 1000;
private Parameters mParameters;
private OrientationEventListener mOrientationListener;
private int mLastOrientation = 0;
private SharedPreferences mPreferences;
private static final int IDLE = 1;
private static final int IMAGE_DISPLAYED = 1000;
private static final int SNAPSHOT_IN_PROGRESS = 2;
private int mStatus = IDLE;
private android.hardware.Camera mCameraDevice;
private ContentProviderClient mMediaProviderClient;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder = null;
private ShutterButton mShutterButton;
private FocusRectangle mFocusRectangle;
private boolean mStartPreviewFail = false;
private static final int NO_STORAGE_ERROR = -1;
private static final int CANNOT_STAT_ERROR = -2;
private Uri mSaveUri;
private ImageCapture mImageCapture = null;
private boolean mPreviewing;
private boolean mPausing;
private boolean mFirstTimeInitialized;
private static final int FOCUS_NOT_STARTED = 0;
private static final int FOCUSING = 1;
private static final int FOCUSING_SNAP_ON_FINISH = 2;
private static final int FOCUS_SUCCESS = 3;
private static final int FOCUS_FAIL = 4;
private int mFocusState = FOCUS_NOT_STARTED;
private ContentResolver mContentResolver;
private final ShutterCallback mShutterCallback = new ShutterCallback();
private final PostViewPictureCallback mPostViewPictureCallback = new PostViewPictureCallback();
private final RawPictureCallback mRawPictureCallback = new RawPictureCallback();
private final AutoFocusCallback mAutoFocusCallback = new AutoFocusCallback();
// Use the ErrorCallback to capture the crash count
// on the mediaserver
private final ErrorCallback mErrorCallback = new ErrorCallback();
private long mFocusStartTime;
private long mFocusCallbackTime;
private long mCaptureStartTime;
private long mShutterCallbackTime;
private long mPostViewPictureCallbackTime;
private long mRawPictureCallbackTime;
private long mJpegPictureCallbackTime;
private int mPicturesRemaining;
private int mOrientationCompensation;
private OnScreenHint mStorageHint;
// These latency time are for the CameraLatency test.
public long mAutoFocusTime;
public long mShutterLag;
public long mShutterToPictureDisplayedTime;
public long mPictureDisplayedToJpegCallbackTime;
public long mJpegCallbackFinishTime;
// Add for test
public static boolean mMediaServerDied = false;
@Override
protected void onStart() {
super.onStart();
FlurryAgent.onStartSession(this, getString(R.string.flurry_key));
}
private final Handler mHandler = new MainHandler();
/**
* This Handler is used to post message back onto the main thread of the
* application
*/
private class MainHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case RESTART_PREVIEW: {
restartPreview();
if (mJpegPictureCallbackTime != 0) {
long now = System.currentTimeMillis();
mJpegCallbackFinishTime = now - mJpegPictureCallbackTime;
mJpegPictureCallbackTime = 0;
}
break;
}
case CLEAR_SCREEN_DELAY: {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
break;
}
case FIRST_TIME_INIT: {
initializeFirstTime();
break;
}
case SET_CAMERA_PARAMETERS_WHEN_IDLE: {
setCameraParametersWhenIdle(0);
break;
}
}
}
}
private void checkStorage() {
calculatePicturesRemaining();
Log.d("%%%%%%%%%%%%%%%%%%%%%%%%b mPicturesRemaining "+mPicturesRemaining);
updateStorageHint(mPicturesRemaining);
}
private void updateStorageHint(int remaining) {
String noStorageText = null;
if (remaining == NO_STORAGE_ERROR) {
String state = Environment.getExternalStorageState();
if (state == Environment.MEDIA_CHECKING) {
noStorageText = getString(R.string.preparing_sd);
} else {
noStorageText = getString(R.string.no_storage);
}
} else if (remaining < 1) {
noStorageText = getString(R.string.not_enough_space);
}
if (noStorageText != null) {
if (mStorageHint == null) {
mStorageHint = OnScreenHint.makeText(this, noStorageText);
} else {
mStorageHint.setText(noStorageText);
}
mStorageHint.show();
} else if (mStorageHint != null) {
mStorageHint.cancel();
mStorageHint = null;
}
}
private void keepMediaProviderInstance() {
// We want to keep a reference to MediaProvider in camera's lifecycle.
// TODO: Utilize mMediaProviderClient instance to replace
// ContentResolver calls.
if (mMediaProviderClient == null) {
mMediaProviderClient = getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY);
}
}
private int roundOrientation(int orientation) {
boolean changeOrientation = false;
if (mLastOrientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
changeOrientation = true;
} else {
int dist = Math.abs(orientation - mLastOrientation);
dist = Math.min( dist, 360 - dist );
changeOrientation = ( dist >= 45 + ORIENTATION_HYSTERESIS );
}
if (changeOrientation) {
return ((orientation + 45) / 90 * 90) % 360;
}
return mLastOrientation;
}
// Snapshots can only be taken after this is called. It should be called
// once only. We could have done these things in onCreate() but we want to
// make preview screen appear as soon as possible.
private void initializeFirstTime() {
if (mFirstTimeInitialized) return;
// Create orientation listenter. This should be done first because it
// takes some time to get first orientation.
mOrientationListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int orientation) {
// We keep the last known orientation. So if the user
// first orient the camera then point the camera to
if (orientation == ORIENTATION_UNKNOWN) return;
mLastOrientation = roundOrientation(orientation);
int orientationCompensation = mLastOrientation + getDisplayRotation();
if (mOrientationCompensation != orientationCompensation) {
mOrientationCompensation = orientationCompensation;
setOrientationIndicator(mOrientationCompensation);
}
}
};
mOrientationListener.enable();
keepMediaProviderInstance();
checkStorage();
// Initialize last picture button.
mContentResolver = getContentResolver();
// Initialize shutter button.
mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
mShutterButton.setOnShutterButtonListener(this);
mShutterButton.setVisibility(View.VISIBLE);
mFocusRectangle = (FocusRectangle) findViewById(R.id.focus_rectangle);
updateFocusIndicator();
initializeScreenBrightness();
installIntentFilter();
mFirstTimeInitialized = true;
}
// If the activity is paused and resumed, this method will be called in
// onResume.
private void initializeSecondTime() {
// Start orientation listener as soon as possible because it takes
// some time to get first orientation.
mOrientationListener.enable();
// Start location update if needed.
installIntentFilter();
keepMediaProviderInstance();
checkStorage();
}
private class ShutterCallback implements android.hardware.Camera.ShutterCallback {
public void onShutter() {
mShutterCallbackTime = System.currentTimeMillis();
mShutterLag = mShutterCallbackTime - mCaptureStartTime;
clearFocusState();
}
}
private class PostViewPictureCallback implements PictureCallback {
public void onPictureTaken(byte [] data, android.hardware.Camera camera) {
mPostViewPictureCallbackTime = System.currentTimeMillis();
}
}
private class RawPictureCallback implements PictureCallback {
public void onPictureTaken(byte [] rawData, android.hardware.Camera camera) {
mRawPictureCallbackTime = System.currentTimeMillis();
}
}
private class JpegPictureCallback implements PictureCallback {
public void onPictureTaken( byte [] jpegData, android.hardware.Camera camera) {
if (mPausing) {return;}
mJpegPictureCallbackTime = System.currentTimeMillis();
// If postview callback has arrived, the captured image is displayed
// in postview callback. If not, the captured image is displayed in
// raw picture callback.
if (mPostViewPictureCallbackTime != 0) {
mShutterToPictureDisplayedTime = mPostViewPictureCallbackTime - mShutterCallbackTime;
mPictureDisplayedToJpegCallbackTime = mJpegPictureCallbackTime - mPostViewPictureCallbackTime;
} else {
mShutterToPictureDisplayedTime = mRawPictureCallbackTime - mShutterCallbackTime;
mPictureDisplayedToJpegCallbackTime = mJpegPictureCallbackTime - mRawPictureCallbackTime;
}
mImageCapture.storeImage(jpegData, camera);
// Calculate this in advance of each shot so we don't add to shutter
// latency. It's true that someone else could write to the SD card in
// the mean time and fill it, but that could have happened between the
// shutter press and saving the JPEG too.
calculatePicturesRemaining();
if (!mHandler.hasMessages(RESTART_PREVIEW)) {
long now = System.currentTimeMillis();
mJpegCallbackFinishTime = now - mJpegPictureCallbackTime;
mJpegPictureCallbackTime = 0;
}
}
}
private class AutoFocusCallback implements android.hardware.Camera.AutoFocusCallback {
public void onAutoFocus(boolean focused, android.hardware.Camera camera) {
mFocusCallbackTime = System.currentTimeMillis();
mAutoFocusTime = mFocusCallbackTime - mFocusStartTime;
if (mFocusState == FOCUSING_SNAP_ON_FINISH) {
// Take the picture no matter focus succeeds or fails. No need
// to play the AF sound if we're about to play the shutter
// sound.
if (focused) {
mFocusState = FOCUS_SUCCESS;
} else {
mFocusState = FOCUS_FAIL;
}
mImageCapture.onSnap();
} else if (mFocusState == FOCUSING) {
// User is half-pressing the focus key. Play the focus tone.
// Do not take the picture now.
if (focused) {
mFocusState = FOCUS_SUCCESS;
} else {
mFocusState = FOCUS_FAIL;
}
}
updateFocusIndicator();
}
}
private class ErrorCallback implements android.hardware.Camera.ErrorCallback {
public void onError(int error, android.hardware.Camera camera) {
if (error == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) {
mMediaServerDied = true;
}
}
}
private class ImageCapture {
byte[] mCaptureOnlyData;
// Returns the rotation degree in the jpeg header.
public void storeImage(byte[] data,android.hardware.Camera camera) {
mCaptureOnlyData = data;
showPostCaptureAlert();
}
/**
* Initiate the capture of an image.
*/
public void initiate() {
if (mCameraDevice == null) {
return;
}
capture();
}
public byte[] getLastCaptureData() {
return mCaptureOnlyData;
}
private void capture() {
mCaptureOnlyData = null;
// Set rotation.
mParameters.setRotation(mLastOrientation);
///XXX May b error here
mCameraDevice.setParameters(mParameters);
mCameraDevice.takePicture(mShutterCallback, mRawPictureCallback,mPostViewPictureCallback, new JpegPictureCallback());
mPreviewing = false;
}
public void onSnap() {
// If we are already in the middle of taking a snapshot then ignore.
if (mPausing || mStatus == SNAPSHOT_IN_PROGRESS) {return;}
mCaptureStartTime = System.currentTimeMillis();
mPostViewPictureCallbackTime = 0;
mStatus = SNAPSHOT_IN_PROGRESS;
mImageCapture.initiate();
findViewById(R.id.camera_progress_bar).setVisibility(View.VISIBLE);
findViewById(R.id.shutter_button).setVisibility(View.GONE);
findViewById(R.id.flash_button).setVisibility(View.GONE);
}
private void clearLastData() {
mCaptureOnlyData = null;
}
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.camera);
mSurfaceView = (SurfaceView) findViewById(R.id.camera_preview);
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
/*
* To reduce startup time, we start the preview in another thread.
* We make sure the preview is started at the end of onCreate.
*/
Thread startPreviewThread = new Thread(new Runnable() {
public void run() {
try {
mStartPreviewFail = false;
startPreview();
} catch (CameraHardwareException e) {
// In eng build, we throw the exception so that test tool
// can detect it and report it
if ("eng".equals(Build.TYPE)) {
throw new RuntimeException(e);
}
mStartPreviewFail = true;
}
}
});
startPreviewThread.start();
// don't set mSurfaceHolder here. We have it set ONLY within
// surfaceChanged / surfaceDestroyed, other parts of the code
// assume that when it is set, the surface is also set.
SurfaceHolder holder = mSurfaceView.getHolder();
holder.addCallback(this);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
setupCaptureParams();
LayoutInflater inflater = getLayoutInflater();
ViewGroup rootView = (ViewGroup) findViewById(R.id.camera);
inflater.inflate(R.layout.camera_control, rootView);
// Make sure preview is started.
try {
startPreviewThread.join();
if (mStartPreviewFail) {
showCameraErrorAndFinish();
return;
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
((CameraButton) findViewById(R.id.btn_cancel)).setOnClickListener(this);
((CameraButton) findViewById(R.id.btn_done)).setOnClickListener(this);
((CameraButton) findViewById(R.id.btn_retake)).setOnClickListener(this);
((CameraFlashButton) findViewById(R.id.flash_button)).setButtonCallback(flashCB);
}
private String getFlashParameter(int resId) {
switch (resId) {
case 0:
return Parameters.FLASH_MODE_AUTO;
case 1:
return Parameters.FLASH_MODE_OFF;
case 2:
return Parameters.FLASH_MODE_ON;
default:
return Parameters.FLASH_MODE_AUTO;
}
}
private void setOrientationIndicator(int degree) {
((CameraFlashButton) findViewById(R.id.flash_button)).setDegree(degree);
((CameraButton) findViewById(R.id.btn_cancel)).setDegree(degree);
((CameraButton) findViewById(R.id.btn_done)).setDegree(degree);
((CameraButton) findViewById(R.id.btn_retake)).setDegree(degree);
}
private CameraFlashButtonCBInterface flashCB = new CameraFlashButtonCBInterface() {
@Override
public void onClickListener(int item) {
updateFlashModeParameter();
}
};
@Override
public void onStop() {
super.onStop();
FlurryAgent.onEndSession(this);
if (mMediaProviderClient != null) {
mMediaProviderClient.release();
mMediaProviderClient = null;
}
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_retake:
hidePostCaptureAlert();
restartPreview();
break;
case R.id.btn_done:
finish();
break;
case R.id.btn_cancel:
doCancel();
}
}
private Bitmap makeBitmap(byte[] jpegData) {
try {
if(jpegData != null && jpegData.length > 0) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options);
if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {return null;}
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPurgeable = true;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options);
}
} catch (OutOfMemoryError ex) {
ex.printStackTrace();
}
return null;
}
private void doAttach() {
if (mPausing) {
return;
}
byte[] data = mImageCapture.getLastCaptureData();
if(mSaveUri != null && data != null) {
Bitmap bitmap = createCaptureBitmap(data);
OutputStream outputStream = null;
try {
outputStream = mContentResolver.openOutputStream(mSaveUri);
if(bitmap != null) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream);
outputStream.close();
bitmap.recycle();
setResult(RESULT_OK);
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
closeSilently(outputStream);
}
} else {
setResult(RESULT_CANCELED);
}
}
private Bitmap rotate(Bitmap b, int degrees) {
if (degrees != 0 && b != null) {
Matrix m = new Matrix();
if (degrees != 0) {
m.postRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
}
try {
Bitmap b2 = Bitmap.createBitmap( b, 0, 0, b.getWidth(), b.getHeight(), m, true);
if (b != b2) {
b.recycle();
b = b2;
}
} catch (OutOfMemoryError ex) {
// We have no memory to rotate. Return the original bitmap.
}
}
return b;
}
private int getDisplayRotation() {
int rotation = getWindowManager().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 boolean saveDataToFile(String filePath, byte[] data) {
FileOutputStream f = null;
try {
f = new FileOutputStream(filePath);
f.write(data);
} catch (IOException e) {
return false;
} finally {
closeSilently(f);
}
return true;
}
private int getExifOrientation(String filepath) {
int degree = 0;
ExifInterface exif = null;
try {
exif = new ExifInterface(filepath);
} catch (IOException ex) {
ex.printStackTrace();
}
if (exif != null) {
int orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION, -1);
if (orientation != -1) {
// We only recognize a subset of orientation tag values.
switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
}
}
return degree;
}
private Bitmap createCaptureBitmap(byte[] data) {
String filepath = mSaveUri.getPath();
int degree = 0;
if (saveDataToFile(filepath, data)) {
degree = getExifOrientation(filepath);
new File(filepath).delete();
}
Bitmap bitmap = makeBitmap(data);
bitmap = rotate(bitmap, degree + getDisplayRotation());
return bitmap;
}
private void closeSilently(Closeable c) {
if (c == null) return;
try {
c.close();
} catch (Throwable t) {
// do nothing
}
}
private void doCancel() {
setResult(RESULT_CANCELED, new Intent());
finish();
}
@Override
public void onShutterButtonFocus(ShutterButton button, boolean pressed) {
if (mPausing) {
return;
}
switch (button.getId()) {
case R.id.shutter_button:
doFocus(pressed);
break;
}
}
@Override
public void onShutterButtonClick(ShutterButton button) {
if (mPausing) {
return;
}
switch (button.getId()) {
case R.id.shutter_button:
doSnap();
break;
}
}
private void installIntentFilter() {
// install an intent filter to receive SD card related events.
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING);
intentFilter.addDataScheme("file");
}
private void initializeScreenBrightness() {
Window win = getWindow();
// Overright the brightness settings if it is automatic
int mode = Settings.System.getInt(
getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
if (mode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
WindowManager.LayoutParams winParams = win.getAttributes();
winParams.screenBrightness = DEFAULT_CAMERA_BRIGHTNESS;
win.setAttributes(winParams);
}
}
@Override
protected void onResume() {
super.onResume();
mPausing = false;
mJpegPictureCallbackTime = 0;
mImageCapture = new ImageCapture();
// Start the preview if it is not started.
if (!mPreviewing && !mStartPreviewFail) {
try {
startPreview();
} catch (CameraHardwareException e) {
showCameraErrorAndFinish();
return;
}
}
if (mSurfaceHolder != null) {
// If first time initialization is not finished, put it in the
// message queue.
if (!mFirstTimeInitialized) {
mHandler.sendEmptyMessage(FIRST_TIME_INIT);
} else {
initializeSecondTime();
}
}
keepScreenOnAwhile();
}
@Override
protected void onPause() {
mPausing = true;
stopPreview();
// Close the camera now because other activities may need to use it.
closeCamera();
resetScreenOn();
if (mFirstTimeInitialized) {
mOrientationListener.disable();
hidePostCaptureAlert();
}
// If we are in an image capture intent and has taken
// a picture, we just clear it in onPause.
mImageCapture.clearLastData();
mImageCapture = null;
if (mStorageHint != null) {
mStorageHint.cancel();
mStorageHint = null;
}
// Remove the messages in the event queue.
mHandler.removeMessages(RESTART_PREVIEW);
mHandler.removeMessages(FIRST_TIME_INIT);
super.onPause();
}
private boolean canTakePicture() {
return isCameraIdle() && mPreviewing && (mPicturesRemaining > 0);
}
private void autoFocus() {
// Initiate autofocus only when preview is started and snapshot is not
// in progress.
if (canTakePicture()) {
mFocusStartTime = System.currentTimeMillis();
mFocusState = FOCUSING;
updateFocusIndicator();
mCameraDevice.autoFocus(mAutoFocusCallback);
}
}
private void cancelAutoFocus() {
// User releases half-pressed focus key.
if (mFocusState == FOCUSING || mFocusState == FOCUS_SUCCESS || mFocusState == FOCUS_FAIL) {
mCameraDevice.cancelAutoFocus();
}
if (mFocusState != FOCUSING_SNAP_ON_FINISH) {
clearFocusState();
}
}
private void clearFocusState() {
mFocusState = FOCUS_NOT_STARTED;
updateFocusIndicator();
}
private void updateFocusIndicator() {
if (mFocusRectangle == null) return;
if (mFocusState == FOCUSING || mFocusState == FOCUSING_SNAP_ON_FINISH) {
mFocusRectangle.showStart();
} else if (mFocusState == FOCUS_SUCCESS) {
mFocusRectangle.showSuccess();
} else if (mFocusState == FOCUS_FAIL) {
mFocusRectangle.showFail();
} else {
mFocusRectangle.clear();
}
}
@Override
public void onBackPressed() {
if (!isCameraIdle()) {
// ignore backs while we're taking a picture
return;
} else {
doCancel();
super.onBackPressed();
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_FOCUS:
if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
doFocus(true);
}
return true;
case KeyEvent.KEYCODE_CAMERA:
if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
doSnap();
}
return true;
case KeyEvent.KEYCODE_DPAD_CENTER:
// If we get a dpad center event without any focused view, move
// the focus to the shutter button and press it.
if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
// Start auto-focus immediately to reduce shutter lag. After
// the shutter button gets the focus, doFocus() will be
// called again but it is fine.
doFocus(true);
if (mShutterButton.isInTouchMode()) {
mShutterButton.requestFocusFromTouch();
} else {
mShutterButton.requestFocus();
}
mShutterButton.setPressed(true);
}
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_FOCUS:
if (mFirstTimeInitialized) {
doFocus(false);
}
return true;
}
return super.onKeyUp(keyCode, event);
}
private void doSnap() {
// If the user has half-pressed the shutter and focus is completed, we
// can take the photo right away. If the focus mode is infinity, we can
// also take the photo.
if ((mFocusState == FOCUS_SUCCESS
|| mFocusState == FOCUS_FAIL)) {
mImageCapture.onSnap();
} else if (mFocusState == FOCUSING) {
// Half pressing the shutter (i.e. the focus button event) will
// already have requested AF for us, so just request capture on
// focus here.
mFocusState = FOCUSING_SNAP_ON_FINISH;
} else if (mFocusState == FOCUS_NOT_STARTED) {
// Focus key down event is dropped for some reasons. Just ignore.
}
}
private void doFocus(boolean pressed) {
// Do the focus if the mode is not infinity.
if (pressed) { // Focus key down.
autoFocus();
} else { // Focus key up.
cancelAutoFocus();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// Make sure we have a surface in the holder before proceeding.
if (holder.getSurface() == null) {return;}
// We need to save the holder for later use, even when the mCameraDevice
// is null. This could happen if onResume() is invoked after this
// function.
mSurfaceHolder = holder;
// The mCameraDevice will be null if it fails to connect to the camera
// hardware. In this case we will show a dialog and then finish the
// activity, so it's OK to ignore it.
if (mCameraDevice == null) return;
// Sometimes surfaceChanged is called after onPause or before onResume.
// Ignore it.
if (mPausing || isFinishing()) return;
if (mPreviewing && holder.isCreating()) {
// Set preview display if the surface is being created and preview
// was already started. That means preview display was set to null
// and we need to set it now.
setPreviewDisplay(holder);
} else {
// 1. Restart the preview if the size of surface was changed. The
// framework may not support changing preview display on the fly.
// 2. Start the preview now if surface was destroyed and preview
// stopped.
restartPreview();
}
// If first time initialization is not finished, send a message to do
// it later. We want to finish surfaceChanged as soon as possible to let
// user see preview first.
if (!mFirstTimeInitialized) {
mHandler.sendEmptyMessage(FIRST_TIME_INIT);
} else {
initializeSecondTime();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopPreview();
mSurfaceHolder = null;
}
private void closeCamera() {
if (mCameraDevice != null) {
CameraHolder.instance().release();
mCameraDevice.setZoomChangeListener(null);
mCameraDevice = null;
mPreviewing = false;
}
}
private void ensureCameraDevice() throws CameraHardwareException {
if (mCameraDevice == null) {
mCameraDevice = CameraHolder.instance().open();
}
}
private void showCameraErrorAndFinish() {
Resources ress = getResources();
showFatalErrorAndFinish(ress.getString(R.string.camera_error_title),ress.getString(R.string.cannot_connect_camera));
}
private void restartPreview() {
try {
startPreview();
} catch (CameraHardwareException e) {
showCameraErrorAndFinish();
return;
}
}
private void showFatalErrorAndFinish(
String title, String message) {
DialogInterface.OnClickListener buttonListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
};
new AlertDialog.Builder(this)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(title)
.setMessage(message)
.setNeutralButton(R.string.details_ok, buttonListener)
.show();
}
private void setPreviewDisplay(SurfaceHolder holder) {
try {
mCameraDevice.setPreviewDisplay(holder);
} catch (Throwable ex) {
closeCamera();
throw new RuntimeException("setPreviewDisplay failed", ex);
}
}
private void startPreview() throws CameraHardwareException {
if (mPausing || isFinishing()) return;
ensureCameraDevice();
// If we're previewing already, stop the preview first (this will blank
// the screen).
if (mPreviewing) stopPreview();
setPreviewDisplay(mSurfaceHolder);
setCameraParameters(UPDATE_PARAM_ALL);
mCameraDevice.setErrorCallback(mErrorCallback);
try {
mCameraDevice.startPreview();
} catch (Throwable ex) {
closeCamera();
throw new RuntimeException("startPreview failed", ex);
}
mPreviewing = true;
mStatus = IDLE;
}
private void stopPreview() {
if (mCameraDevice != null && mPreviewing) {
mCameraDevice.stopPreview();
}
mPreviewing = false;
// If auto focus was in progress, it would have been canceled.
clearFocusState();
}
private Size getOptimalPreviewSize(List<Size> sizes, double targetRatio) {
final double ASPECT_TOLERANCE = 0.05;
if (sizes == null) return null;
Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
// Because of bugs of overlay and layout, we sometimes will try to
// layout the viewfinder in the portrait orientation and thus get the
// wrong size of mSurfaceView. When we change the preview size, the
// new overlay will be created before the old one closed, which causes
// an exception. For now, just get the screen size
Display display = getWindowManager().getDefaultDisplay();
int targetHeight = Math.min(display.getHeight(), display.getWidth());
if (targetHeight <= 0) {
// We don't know the size of SurefaceView, use screen height
WindowManager windowManager = (WindowManager)
getSystemService(Context.WINDOW_SERVICE);
targetHeight = windowManager.getDefaultDisplay().getHeight();
}
// Try to find an size match aspect ratio and size
for (Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
private void updateCameraParametersInitialize() {
// Reset preview frame rate to the maximum because it may be lowered by
// video camera application.
List<Integer> frameRates = mParameters.getSupportedPreviewFrameRates();
if (frameRates != null) {
Integer max = Collections.max(frameRates);
mParameters.setPreviewFrameRate(max);
}
}
private boolean setCameraPictureSize(String candidate, List<Size> supported, Parameters parameters) {
int index = candidate.indexOf('x');
if (index == NOT_FOUND) return false;
int width = Integer.parseInt(candidate.substring(0, index));
int height = Integer.parseInt(candidate.substring(index + 1));
for (Size size: supported) {
if (size.width == width && size.height == height) {
parameters.setPictureSize(width, height);
return true;
}
}
return false;
}
private void updateCameraParametersPreference() {
// Set picture size.
String pictureSize = mPreferences.getString(KEY_PICTURE_SIZE, null);
if (pictureSize == null) {
initialCameraPictureSize(this, mParameters);
} else {
List<Size> supported = mParameters.getSupportedPictureSizes();
setCameraPictureSize(pictureSize, supported, mParameters);
}
// Set the preview frame aspect ratio according to the picture size.
Size size = mParameters.getPictureSize();
PreviewFrameLayout frameLayout = (PreviewFrameLayout) findViewById(R.id.frame_layout);
frameLayout.setAspectRatio((double) size.width / size.height);
// Set a preview size that is closest to the viewfinder height and has
// the right aspect ratio.
List<Size> sizes = mParameters.getSupportedPreviewSizes();
Size optimalSize = getOptimalPreviewSize(sizes, (double) size.width / size.height);
if (optimalSize != null) {
mParameters.setPreviewSize(optimalSize.width, optimalSize.height);
}
mParameters.setJpegQuality(85);
updateFlashModeParameter();
}
private void updateFlashModeParameter() {
String flashMode = getFlashParameter(SharedPreferencesHelper.getSharedPreferences().getInt(getString(R.string.pref_key_flash_res_id), 0));
mParameters.setFlashMode(flashMode);
}
private void initialCameraPictureSize(Context context, Parameters parameters) {
List<Size> supported = parameters.getSupportedPictureSizes();
if (supported == null) return;
for (String candidate : context.getResources().getStringArray(R.array.pref_camera_picturesize_entryvalues)) {
if (setCameraPictureSize(candidate, supported, parameters)) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putString(KEY_PICTURE_SIZE, candidate);
editor.commit();
return;
}
}
}
// We separate the parameters into several subsets, so we can update only
// the subsets actually need updating. The PREFERENCE set needs extra
// locking because the preference can be changed from GLThread as well.
private void setCameraParameters(int updateSet) {
mParameters = mCameraDevice.getParameters();
if ((updateSet & UPDATE_PARAM_INITIALIZE) != 0) {
updateCameraParametersInitialize();
}
if ((updateSet & UPDATE_PARAM_PREFERENCE) != 0) {
synchronized (mPreferences) {
updateCameraParametersPreference();
}
}
//// XXX May be error here
mCameraDevice.setParameters(mParameters);
}
// If the Camera is idle, update the parameters immediately, otherwise
// accumulate them in mUpdateSet and update later.
private void setCameraParametersWhenIdle(int additionalUpdateSet) {
mUpdateSet |= additionalUpdateSet;
if (mCameraDevice == null) {
// We will update all the parameters when we open the device, so
// we don't need to do anything now.
mUpdateSet = 0;
return;
} else if (isCameraIdle()) {
setCameraParameters(mUpdateSet);
mUpdateSet = 0;
} else {
if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) {
mHandler.sendEmptyMessageDelayed(SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000);
}
}
}
private boolean isCameraIdle() {
return mStatus == IDLE && mFocusState == FOCUS_NOT_STARTED;
}
private void setupCaptureParams() {
Bundle extras = getIntent().getExtras();
if(extras.containsKey(Constants.KEY_FULL_SIZE_IMAGE_PATH)) {
mSaveUri = Uri.fromFile(new File(extras.getString(Constants.KEY_FULL_SIZE_IMAGE_PATH)));
} else {
Toast.makeText(getApplicationContext(), getString(R.string.camera_error_title), Toast.LENGTH_LONG).show();
finish();
}
}
private void showPostCaptureAlert() {
mStatus = IMAGE_DISPLAYED;
findViewById(R.id.camera_progress_bar).setVisibility(View.GONE);
doAttach();
mStatus = IDLE;
int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_cancel};
for (int id : pickIds) {
CameraButton button = (CameraButton) findViewById(id);
button.setVisibility(View.VISIBLE);
}
}
private void hidePostCaptureAlert() {
int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_cancel};
for (int id : pickIds) {
CameraButton button = (CameraButton) findViewById(id);
button.setVisibility(View.GONE);
}
findViewById(R.id.shutter_button).setVisibility(View.VISIBLE);
((CameraFlashButton) findViewById(R.id.flash_button)).setVisibility(View.VISIBLE);
}
private int calculatePicturesRemaining() {
try {
if (!hasStorage()) {
return NO_STORAGE_ERROR;
} else {
if(!ExpenseTrackerApplication.isInitialized){ExpenseTrackerApplication.Initialize();}
StatFs stat = new StatFs(Constants.DIRECTORY);
final int PICTURE_BYTES = 1500000;
mPicturesRemaining = (int)(((float) stat.getAvailableBlocks() * (float) stat.getBlockSize()) / PICTURE_BYTES);
}
} catch (Exception ex) {
mPicturesRemaining = CANNOT_STAT_ERROR;
}
return mPicturesRemaining;
}
private boolean hasStorage() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
boolean writable = checkFsWritable();
return writable;
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
private boolean checkFsWritable() {
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if(!ExpenseTrackerApplication.isInitialized){ExpenseTrackerApplication.Initialize();}
}
return new File(Constants.DIRECTORY).canWrite();
}
@Override
public void onUserInteraction() {
super.onUserInteraction();
keepScreenOnAwhile();
}
private void resetScreenOn() {
mHandler.removeMessages(CLEAR_SCREEN_DELAY);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void keepScreenOnAwhile() {
mHandler.removeMessages(CLEAR_SCREEN_DELAY);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
}
}
class FocusRectangle extends View {
public FocusRectangle(Context context, AttributeSet attrs) {
super(context, attrs);
}
private void setDrawable(int resid) {
setBackgroundDrawable(getResources().getDrawable(resid));
}
public void showStart() {
setDrawable(R.drawable.focus_focusing);
}
public void showSuccess() {
setDrawable(R.drawable.focus_focused);
}
public void showFail() {
setDrawable(R.drawable.focus_focus_failed);
}
public void clear() {
setBackgroundDrawable(null);
}
}