SnapshotManager.java

/*
 * Copyright (C) 2013 Guillaume Lesniak
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */

package com.aspirs.focal;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.CursorIndexOutOfBoundsException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.hardware.Camera;
import android.location.Location;
import android.media.CamcorderProfile;
import android.media.ExifInterface;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.util.Log;

import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;

import org.cyanogenmod.focal.feats.AutoPictureEnhancer;
import org.cyanogenmod.focal.feats.PixelBuffer;
import org.cyanogenmod.focal.widgets.SimpleToggleWidget;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import fr.xplod.focal.R;

/**
 * This class manages taking snapshots and videos from Camera
 */
public class SnapshotManager {
    public final static String TAG = "SnapshotManager";
    private boolean mPaused;

    public interface SnapshotListener {
        /**
         * This callback is called when a snapshot is taken (shutter)
         *
         * @param info A structure containing information about the snapshot taken
         */
        public void onSnapshotShutter(SnapshotInfo info);
 
    }

    public class SnapshotInfo {
        // Whether or not the snapshot has to be saved to internal memory
        public boolean mSave;

        // The URI of the saved shot, if it has been saved, and if the save is done (mSave = true).
        // This field will likely be only different than null when onSnapshotSaved is called.
        public Uri mUri;

        // The exposure compensation value set for the shot, IF a specific
        // value was needed
        public int mExposureCompensation;

        // A bitmap containing a thumbnail of the image
        public Bitmap mThumbnail;

        // Whether or not to bypass image processing (even if user enabled it)
        public boolean mBypassProcessing;
    }

    private Context mContext;
    private CameraManager mCameraManager;
    private boolean mBypassProcessing;

    // Photo-related variables
    private boolean mWaitExposureSettle;
    private int mResetExposure;
    private List<SnapshotInfo> mSnapshotsQueue;
    private int mCurrentShutterQueueIndex;
    private List<SnapshotListener> mListeners;
    private Handler mHandler;
    private ContentResolver mContentResolver;
    private ImageSaver mImageSaver;
    private ImageNamer mImageNamer;
    private PixelBuffer mOffscreenGL;
    private AutoPictureEnhancer mAutoPicEnhancer;
    private boolean mImageIsProcessing;
    private boolean mDoAutoEnhance;

    // Video-related variables
    private long mRecordingStartTime;
    private boolean mIsRecording;
    private VideoNamer mVideoNamer;
    private CamcorderProfile mProfile;

    // The video file that the hardware camera is about to record into
    // (or is recording into.)
    private String mVideoFilename;
    private ParcelFileDescriptor mVideoFileDescriptor;

    // The video file that has already been recorded, and that is being
    // examined by the user.
    private String mCurrentVideoFilename;
    private Uri mCurrentVideoUri;
    private ContentValues mCurrentVideoValues;

    private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {
        @Override
        public void onShutter() {
            Log.v(TAG, "onShutter");

            Camera.Size s = mCameraManager.getParameters().getPictureSize();
            mImageNamer.prepareUri(mContentResolver, System.currentTimeMillis(), s.width, s.height, 0);

            // On shutter confirmed, play a small flashing animation
            final SnapshotInfo snap = mSnapshotsQueue.get(mCurrentShutterQueueIndex);

            for (SnapshotListener listener : mListeners) {
                listener.onSnapshotShutter(snap);
            }

            // If we used Samsung HDR, reset exposure
            if (mContext.getResources().getBoolean(R.bool.config_useSamsungHDR) &&
                SimpleToggleWidget.isWidgetEnabled(mContext, mCameraManager, "scene-mode", "hdr")) {
                mCameraManager.setParameterAsync("exposure-compensation",
                        Integer.toString(mResetExposure));
            }
        }
    };

    private Camera.PictureCallback mJpegPictureCallback = new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] jpegData, Camera camera) {
            Log.v(TAG, "onPicture: Got JPEG data");
            mCameraManager.restartPreviewIfNeeded();

            // Calculate the width and the height of the jpeg.
            final Camera.Size s = mCameraManager.getParameters().getPictureSize();
            int orientation = CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PANO ? 0 :
                    mCameraManager.getOrientation();
            final int width = s.width,
                    height = s.height;

            if (mSnapshotsQueue.size() == 0) {
                Log.e(TAG, "DERP! Why is snapshotqueue empty? Two JPEG callbacks!?");
                return;
            }

            final SnapshotInfo snap = mSnapshotsQueue.get(0);

            // If we have a Samsung HDR, convert from YUV422 to JPEG first
            if (mContext.getResources().getBoolean(R.bool.config_useSamsungHDR) &&
                SimpleToggleWidget.isWidgetEnabled(mContext, mCameraManager, "scene-mode", "hdr")) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                Bitmap bm = Util.decodeYUV422P(jpegData, s.width, s.height);
                // TODO: Replace 90 with real JPEG compression level when we'll have that setting
                bm.compress(Bitmap.CompressFormat.JPEG, 90, baos);
                jpegData = baos.toByteArray();
            }

            // Store the jpeg on internal memory if needed
            if (snap.mSave) {
                final Uri uri = mImageNamer.getUri();
                final String title = mImageNamer.getTitle();
                snap.mUri = uri;

                // If the orientation is somehow negative, avoid the Gallery crashing dumbly
                // (see com/android/gallery3d/ui/PhotoView.java line 758 (setTileViewPosition))
                while (orientation < 0) {
                    orientation += 360;
                }
                orientation = orientation % 360;

                final int correctedOrientation = orientation;
                final byte[] finalData = jpegData;

                if (!snap.mBypassProcessing && mDoAutoEnhance) {
                    new Thread() {
                        public void run() {
                            mImageIsProcessing = true;
                            for (SnapshotListener listener : mListeners) {
                                listener.onSnapshotProcessing(snap);
                            }

                            // Read EXIF
                            List<Tag> tagsList = new ArrayList<Tag>();
                            BufferedInputStream is = new BufferedInputStream(
                                    new ByteArrayInputStream(finalData));
                            try {
                                Metadata metadata = JpegMetadataReader.readMetadata(is, false);

                                for (Directory directory : metadata.getDirectories()) {
                                    for (Tag tag : directory.getTags()) {
                                        tagsList.add(tag);
                                    }
                                }
                            } catch (JpegProcessingException e) {
                                Log.e(TAG, "Error processing input JPEG", e);
                            }

                            // XXX: PixelBuffer has to be created every time because the GL context
                            // can only be used from its original thread. It's not very intense, but
                            // ideally we would be re-using the same thread every time.
                            try {
                                mOffscreenGL = new PixelBuffer(mContext, s.width, s.height);
                                mAutoPicEnhancer = new AutoPictureEnhancer(mContext);
                                mOffscreenGL.setRenderer(mAutoPicEnhancer);
                                mAutoPicEnhancer.setTexture(BitmapFactory.decodeByteArray(finalData,
                                        0, finalData.length));

                                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                                mOffscreenGL.getBitmap().compress(Bitmap.CompressFormat.JPEG, 90, baos);

                                if (mImageSaver != null) {
                                    mImageSaver.addImage(baos.toByteArray(), uri, title, null,
                                            width, height, correctedOrientation, tagsList, snap);
                                } else {
                                    Log.e(TAG, "ImageSaver was null: couldn't save image!");
                                }

                                if (mPaused && mImageSaver != null) {
                                    // We were paused, stop the saver now
                                    mImageSaver.finish();
                                    mImageSaver = null;
                                }

                                mImageIsProcessing = false;
                            }
                            catch (Exception e) {
                                // The rendering failed, the device might not be compatible for
                                // whatever reason. We just save the original file.
                                if (mImageSaver != null) {
                                    mImageSaver.addImage(finalData, uri, title, null,
                                            width, height, correctedOrientation, snap);
                                }

                                CameraActivity.notify("Auto-enhance failed: Original shot saved", 2000);
                            }
                            catch (OutOfMemoryError e) {
                                // The rendering failed, the device might not be compatible for
                                // whatever reason. We just save the original file.
                                if (mImageSaver != null) {
                                    mImageSaver.addImage(finalData, uri, title, null,
                                            width, height, correctedOrientation, snap);
                                }

                                CameraActivity.notify("Error: Out of memory. Original shot saved", 2000);
                            }
                        }
                    }.start();
                } else {
                    // Just save it as is
                    mImageSaver.addImage(finalData, uri, title, null,
                            width, height, correctedOrientation, snap);
                }
            }

            // Camera is ready to take another shot, doit
            if (mSnapshotsQueue.size() > mCurrentShutterQueueIndex + 1) {
                mCurrentShutterQueueIndex++;
                mHandler.post(mCaptureRunnable);
            }

            // We're done with our shot here!
            mCurrentShutterQueueIndex--;
            mSnapshotsQueue.remove(0);
        }
    };

    private Runnable mCaptureRunnable = new Runnable() {
        @Override
        public void run() {
            if (mWaitExposureSettle) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mCameraManager.takeSnapshot(mShutterCallback, null, mJpegPictureCallback);
        }
    };

    public SnapshotManager(CameraManager man, Context ctx) {
        mContext = ctx;
        mCameraManager = man;
        mSnapshotsQueue = new ArrayList<SnapshotInfo>();
        mListeners = new ArrayList<SnapshotListener>();
        mHandler = new Handler();
        mImageSaver = new ImageSaver();
        mImageNamer = new ImageNamer();
        mVideoNamer = new VideoNamer();
        mContentResolver = ctx.getContentResolver();
        mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
        mPaused = false;
        mImageIsProcessing = false;
    }

    public void addListener(SnapshotListener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
    }

    public void removeListener(SnapshotListener listener) {
        mListeners.remove(listener);
    }

    /**
     * Sets whether or not to bypass image processing (for burst shot or hdr for instance)
     * This value is reset after each snapshot queued!
     * @param bypass
     */
    public void setBypassProcessing(boolean bypass) {
        mBypassProcessing = bypass;
    }

    public void setAutoEnhance(boolean enhance) {
        mDoAutoEnhance = enhance;
    }

    public boolean getAutoEnhance() {
        return mDoAutoEnhance;
    }

    public void prepareNamerUri(int width, int height) {
        if (mImageNamer == null) {
            // ImageNamer can be dead if the user exitted the app.
            // We restart it temporarily.
            mImageNamer = new ImageNamer();
        }
        mImageNamer.prepareUri(mContentResolver,
                System.currentTimeMillis(), width, height, 0);
    }

    public Uri getNamerUri() {
        if (mImageNamer == null) {
            // ImageNamer can be dead if the user exitted the app.
            // We restart it temporarily.
            mImageNamer = new ImageNamer();
        }
        return mImageNamer.getUri();
    }

    public String getNamerTitle() {
        if (mImageNamer == null) {
            // ImageNamer can be dead if the user exitted the app.
            // We restart it temporarily.
            mImageNamer = new ImageNamer();
        }
        return mImageNamer.getTitle();
    }

    public void saveImage(Uri uri, String title, int width, int height,
                          int orientation, byte[] jpegData) {
        if (mImageSaver == null) {
            // ImageSaver can be dead if the user exitted the app.
            // We restart it temporarily.
            mImageSaver = new ImageSaver();
        }
        mImageSaver.addImage(jpegData, uri, title, null,
                width, height, orientation);
        mImageSaver.waitDone();
        mImageSaver.finish();
    }

    /**
     * Queues a snapshot that will be taken as soon as possible
     *
     * @param save                 Whether or not to save the snapshot in gallery
     *                             (for example, software HDR doesn't need all the shots to
     *                             be saved)
     * @param exposureCompensation If the shot has to be taken at a different
     *                             exposure value, otherwise set it to 0
     */
    public void queueSnapshot(boolean save, int exposureCompensation) {

        if (mSnapshotsQueue.size() == 2) return; // No more than 2 shots at a time

        SnapshotInfo info = new SnapshotInfo();
        info.mSave = save;
        info.mExposureCompensation = exposureCompensation;
        info.mThumbnail = mCameraManager.getLastPreviewFrame();
        info.mBypassProcessing = mBypassProcessing;

        Camera.Parameters params = mCameraManager.getParameters();
        if (params != null && params.getExposureCompensation() != exposureCompensation) {
            mCameraManager.setParameterAsync("exposure-compensation",
                    Integer.toString(exposureCompensation));
            mWaitExposureSettle = true;
        }
        mSnapshotsQueue.add(info);

        // Reset bypass in any case
        mBypassProcessing = false;

        if (mSnapshotsQueue.size() == 1) {
            // We had no other snapshot queued so far, so start things up
            Log.v(TAG, "No snapshot queued, starting runnable");

            mCurrentShutterQueueIndex = 0;
            new Thread(mCaptureRunnable).start();
        }
    }

    public ImageNamer getImageNamer() {
        return mImageNamer;
    }

    public ImageSaver getImageSaver() {
        return mImageSaver;
    }

    public CamcorderProfile getVideoProfile(){
        return mProfile;
    }
   
    public void setVideoProfile(final CamcorderProfile profile) {
        mProfile = profile;

        if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
            // TODO: setVideoSize is handling preview changing
            mCameraManager.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
        }
    }

    /**
     * Starts recording a video with the current settings
     */
    public void startVideo() {
        Log.v(TAG, "startVideo");

        // Setup output file
        generateVideoFilename(mProfile.fileFormat);
        mCameraManager.prepareVideoRecording(mVideoFilename, mProfile);

        mCameraManager.startVideoRecording();
        mIsRecording = true;
        mRecordingStartTime = SystemClock.uptimeMillis();

        for (SnapshotListener listener : mListeners) {
            listener.onVideoRecordingStart();
        }
    }

    /**
     * Stops the current recording video, if any
     */
    public void stopVideo() {
        Log.v(TAG, "stopVideo");
        if (mIsRecording) {
            // Stop the video in a thread to not stall the UI thread
            new Thread() {
                public void run() {
                    mCameraManager.stopVideoRecording();

                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            for (SnapshotListener listener : mListeners) {
                                listener.onVideoRecordingStop();
                                listener.onMediaSavingDone();
                            }
                        }
                    });
                }
            }.start();

            mCurrentVideoFilename = mVideoFilename;

            mIsRecording = false;

            for (SnapshotListener listener : mListeners) {
                listener.onVideoRecordingStop();
                listener.onMediaSavingStart();
            }

            addVideoToMediaStore();
        }
    }

    /**
     * Returns whether or not a video is recording
     */
    public boolean isRecording() {
        return mIsRecording;
    }

    /**
     * Add the last recorded video to the MediaStore
     *
     * @return True if operation succeeded
     */
    private boolean addVideoToMediaStore() {
        boolean fail = false;
        if (mVideoFileDescriptor == null) {
            mCurrentVideoValues.put(MediaStore.Video.Media.SIZE,
                    new File(mCurrentVideoFilename).length());
            long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
            if (duration > 0) {
                mCurrentVideoValues.put(MediaStore.Video.Media.DURATION, duration);
            } else {
                Log.w(TAG, "Video duration <= 0 : " + duration);
            }
            try {
                mCurrentVideoUri = mVideoNamer.getUri();

                // Rename the video file to the final name. This avoids other
                // apps reading incomplete data.  We need to do it after the
                // above mVideoNamer.getUri() call, so we are certain that the
                // previous insert to MediaProvider is completed.
                String finalName = mCurrentVideoValues.getAsString(
                        MediaStore.Video.Media.DATA);
                if (new File(mCurrentVideoFilename).renameTo(new File(finalName))) {
                    mCurrentVideoFilename = finalName;
                }

                mContentResolver.update(mCurrentVideoUri, mCurrentVideoValues
                        , null, null);
                mContext.sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO,
                        mCurrentVideoUri));
            } catch (Exception e) {
                // We failed to insert into the database. This can happen if
                // the SD card is unmounted.
                Log.e(TAG, "failed to add video to media store", e);
                mCurrentVideoUri = null;
                mCurrentVideoFilename = null;
                fail = true;
            } finally {
                Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
            }
        }
        mCurrentVideoValues = null;
        return fail;
    }

    /**
     * Generates a filename for the next video to record
     *
     * @param outputFileFormat The file format of the video
     */
    private void generateVideoFilename(int outputFileFormat) {
        long dateTaken = System.currentTimeMillis();
        String title = Util.createVideoName(dateTaken);
        // Used when emailing.
        String filename = title + convertOutputFormatToFileExt(outputFileFormat);
        String mime = convertOutputFormatToMimeType(outputFileFormat);
        String path = Storage.getStorage().generateDirectory() + '/' + filename;
        String tmpPath = path + ".tmp";
        mCurrentVideoValues = new ContentValues(7);
        mCurrentVideoValues.put(MediaStore.Video.Media.TITLE, title);
        mCurrentVideoValues.put(MediaStore.Video.Media.DISPLAY_NAME, filename);
        mCurrentVideoValues.put(MediaStore.Video.Media.DATE_TAKEN, dateTaken);
        mCurrentVideoValues.put(MediaStore.Video.Media.MIME_TYPE, mime);
        mCurrentVideoValues.put(MediaStore.Video.Media.DATA, path);
        mCurrentVideoValues.put(MediaStore.Video.Media.RESOLUTION,
                Integer.toString(mProfile.videoFrameWidth) + "x" +
                        Integer.toString(mProfile.videoFrameHeight));
        Location loc = null; // TODO: mLocationManager.getCurrentLocation();
        if (loc != null) {
            mCurrentVideoValues.put(MediaStore.Video.Media.LATITUDE, loc.getLatitude());
            mCurrentVideoValues.put(MediaStore.Video.Media.LONGITUDE, loc.getLongitude());
        }
        mVideoNamer.prepareUri(mContentResolver, mCurrentVideoValues);
        mVideoFilename = tmpPath;
        Log.v(TAG, "New video filename: " + mVideoFilename);
    }

    private String convertOutputFormatToMimeType(int outputFileFormat) {
        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
            return "video/mp4";
        }
        return "video/3gpp";
    }

    private String convertOutputFormatToFileExt(int outputFileFormat) {
        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
            return ".mp4";
        }
        return ".3gp";
    }

    public void onPause() {
        mPaused = true;

        if (!mImageIsProcessing && mImageSaver != null) {
            // We wait until the last processing image was saved
            mImageSaver.finish();
        }
        mImageNamer.finish();
        mVideoNamer.finish();
        mImageSaver = null;
        mImageNamer = null;
        mVideoNamer = null;
    }

    public void onResume() {
        mPaused = false;

        // Restore threads if needed
        if (mImageSaver == null) {
            mImageSaver = new ImageSaver();
        }

        if (mImageNamer == null) {
            mImageNamer = new ImageNamer();
        }

        if (mVideoNamer == null) {
            mVideoNamer = new VideoNamer();
        }
    }

    // Each SaveRequest remembers the data needed to save an image.
    private static class SaveRequest {
        byte[] data;
        Uri uri;
        String title;
        Location loc;
        int width, height;
        int orientation;
        List<Tag> exifTags;
        SnapshotInfo snap;
    }

    // We use a queue to store the SaveRequests that have not been completed
    // yet. The main thread puts the request into the queue. The saver thread
    // gets it from the queue, does the work, and removes it from the queue.
    //
    // The main thread needs to wait for the saver thread to finish all the work
    // in the queue, when the activity's onPause() is called, we need to finish
    // all the work, so other programs (like Gallery) can see all the images.
    //
    // If the queue becomes too long, adding a new request will block the main
    // thread until the queue length drops below the threshold (QUEUE_LIMIT).
    // If we don't do this, we may face several problems: (1) We may OOM
    // because we are holding all the jpeg data in memory. (2) We may ANR
    // when we need to wait for saver thread finishing all the work (in
    // onPause() or gotoGallery()) because the time to finishing a long queue
    // of work may be too long.
    private class ImageSaver extends Thread {
        private static final int QUEUE_LIMIT = 3;

        private ArrayList<SaveRequest> mQueue;
        private boolean mStop;

        // Runs in main thread
        public ImageSaver() {
            mQueue = new ArrayList<SaveRequest>();
            start();
        }

        // Runs in main thread
        public void addImage(final byte[] data, Uri uri, String title,
                             Location loc, int width, int height, int orientation, SnapshotInfo snap) {
            addImage(data, uri, title, loc, width, height, orientation, null, snap);
        }

        // Runs in main thread
        public void addImage(final byte[] data, Uri uri, String title,
                             Location loc, int width, int height, int orientation) {
           addImage(data, uri, title, loc, width, height, orientation, null, null);
        }

        // Runs in main thread
        public void addImage(final byte[] data, Uri uri, String title,
                             Location loc, int width, int height, int orientation,
                             List<Tag> exifTags, SnapshotInfo snap) {
            SaveRequest r = new SaveRequest();
            r.data = data;
            r.uri = uri;
            r.title = title;
            r.loc = (loc == null) ? null : new Location(loc);  // make a copy
            r.width = width;
            r.height = height;
            r.orientation = orientation;
            r.exifTags = exifTags;
            r.snap = snap;
            synchronized (this) {
                while (mQueue.size() >= QUEUE_LIMIT) {
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                        // ignore.
                    }
                }
                mQueue.add(r);
                notifyAll();  // Tell saver thread there is new work to do.
            }
        }

        // Runs in saver thread
        @Override
        public void run() {
            while (true) {
                SaveRequest r;
                synchronized (this) {
                    if (mQueue.isEmpty()) {
                        notifyAll();  // notify main thread in waitDone

                        // Note that we can only stop after we saved all images
                        // in the queue.
                        if (mStop) break;

                        try {
                            wait();
                        } catch (InterruptedException ex) {
                            // ignore.
                        }
                        continue;
                    }
                    r = mQueue.get(0);
                    for (SnapshotListener listener : mListeners) {
                        listener.onMediaSavingStart();
                    }
                }
                storeImage(r.data, r.uri, r.title, r.loc, r.width, r.height,
                        r.orientation, r.exifTags, r.snap);
                synchronized (this) {
                    mQueue.remove(0);
                    for (SnapshotListener listener : mListeners) {
                        listener.onMediaSavingDone();
                    }
                    notifyAll();  // the main thread may wait in addImage
                }
            }
        }

        // Runs in main thread
        public void waitDone() {
            synchronized (this) {
                while (!mQueue.isEmpty()) {
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                        // ignore.
                    }
                }
            }
        }

        // Runs in main thread
        public void finish() {
            waitDone();
            synchronized (this) {
                mStop = true;
                notifyAll();
            }
            try {
                join();
            } catch (InterruptedException ex) {
                // ignore.
            }
        }

        // Runs in saver thread
        private void storeImage(final byte[] data, Uri uri, String title,
                                Location loc, int width, int height, int orientation,
                                List<Tag> exifTags, SnapshotInfo snap) {
            boolean ok = Storage.getStorage().updateImage(mContentResolver, uri, title, loc,
                    orientation, data, width, height);

            if (ok) {
                if (exifTags != null && exifTags.size() > 0) {
                    // Write exif tags to final picture
                    try {
                        ExifInterface exifIf = new ExifInterface(Util.getRealPathFromURI(mContext, uri));

                        for (Tag tag : exifTags) {
                            // move along
                            String[] hack = tag.toString().split("\\]");
                            hack = hack[1].split("-");
                            exifIf.setAttribute(tag.getTagName(), hack[1].trim());
                        }

                        exifIf.saveAttributes();
                    } catch (IOException e) {
                        Log.e(TAG, "Couldn't write exif", e);
                    } catch (CursorIndexOutOfBoundsException e) {
                        Log.e(TAG, "Couldn't find original picture", e);
                    }
                }

                Util.broadcastNewPicture(mContext, uri);
            }

            if (snap != null) {
                for (SnapshotListener listener : mListeners) {
                    listener.onSnapshotSaved(snap);
                }
            }
        }
    }

    private static class ImageNamer extends Thread {
        private boolean mRequestPending;
        private ContentResolver mResolver;
        private long mDateTaken;
        private int mWidth, mHeight;
        private boolean mStop;
        private Uri mUri;
        private String mTitle;

        // Runs in main thread
        public ImageNamer() {
            start();
        }

        // Runs in main thread
        public synchronized void prepareUri(ContentResolver resolver,
                long dateTaken, int width, int height, int rotation) {
            if (rotation % 180 != 0) {
                int tmp = width;
                width = height;
                height = tmp;
            }
            mRequestPending = true;
            mResolver = resolver;
            mDateTaken = dateTaken;
            mWidth = width;
            mHeight = height;
            notifyAll();
        }

        // Runs in main thread
        public synchronized Uri getUri() {
            // wait until the request is done.
            while (mRequestPending) {
                try {
                    wait();
                } catch (InterruptedException ex) {
                    // Do nothing here
                }
            }

            // Return the uri generated
            Uri uri = mUri;
            mUri = null;
            return uri;
        }

        // Runs in main thread, should be called after getUri().
        public synchronized String getTitle() {
            return mTitle;
        }

        // Runs in namer thread
        @Override
        public synchronized void run() {
            while (true) {
                if (mStop) break;
                if (!mRequestPending) {
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                        // ignore.
                    }
                    continue;
                }
                cleanOldUri();
                generateUri();
                mRequestPending = false;
                notifyAll();
            }
            cleanOldUri();
        }

        // Runs in main thread
        public synchronized void finish() {
            mStop = true;
            notifyAll();
        }

        // Runs in namer thread
        private void generateUri() {
            mTitle = Util.createJpegName(mDateTaken);
            mUri = Storage.getStorage().newImage(mResolver, mTitle, mDateTaken, mWidth, mHeight);
        }

        // Runs in namer thread
        private void cleanOldUri() {
            if (mUri == null) return;
            Storage.getStorage().deleteImage(mResolver, mUri);
            mUri = null;
        }
    }


    private static class VideoNamer extends Thread {
        private boolean mRequestPending;
        private ContentResolver mResolver;
        private ContentValues mValues;
        private boolean mStop;
        private Uri mUri;

        // Runs in main thread
        public VideoNamer() {
            start();
        }

        // Runs in main thread
        public synchronized void prepareUri(
                ContentResolver resolver, ContentValues values) {
            mRequestPending = true;
            mResolver = resolver;
            mValues = new ContentValues(values);
            notifyAll();
        }

        // Runs in main thread
        public synchronized Uri getUri() {
            // wait until the request is done.
            while (mRequestPending) {
                try {
                    wait();
                } catch (InterruptedException ex) {
                    // ignore.
                }
            }
            Uri uri = mUri;
            mUri = null;
            return uri;
        }

        // Runs in namer thread
        @Override
        public synchronized void run() {
            while (true) {
                if (mStop) break;
                if (!mRequestPending) {
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                        // Do nothing here
                    }
                    continue;
                }
                cleanOldUri();
                generateUri();
                mRequestPending = false;
                notifyAll();
            }
            cleanOldUri();
        }

        // Runs in main thread
        public synchronized void finish() {
            mStop = true;
            notifyAll();
        }

        // Runs in namer thread
        private void generateUri() {
            Uri videoTable = Uri.parse("content://media/external/video/media");
            mUri = mResolver.insert(videoTable, mValues);
        }

        // Runs in namer thread
        private void cleanOldUri() {
            if (mUri == null) return;
            mResolver.delete(mUri, null, null);
            mUri = null;
        }
    }
}

 

你可能感兴趣的:(SnapshotManager.java)