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