com.fatelon.flyingphotobooth.fragments.CaptureFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.fatelon.flyingphotobooth.fragments.CaptureFragment.java

Source

/*
 * This file is part of Flying PhotoBooth.
 * 
 * Flying PhotoBooth 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, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Flying PhotoBooth 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 Flying PhotoBooth.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.fatelon.flyingphotobooth.fragments;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.fatelon.flyingphotobooth.LaunchActivity;
import com.fatelon.flyingphotobooth.MyPreferenceActivity;
import com.groundupworks.flyingphotobooth.R;
import com.fatelon.lib.photobooth.framework.BaseApplication;
import com.fatelon.lib.photobooth.helpers.CameraAudioHelper;
import com.fatelon.lib.photobooth.helpers.CameraHelper;
import com.fatelon.lib.photobooth.helpers.ImageHelper;
import com.fatelon.lib.photobooth.views.CenteredPreview;

import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Ui for the camera preview and capture screen.
 *
 * @author Benedict Lau
 */
public class CaptureFragment extends Fragment {

    //
    // Fragment bundle keys.
    //

    private static final String FRAGMENT_BUNDLE_KEY_CAMERA = "camera";

    /**
     * Manual trigger mode.
     */
    private static final int TRIGGER_MODE_MANUAL = 0;

    /**
     * Countdown trigger mode.
     */
    private static final int TRIGGER_MODE_COUNTDOWN = 1;

    /**
     * Burst trigger mode.
     */
    private static final int TRIGGER_MODE_BURST = 2;

    /**
     * Invalid camera id.
     */
    private static final int INVALID_CAMERA_ID = -1;

    /**
     * Delay between countdown steps in milliseconds.
     */
    private static final int COUNTDOWN_STEP_DELAY = 1000;

    /**
     * Delay between captures in burst mode.
     */
    private static final int BURST_DELAY = 1000;

    /**
     * Duration to display the review overlay.
     */
    private static final int REVIEW_OVERLAY_WAIT_DURATION = 2000;

    /**
     * Duration to display the review overlay in burst mode.
     */
    private static final int REVIEW_OVERLAY_WAIT_DURATION_BURST = 1000;

    /**
     * Threshold distance to recognize a swipe to remove gesture.
     */
    private static final float REVIEW_REMOVE_GESTURE_THRESHOLD = 100f;

    /**
     * The default captured Jpeg quality.
     */
    private static final int CAPTURED_JPEG_QUALITY = 100;

    /**
     * Flag to indicate whether the fragment is launched with preference to use the front-facing camera.
     */
    private boolean mUseFrontFacing = false;

    /**
     * The selected trigger mode.
     */
    private int mTriggerMode = TRIGGER_MODE_MANUAL;

    /**
     * Flag to track whether {@link #onPause()} is called.
     */
    private boolean mOnPauseCalled = false;

    /**
     * Number of cameras on the device.
     */
    private int mNumCameras = 0;

    /**
     * Id of the selected camera.
     */
    private int mCameraId = INVALID_CAMERA_ID;

    /**
     * Timer for scheduling tasks.
     */
    private Timer mTimer = null;

    /**
     * Flag to track whether capture sequence is running.
     */
    private boolean mIsCaptureSequenceRunning = false;

    /**
     * The selected camera.
     */
    private Camera mCamera = null;

    /**
     * Helper for audio feedback.
     */
    private CameraAudioHelper mCameraAudioHelper = null;

    /**
     * The preview display orientation.
     */
    private int mPreviewDisplayOrientation = CameraHelper.CAMERA_SCREEN_ORIENTATION_0;

    /**
     * Flag to indicate whether the camera image is reflected.
     */
    private boolean mIsReflected = false;

    /**
     * The total number of frames to capture.
     */
    private int mFramesTotal = 0;

    /**
     * The current frame index.
     */
    private int mFrameIndex = 0;

    /**
     * Jpeg frames in byte arrays. The first index is the frame count.
     */
    private byte[][] mFramesData = null;

    //
    // Key event handlers.
    //

    private LaunchActivity.BackPressedHandler mBackPressedHandler;

    private LaunchActivity.KeyEventHandler mKeyEventHandler;

    //
    // Views.
    //

    private TextView mTitle;

    private ImageButton mPreferencesButton;

    private ImageButton mSwitchButton;

    private CenteredPreview mPreview;

    private LinearLayout mCountdown;

    private TextView mCountdownThree;

    private TextView mCountdownTwo;

    private TextView mCountdownOne;

    private Button mStartButton;

    private LinearLayout mReviewOverlay;

    private TextView mReviewStatus;

    private ImageView mReviewImage;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        final Handler handler = new Handler(BaseApplication.getWorkerLooper());
        mCameraAudioHelper = new CameraAudioHelper(activity, R.raw.beep_once, handler);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        /*
         * Get params.
         */
        Bundle args = getArguments();
        mUseFrontFacing = args.getBoolean(FRAGMENT_BUNDLE_KEY_CAMERA);

        /*
         * Default to first camera available.
         */
        mNumCameras = Camera.getNumberOfCameras();
        if (mNumCameras > 0) {
            mCameraId = 0;
        }

        /*
         * Try to select camera based on preference.
         */
        int cameraPreference = CameraInfo.CAMERA_FACING_BACK;
        if (mUseFrontFacing) {
            cameraPreference = CameraInfo.CAMERA_FACING_FRONT;
        }

        CameraInfo cameraInfo = new CameraInfo();
        for (int cameraId = 0; cameraId < mNumCameras; cameraId++) {
            Camera.getCameraInfo(cameraId, cameraInfo);

            // Set flag to indicate whether the camera image is reflected.
            mIsReflected = isCameraImageReflected(cameraInfo);

            // Break on finding the preferred camera.
            if (cameraInfo.facing == cameraPreference) {
                mCameraId = cameraId;
                break;
            }
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        /*
         * Inflate views from XML.
         */
        View view = inflater.inflate(R.layout.fragment_capture, container, false);

        mTitle = (TextView) view.findViewById(R.id.title);
        mPreferencesButton = (ImageButton) view.findViewById(R.id.preferences_button);
        mSwitchButton = (ImageButton) view.findViewById(R.id.switch_button);
        mPreview = (CenteredPreview) view.findViewById(R.id.preview);
        mCountdown = (LinearLayout) view.findViewById(R.id.countdown);
        mCountdownThree = (TextView) view.findViewById(R.id.countdown_three);
        mCountdownTwo = (TextView) view.findViewById(R.id.countdown_two);
        mCountdownOne = (TextView) view.findViewById(R.id.countdown_one);
        mStartButton = (Button) view.findViewById(R.id.start_button);
        mReviewOverlay = (LinearLayout) view.findViewById(R.id.review_overlay);
        mReviewStatus = (TextView) view.findViewById(R.id.review_status);
        mReviewImage = (ImageView) view.findViewById(R.id.review_image);

        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        final LaunchActivity activity = (LaunchActivity) getActivity();
        SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(activity.getApplicationContext());

        /*
         * Reset frames.
         */
        String numPhotosPref = preferences.getString(getString(R.string.pref__number_of_photos_key),
                getString(R.string.pref__number_of_photos_default));
        mFramesTotal = Integer.parseInt(numPhotosPref);
        mFrameIndex = 0;
        mFramesData = new byte[mFramesTotal][];

        /*
         * Initialize and set key event handlers.
         */
        mBackPressedHandler = new LaunchActivity.BackPressedHandler() {

            @Override
            public boolean onBackPressed() {
                // If capture sequence is running, exit capture sequence on back pressed event.
                if (mIsCaptureSequenceRunning) {
                    activity.replaceFragment(CaptureFragment.newInstance(mUseFrontFacing), false, true);
                }

                return mIsCaptureSequenceRunning;
            }
        };
        activity.setBackPressedHandler(mBackPressedHandler);

        mKeyEventHandler = new LaunchActivity.KeyEventHandler() {
            @Override
            public boolean onKeyEvent(KeyEvent event) {
                final int keyCode = event.getKeyCode();
                if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
                    mStartButton.dispatchKeyEvent(event);
                    return true;
                }
                return false;
            }
        };
        activity.setKeyEventHandler(mKeyEventHandler);

        /*
         * Functionalize views.
         */
        mPreferencesButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent preferencesIntent = new Intent(activity, MyPreferenceActivity.class);
                startActivity(preferencesIntent);
            }
        });

        // Show switch button only if more than one camera is available.
        if (mNumCameras > 1) {
            mSwitchButton.setVisibility(View.VISIBLE);
            mSwitchButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Switch camera.
                    boolean useFrontFacing = false;
                    CameraInfo cameraInfo = new CameraInfo();
                    if (mCameraId != INVALID_CAMERA_ID) {
                        Camera.getCameraInfo(mCameraId, cameraInfo);
                        if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
                            useFrontFacing = true;
                        }

                        // Relaunch fragment with new camera.
                        activity.replaceFragment(CaptureFragment.newInstance(useFrontFacing), false, true);
                    }
                }
            });
        }

        // Get trigger mode preference.
        String triggerPref = preferences.getString(getString(R.string.pref__trigger_key),
                getString(R.string.pref__trigger_default));
        if (triggerPref.equals(getString(R.string.pref__trigger_countdown))) {
            mTriggerMode = TRIGGER_MODE_COUNTDOWN;
        } else if (triggerPref.equals(getString(R.string.pref__trigger_burst))) {
            mTriggerMode = TRIGGER_MODE_BURST;
        } else {
            mTriggerMode = TRIGGER_MODE_MANUAL;
        }

        // Configure title and start button text.
        mTitle.setText(String.format(getString(R.string.capture__title_frame), mFrameIndex + 1, mFramesTotal));
        if (mTriggerMode == TRIGGER_MODE_MANUAL) {
            // Update title.
            mStartButton.setText(getString(R.string.capture__start_manual_button_text));
        } else {
            mStartButton.setText(getString(R.string.capture__start_countdown_button_text));
        }

        // Configure start button behaviour.
        mStartButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onStartButtonPressedImpl();
            }
        });

        mStartButton.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                onStartButtonPressedImpl();
                return true;
            }
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        mCameraAudioHelper.prepare();
    }

    @Override
    public void onResume() {
        super.onResume();

        /*
         * Initialize timer for scheduling tasks.
         */
        mTimer = new Timer("countdownTimer");

        /*
         * Reload the fragment if resuming from onPause().
         */
        LaunchActivity activity = (LaunchActivity) getActivity();
        if (mOnPauseCalled) {
            // Relaunch fragment with new camera.
            activity.replaceFragment(CaptureFragment.newInstance(mUseFrontFacing), false, true);
        } else {
            if (mCameraId != INVALID_CAMERA_ID) {
                try {
                    mCamera = Camera.open(mCameraId);

                    /*
                     * Configure camera parameters.
                     */
                    Parameters params = mCamera.getParameters();

                    // Set auto white balance if supported.
                    List<String> whiteBalances = params.getSupportedWhiteBalance();
                    if (whiteBalances != null) {
                        for (String whiteBalance : whiteBalances) {
                            if (whiteBalance.equals(Camera.Parameters.WHITE_BALANCE_AUTO)) {
                                params.setWhiteBalance(whiteBalance);
                            }
                        }
                    }

                    // Set auto antibanding if supported.
                    List<String> antibandings = params.getSupportedAntibanding();
                    if (antibandings != null) {
                        for (String antibanding : antibandings) {
                            if (antibanding.equals(Camera.Parameters.ANTIBANDING_AUTO)) {
                                params.setAntibanding(antibanding);
                            }
                        }
                    }

                    // Set macro focus mode if supported.
                    List<String> focusModes = params.getSupportedFocusModes();
                    if (focusModes != null) {
                        for (String focusMode : focusModes) {
                            if (focusMode.equals(Camera.Parameters.FOCUS_MODE_MACRO)) {
                                params.setFocusMode(focusMode);
                            }
                        }
                    }

                    // Set quality for Jpeg capture.
                    params.setJpegQuality(CAPTURED_JPEG_QUALITY);

                    // Set optimal size for Jpeg capture.
                    Size pictureSize = CameraHelper.getOptimalPictureSize(params.getSupportedPreviewSizes(),
                            params.getSupportedPictureSizes(), ImageHelper.IMAGE_SIZE, ImageHelper.IMAGE_SIZE);
                    params.setPictureSize(pictureSize.width, pictureSize.height);

                    mCamera.setParameters(params);

                    /*
                     * Setup preview.
                     */
                    mPreviewDisplayOrientation = CameraHelper.getCameraScreenOrientation(activity, mCameraId);
                    mCamera.setDisplayOrientation(mPreviewDisplayOrientation);
                    mPreview.start(mCamera, pictureSize.width, pictureSize.height, mPreviewDisplayOrientation);
                } catch (RuntimeException e) {
                    String title = getString(R.string.capture__error_camera_dialog_title);
                    String message = getString(R.string.capture__error_camera_dialog_message_in_use);
                    activity.showDialogFragment(ErrorDialogFragment.newInstance(title, message));
                }
            } else {
                String title = getString(R.string.capture__error_camera_dialog_title);
                String message = getString(R.string.capture__error_camera_dialog_message_none);
                activity.showDialogFragment(ErrorDialogFragment.newInstance(title, message));
            }
        }
    }

    @Override
    public void onPause() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
            mIsCaptureSequenceRunning = false;
        }

        if (mCamera != null) {
            mPreview.stop();
            mCamera.release();
            mCamera = null;
        }

        mOnPauseCalled = true;

        // Save the camera preference.
        saveCameraPreference(getActivity());

        super.onPause();
    }

    @Override
    public void onStop() {
        mCameraAudioHelper.release();
        super.onStop();
    }

    //
    // Private inner classes.
    //

    /**
     * Callback when focus is ready for capture.
     */
    private class MyAutoFocusCallback implements AutoFocusCallback {

        @Override
        public void onAutoFocus(boolean success, Camera camera) {
            // Capture frame.
            if (mTriggerMode == TRIGGER_MODE_MANUAL) {
                takePicture();
            }
        }
    }

    /**
     * Callback when captured Jpeg is ready.
     */
    private class JpegPictureCallback implements Camera.PictureCallback {

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            if (isActivityAlive()) {
                // Save Jpeg frame in memory.
                mFramesData[mFrameIndex] = data;

                // Setup review overlay for user to review captured frame.
                mReviewStatus.setText(getString(R.string.capture__review_instructions));
                mReviewStatus.setTextColor(getResources().getColor(R.color.text_color));
                Bitmap bitmap = ImageHelper.createImage(data, mPreviewDisplayOrientation, mIsReflected, null);
                mReviewImage.setImageBitmap(bitmap);

                // Setup task to clear the review overlay after a frame removal event or after timeout.
                final int timeout;
                if (mTriggerMode == TRIGGER_MODE_BURST) {
                    timeout = REVIEW_OVERLAY_WAIT_DURATION_BURST;
                } else {
                    timeout = REVIEW_OVERLAY_WAIT_DURATION;
                }

                final CountDownLatch latch = new CountDownLatch(1);
                if (mTimer != null) {
                    mTimer.schedule(new TimerTask() {

                        @Override
                        public void run() {
                            try {
                                // Wait for user input or a fixed timeout.
                                latch.await(timeout, TimeUnit.MILLISECONDS);

                                // Post task to ui thread to prepare for next capture.
                                final Activity activity = getActivity();
                                if (activity != null && !activity.isFinishing()) {
                                    activity.runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            prepareNextCapture();
                                        }
                                    });
                                }
                            } catch (InterruptedException e) {
                                // Do nothing.
                            }
                        }
                    }, 0);
                }

                mReviewOverlay.setOnTouchListener(new ReviewOverlayOnTouchListener(latch));
                mReviewOverlay.setVisibility(View.VISIBLE);
            }
        }
    }

    /**
     * {@link OnTouchListener} for the review overlay. Removes the last captured frame when a remove gesture is
     * detected.
     */
    private class ReviewOverlayOnTouchListener implements OnTouchListener {

        private CountDownLatch mReviewLatch;

        private boolean isEnabled = true;

        private float mDownX = -1f;

        private float mDownY = -1f;

        /**
         * Constructor.
         *
         * @param latch the latch to countdown as a signal to proceed with capture sequence.
         */
        private ReviewOverlayOnTouchListener(CountDownLatch latch) {
            mReviewLatch = latch;
        }

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (isEnabled) {
                int action = event.getAction();
                if (action == MotionEvent.ACTION_DOWN) {
                    // Set anchor.
                    mDownX = event.getX();
                    mDownY = event.getY();
                } else if (action == MotionEvent.ACTION_MOVE) {
                    if (mDownX == -1f && mDownY == -1f) {
                        // Set anchor.
                        mDownX = event.getX();
                        mDownY = event.getY();
                    } else {
                        // Check swipe distance for discard gesture recognition.
                        float distance = Math.abs(event.getX() - mDownX) + Math.abs(event.getY() - mDownY);
                        if (distance > REVIEW_REMOVE_GESTURE_THRESHOLD) {
                            // Disable listener.
                            isEnabled = false;

                            // Remove frame by decrementing index.
                            mFrameIndex--;

                            // Indicate removed status.
                            mReviewStatus.setText(getString(R.string.capture__review_discarded));
                            mReviewStatus.setTextColor(getResources().getColor(R.color.selection_color));

                            // Notify latch to proceed with capture sequence.
                            mReviewLatch.countDown();
                        }
                    }
                }
            }

            // Handle all touch events.
            return true;
        }
    }

    //
    // Private methods.
    //

    /**
     * Checks whether the {@link Activity} is attached and not finishing. This should be used as a validation check in a
     * runnable posted to the ui thread, and the {@link Activity} may be have detached by the time the runnable
     * executes. This method should be called on the ui thread.
     *
     * @return true if {@link Activity} is still alive; false otherwise.
     */
    private boolean isActivityAlive() {
        Activity activity = getActivity();
        return activity != null && !activity.isFinishing();
    }

    /**
     * Handles the press of the start button.
     */
    private void onStartButtonPressedImpl() {
        if (mCamera != null) {
            mStartButton.setEnabled(false);
            mStartButton.setVisibility(View.INVISIBLE);
            mSwitchButton.setVisibility(View.GONE);
            mPreferencesButton.setVisibility(View.GONE);

            // Set flag to indicate capture sequence is running.
            mIsCaptureSequenceRunning = true;

            // Kick off capture sequence.
            if (mTriggerMode == TRIGGER_MODE_MANUAL) {
                kickoffManualCapture();
            } else {
                kickoffCountdownCapture();
            }
        }
    }

    /**
     * Takes picture.
     */
    private void takePicture() {
        if (isActivityAlive() && mCamera != null) {
            try {
                mCamera.takePicture(new Camera.ShutterCallback() {
                    @Override
                    public void onShutter() {
                        // Setting a listener enables the system shutter sound.
                    }
                }, null, new JpegPictureCallback());
            } catch (RuntimeException e) {
                // The native camera crashes occasionally. Self-recover by relaunching fragment.
                final LaunchActivity activity = (LaunchActivity) getActivity();
                Toast.makeText(activity, getString(R.string.capture__error_camera_crash), Toast.LENGTH_SHORT)
                        .show();
                activity.replaceFragment(CaptureFragment.newInstance(mUseFrontFacing), false, true);
            }
        }
    }

    /**
     * Resets countdown timer.
     */
    private void resetCountdownTimer() {
        if (mCountdown != null) {
            mCountdown.setVisibility(View.INVISIBLE);

            // Reset colors.
            int color = getResources().getColor(R.color.text_color);
            mCountdownOne.setTextColor(color);
            mCountdownTwo.setTextColor(color);
            mCountdownThree.setTextColor(color);
        }
    }

    /**
     * Kicks off auto-focus, captures frame at the end.
     */
    private void kickoffManualCapture() {
        // Kick off auto-focus and indicate status.
        if (mCamera != null) {
            mCamera.autoFocus(new MyAutoFocusCallback());
        }
    }

    /**
     * Kicks off countdown and auto-focus, captures frame at the end.
     */
    private void kickoffCountdownCapture() {
        // Set visibility of countdown timer.
        mCountdown.setVisibility(View.VISIBLE);

        if (mTimer != null) {
            mTimer.schedule(new TimerTask() {

                @Override
                public void run() {
                    final Activity activity = getActivity();
                    if (activity != null && !activity.isFinishing()) {
                        activity.runOnUiThread(new Runnable() {

                            @Override
                            public void run() {
                                if (isActivityAlive()) {
                                    if (mCountdownThree != null) {
                                        mCountdownThree
                                                .setTextColor(getResources().getColor(R.color.selection_color));
                                    }
                                    mCameraAudioHelper.beep();

                                    // Kick off auto-focus and indicate status.
                                    if (mCamera != null) {
                                        mCamera.autoFocus(new MyAutoFocusCallback());
                                    }
                                }
                            }
                        });
                    }
                }
            }, COUNTDOWN_STEP_DELAY * 2);

            mTimer.schedule(new TimerTask() {

                @Override
                public void run() {
                    final Activity activity = getActivity();
                    if (activity != null && !activity.isFinishing()) {
                        activity.runOnUiThread(new Runnable() {

                            @Override
                            public void run() {
                                if (isActivityAlive()) {
                                    if (mCountdownTwo != null) {
                                        mCountdownTwo
                                                .setTextColor(getResources().getColor(R.color.selection_color));
                                    }
                                    mCameraAudioHelper.beep();
                                }
                            }
                        });
                    }
                }
            }, COUNTDOWN_STEP_DELAY * 3);

            mTimer.schedule(new TimerTask() {

                @Override
                public void run() {
                    final Activity activity = getActivity();
                    if (activity != null && !activity.isFinishing()) {
                        activity.runOnUiThread(new Runnable() {

                            @Override
                            public void run() {
                                if (isActivityAlive()) {
                                    if (mCountdownOne != null) {
                                        mCountdownOne
                                                .setTextColor(getResources().getColor(R.color.selection_color));
                                    }
                                    mCameraAudioHelper.beep();
                                }
                            }
                        });
                    }
                }
            }, COUNTDOWN_STEP_DELAY * 4);

            mTimer.schedule(new TimerTask() {

                @Override
                public void run() {
                    final Activity activity = getActivity();
                    if (activity != null && !activity.isFinishing()) {
                        activity.runOnUiThread(new Runnable() {

                            @Override
                            public void run() {
                                if (isActivityAlive()) {
                                    // Reset countdown timer to initial state.
                                    resetCountdownTimer();

                                    // Capture frame.
                                    takePicture();
                                }
                            }
                        });
                    }
                }
            }, COUNTDOWN_STEP_DELAY * 5);
        }
    }

    /**
     * Kicks off capture in burst mode, which is basically capturing without auto-focus.
     */
    private void kickoffBurstCapture() {
        mTimer.schedule(new TimerTask() {

            @Override
            public void run() {
                final Activity activity = getActivity();
                if (activity != null && !activity.isFinishing()) {
                    activity.runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
                            takePicture();
                        }
                    });
                }
            }
        }, BURST_DELAY);
    }

    /**
     * Checks whether the camera image is reflected.
     *
     * @return true if the camera image is reflected; false otherwise.
     */
    private boolean isCameraImageReflected(CameraInfo cameraInfo) {
        return cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT;
    }

    /**
     * Prepares for the next capture or jump to next fragment if all frames have been collected.
     */
    private void prepareNextCapture() {
        if (isActivityAlive()) {
            // Cancel review overlay touch listener.
            mReviewOverlay.setOnTouchListener(null);

            // Increment frame index.
            mFrameIndex++;

            // Check if we need more frames.
            if (mFrameIndex < mFramesTotal) {
                // Update title.
                mTitle.setText(
                        String.format(getString(R.string.capture__title_frame), mFrameIndex + 1, mFramesTotal));

                // Restart preview.
                if (mCamera != null && mPreview != null) {
                    try {
                        mPreview.restart();
                    } catch (RuntimeException exception) {
                        final LaunchActivity activity = (LaunchActivity) getActivity();
                        if (activity != null && !activity.isFinishing()) {
                            String title = getString(R.string.capture__error_camera_dialog_title);
                            String message = getString(R.string.capture__error_camera_dialog_message_dead);
                            activity.showDialogFragment(ErrorDialogFragment.newInstance(title, message));
                        }
                    }
                }

                // Fade out review overlay.
                Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.fade_out);
                animation.setAnimationListener(new AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                        // Do nothing.
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                        // Do nothing.
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        // Hide the review overlay.
                        mReviewOverlay.setVisibility(View.GONE);
                        mReviewImage.setImageBitmap(null);

                        // Capture next frames.
                        if (mTriggerMode == TRIGGER_MODE_COUNTDOWN) {
                            kickoffCountdownCapture();
                        } else if (mTriggerMode == TRIGGER_MODE_BURST) {
                            kickoffBurstCapture();
                        } else {
                            // Enable start button.
                            mStartButton.setEnabled(true);
                            mStartButton.setVisibility(View.VISIBLE);
                        }
                    }
                });
                mReviewOverlay.startAnimation(animation);
            } else {
                // Set flag to indicate capture sequence is no longer running.
                mIsCaptureSequenceRunning = false;

                // Transition to next fragment since enough frames have been captured.
                nextFragment();
            }
        }
    }

    /**
     * Launches the next {@link Fragment}.
     */
    private void nextFragment() {
        ((LaunchActivity) getActivity()).replaceFragment(
                ShareFragment.newInstance(mFramesData, mPreviewDisplayOrientation, mIsReflected), true, false);
    }

    /**
     * Saves the current camera preference.
     *
     * @param context the {@link Context}.
     */
    private void saveCameraPreference(Context context) {
        SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(context.getApplicationContext());
        preferences.edit().putBoolean(getString(R.string.pref__camera_key), mUseFrontFacing).apply();
    }

    //
    // Public methods.
    //

    /**
     * Creates a new {@link CaptureFragment} instance.
     *
     * @param useFrontFacing true to use front facing camera; false otherwise.
     * @return the new {@link CaptureFragment} instance.
     */
    public static CaptureFragment newInstance(boolean useFrontFacing) {
        CaptureFragment fragment = new CaptureFragment();

        Bundle args = new Bundle();
        args.putBoolean(FRAGMENT_BUNDLE_KEY_CAMERA, useFrontFacing);
        fragment.setArguments(args);

        return fragment;
    }
}