OSDN Git Service

CaptureSession can now be started with an empty placeholder.
authorSascha Haeberling <haeberling@google.com>
Wed, 21 Jan 2015 02:59:08 +0000 (18:59 -0800)
committerSascha Haeberling <haeberling@google.com>
Fri, 23 Jan 2015 22:06:00 +0000 (14:06 -0800)
  Bug: 19014774
  Bug: 18853119
  Bug: 18979823

Being able to start with an empty placeholder and then updating that
placeholder allows us to add a visual entry to the filmstrip early on.

This avoid jarring effects where after taking many apid fire shots and
going to filmstrip, the entries are appearing one after the other.
This was caused by sessions only getting started when the thumbnail was
produced, which can take a while when multiple shots are in the
encoding pipeline.

Also contains changes that make CaptureSession testable.

Change-Id: I3d44704099be81524f8e165f3f09de6eebfc147b

18 files changed:
res/values/colors.xml
src/com/android/camera/CaptureModule.java
src/com/android/camera/Storage.java
src/com/android/camera/app/CameraServicesImpl.java
src/com/android/camera/data/FilmstripItem.java
src/com/android/camera/data/FilmstripItemBase.java
src/com/android/camera/data/SessionItem.java
src/com/android/camera/one/v2/OneCameraImpl.java
src/com/android/camera/one/v2/OneCameraZslImpl.java
src/com/android/camera/one/v2/imagesaver/JpegImageBackendImageSaver.java
src/com/android/camera/one/v2/imagesaver/YuvImageBackendImageSaver.java
src/com/android/camera/processing/ProcessingService.java
src/com/android/camera/session/CaptureSession.java
src/com/android/camera/session/CaptureSessionManager.java
src/com/android/camera/session/CaptureSessionManagerImpl.java
src/com/android/camera/session/PlaceholderManager.java
src/com/android/camera/session/SessionStorageManager.java
src/com/android/camera/session/SessionStorageManagerImpl.java

index 3fc3131..4bc2aeb 100644 (file)
     <color name="settings_cling_color">#2962FF</color>
 
     <color name="mode_icon_hover_highlight">#2DFFFFFF</color>
-    
+
     <color name="blue_button_text_color">#00838F</color>
 </resources>
index df9429d..f4236bb 100644 (file)
@@ -401,12 +401,12 @@ public class CaptureModule extends CameraModule implements
         try {
             tempSessionDataDirectory = getServices().getCaptureSessionManager()
                     .getSessionDirectory(BURST_SESSIONS_DIR);
+            CaptureSession session = createAndStartCaptureSession();
+            mBurstController.startBurst(session, tempSessionDataDirectory);
         } catch (IOException e) {
             Log.e(TAG, "Cannot start burst", e);
             return;
         }
-        CaptureSession session = createCaptureSession();
-        mBurstController.startBurst(session, tempSessionDataDirectory);
     }
 
     @Override
@@ -443,7 +443,13 @@ public class CaptureModule extends CameraModule implements
     }
 
     private void takePictureNow() {
-        CaptureSession session = createCaptureSession();
+        CaptureSession session = null;
+        try {
+            session = createAndStartCaptureSession();
+        } catch (IOException e) {
+            Log.e(TAG, "Cannot take picture.", e);
+            return;
+        }
         int orientation = mAppController.getOrientationManager().getDeviceOrientation()
                 .getDegrees();
 
@@ -453,16 +459,21 @@ public class CaptureModule extends CameraModule implements
                 session.getTitle(), orientation, session.getLocation(),
                 mContext.getExternalCacheDir(), this, mPictureSaverCallback,
                 mHeadingSensor.getCurrentHeading(), mZoomValue, 0);
-
         mCamera.takePicture(params, session);
     }
 
-    private CaptureSession createCaptureSession() {
+    /**
+     * Creates, starts and returns a new capture session. The returned session
+     * will have been started with an empty placeholder image.
+     */
+    private CaptureSession createAndStartCaptureSession() throws IOException {
         long sessionTime = getSessionTime();
         Location location = mLocationManager.getCurrentLocation();
         String title = CameraUtil.instance().createJpegName(sessionTime);
-        return getServices().getCaptureSessionManager()
+        CaptureSession session = getServices().getCaptureSessionManager()
                 .createNewSession(title, sessionTime, location);
+        session.startEmpty(new Size((int) mPreviewArea.width(), (int) mPreviewArea.height()));
+        return session;
     }
 
     private long getSessionTime() {
index 84ef964..b313436 100644 (file)
@@ -32,6 +32,7 @@ import com.android.camera.data.FilmstripItemData;
 import com.android.camera.debug.Log;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.util.ApiHelper;
+import com.android.camera.util.Size;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -39,6 +40,8 @@ import java.util.HashMap;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import javax.annotation.Nonnull;
+
 public class Storage {
     public static final String DCIM =
             Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
@@ -192,12 +195,7 @@ public class Storage {
      * @return A new URI used to reference this placeholder
      */
     public static Uri addPlaceholder(Bitmap placeholder) {
-        Uri uri;
-        Uri.Builder builder = new Uri.Builder();
-        String uuid = UUID.randomUUID().toString();
-        builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid);
-        uri = builder.build();
-
+        Uri uri = generateUniquePlaceholderUri();
         replacePlaceholder(uri, placeholder);
         return uri;
     }
@@ -219,6 +217,22 @@ public class Storage {
     }
 
     /**
+     * Creates an empty placeholder.
+     *
+     * @param size the size of the placeholder in pixels.
+     * @return A new URI used to reference this placeholder
+     */
+    @Nonnull
+    public static Uri addEmptyPlaceholder(@Nonnull Size size) {
+        Uri uri = generateUniquePlaceholderUri();
+        sSessionsToSizes.put(uri, new Point(size.getWidth(), size.getHeight()));
+        sSessionsToPlaceholderBitmap.put(uri, null /* Bitmap */);
+        Integer currentVersion = sSessionsToPlaceholderVersions.get(uri);
+        sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1);
+        return uri;
+    }
+
+    /**
      * Take jpeg bytes and add them to the media store, either replacing an existing item
      * or a placeholder uri to replace
      * @param imageUri The content uri or session uri of the image being updated
@@ -243,6 +257,13 @@ public class Storage {
                 width, height, mimeType);
     }
 
+    private static Uri generateUniquePlaceholderUri() {
+        Uri.Builder builder = new Uri.Builder();
+        String uuid = UUID.randomUUID().toString();
+        builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid);
+        return builder.build();
+    }
+
     private static void setImageSize(ContentValues values, int width, int height) {
         // The two fields are available since ICS but got published in JB
         if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
index 3b213f7..0f54fb6 100644 (file)
@@ -20,6 +20,7 @@ import android.content.Context;
 
 import com.android.camera.MediaSaverImpl;
 import com.android.camera.Storage;
+import com.android.camera.async.MainThread;
 import com.android.camera.remote.RemoteShutterListener;
 import com.android.camera.session.CaptureSessionManager;
 import com.android.camera.session.CaptureSessionManagerImpl;
@@ -65,7 +66,7 @@ public class CameraServicesImpl implements CameraServices {
         StackSaverFactory mStackSaverFactory = new StackSaverFactory(Storage.DIRECTORY,
               context.getContentResolver());
         mSessionManager = new CaptureSessionManagerImpl(mMediaSaver, context.getContentResolver(),
-              mPlaceHolderManager, mSessionStorageManager, mStackSaverFactory);
+              mPlaceHolderManager, mSessionStorageManager, mStackSaverFactory, MainThread.create());
         mMemoryManager = MemoryManagerImpl.create(context, mMediaSaver);
         mRemoteShutterListener = RemoteShutterHelper.create(context);
         mSettingsManager = new SettingsManager(context);
index ee74112..3c8fd34 100644 (file)
@@ -21,6 +21,7 @@ import android.net.Uri;
 import android.view.View;
 
 import com.android.camera.debug.Log;
+import com.android.camera2.R;
 import com.google.common.base.Optional;
 
 /**
@@ -36,6 +37,10 @@ public interface FilmstripItem {
     public static final int MAXIMUM_TEXTURE_SIZE = 2048;
     public static final int MAXIMUM_SMOOTH_TEXTURE_SIZE = 1024;
 
+    /** Default placeholder to display while images load */
+    static final int DEFAULT_PLACEHOLDER_RESOURCE = R.color.photo_placeholder;
+
+
     /**
      * An action callback to be used for actions on the filmstrip items.
      */
index 89ae010..4f9f51f 100644 (file)
@@ -23,7 +23,6 @@ import android.view.View;
 
 import com.android.camera.Storage;
 import com.android.camera.debug.Log;
-import com.android.camera2.R;
 import com.bumptech.glide.BitmapRequestBuilder;
 import com.bumptech.glide.Glide;
 import com.google.common.base.Optional;
@@ -41,9 +40,6 @@ public abstract class FilmstripItemBase<T extends FilmstripItemData> implements
     /** The minimum id to use to query for all media at a given media store uri */
     public static final int QUERY_ALL_MEDIA_ID = -1;
 
-    /** Default placeholder to display while images load */
-    protected static final int DEFAULT_PLACEHOLDER_RESOURCE = R.color.photo_placeholder;
-
     protected final Context mContext;
     protected final T mData;
     protected final Metadata mMetaData;
index 77bad3f..699cce6 100644 (file)
@@ -77,7 +77,11 @@ public class SessionItem implements FilmstripItem {
         }
 
         Bitmap placeholder = Storage.getPlacerHolderForSession(mData.getUri());
-        imageView.setImageBitmap(placeholder);
+        if (placeholder != null) {
+            imageView.setImageBitmap(placeholder);
+        } else {
+            imageView.setImageResource(DEFAULT_PLACEHOLDER_RESOURCE);
+        }
         imageView.setContentDescription(mContext.getResources().getString(
                 R.string.media_processing_content_description));
         return imageView;
index 26c860a..453c14e 100644 (file)
@@ -755,7 +755,6 @@ public class OneCameraImpl extends AbstractOneCamera {
         } else {
             // Since this is not an HDR+ session, we will just save the
             // result.
-            capture.session.startEmpty();
             byte[] imageBytes = acquireJpegBytesAndClose(capture.image);
             saveJpegPicture(imageBytes, capture.parameters, capture.session);
         }
index b5eb7f0..0a57aec 100644 (file)
@@ -274,7 +274,6 @@ public class OneCameraZslImpl extends AbstractOneCamera {
 
             mReadyStateManager.setInput(ReadyStateRequirement.CAPTURE_NOT_IN_PROGRESS, true);
 
-            mSession.startEmpty();
             savePicture(image, mParams, mSession);
             mParams.callback.onPictureTaken(mSession);
             Log.v(TAG, "Image saved.  Frame number = " + captureResult.getFrameNumber());
index ad2b8eb..80ba984 100644 (file)
@@ -113,9 +113,6 @@ public class JpegImageBackendImageSaver implements ImageSaver.Builder {
 
         @Override
         public void onStart(TaskImageContainer.TaskInfo task) {
-            if (task.destination == TaskImageContainer.TaskInfo.Destination.FINAL_IMAGE) {
-                mSession.startEmpty();
-            }
         }
 
         @Override
index e7a10fb..c7d88eb 100644 (file)
@@ -155,7 +155,8 @@ public class YuvImageBackendImageSaver implements ImageSaver.Builder {
                         @Override
                         public void run() {
                             // TODO: Finalize and I18N string.
-                            mSession.startSession(bitmapIntermediateRotated, "Saving image ...");
+                            mSession.updateThumbnail(bitmapIntermediateRotated);
+                            mSession.setProgressMessage("Saving image ...");
                         }
                     });
                     break;
index b1e020a..682fa10 100644 (file)
@@ -38,6 +38,7 @@ import com.android.camera.session.CaptureSessionManager;
 import com.android.camera.util.AndroidServices;
 import com.android.camera2.R;
 
+import java.io.IOException;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -222,7 +223,12 @@ public class ProcessingService extends Service implements ProgressListener {
         CaptureSession session = task.getSession();
         if (session == null) {
             // TODO: Timestamp is not required right now, refactor this to make it clearer.
-            session = mSessionManager.createNewSession(task.getName(), 0, task.getLocation());
+            try {
+                session = mSessionManager.createNewSession(task.getName(), 0, task.getLocation());
+            } catch (IOException e) {
+                Log.e(TAG, "Cannot process this session.", e);
+                return;
+            }
         }
         resetNotification();
 
index 0f5f522..e21e2f9 100644 (file)
@@ -22,6 +22,7 @@ import android.net.Uri;
 
 import com.android.camera.app.MediaSaver.OnMediaSavedListener;
 import com.android.camera.exif.ExifInterface;
+import com.android.camera.util.Size;
 
 /**
  * A session is an item that is in progress of being created and saved, such as
@@ -74,6 +75,27 @@ public interface CaptureSession {
     public CharSequence getProgressMessage();
 
     /**
+     * Changes the progress status message of this session.
+     *
+     * @param message the new message
+     */
+    public void setProgressMessage(CharSequence message);
+
+    /**
+     * For an ongoing session, this updates the currently displayed thumbnail.
+     *
+     * @param bitmap the thumbnail to be shown while the session is in progress.
+     */
+    public void updateThumbnail(Bitmap bitmap);
+
+    /**
+     * Starts an empty session with the given placeholder size.
+     *
+     * @param mPictureSize the size, in pixels of the empty placeholder.
+     */
+    public void startEmpty(Size mPictureSize);
+
+    /**
      * Starts the session by adding a placeholder to the filmstrip and adding
      * notifications.
      *
@@ -107,25 +129,12 @@ public interface CaptureSession {
     public void startSession(Uri uri, CharSequence progressMessage);
 
     /**
-     * Start a session like this if it's not processing for a long time and
-     * therefore doesn't need a temporary placeholder or a progress message.
-     */
-    public void startEmpty();
-
-    /**
      * Cancel the session without a final result. The session will be removed
      * from the film strip, progress notifications will be cancelled.
      */
     public void cancel();
 
     /**
-     * Changes the progress status message of this session.
-     *
-     * @param message the new message
-     */
-    public void setProgressMessage(CharSequence message);
-
-    /**
      * Finish the session by saving the image to disk. Will add the final item
      * in the film strip and remove the progress notifications.
      */
@@ -163,20 +172,6 @@ public interface CaptureSession {
     public Uri getUri();
 
     /**
-     * Returns the Content URI to the final output of this session. This is only
-     * available if the session has been finished. Returns null if it has not
-     * been finished.
-     */
-    public Uri getContentUri();
-
-    /**
-     * Whether this session has been started. Once it has been started it will
-     * have a valid path and can be processed. False is returned, if the session
-     * has not been started yet and no path is available.
-     */
-    public boolean isStarted();
-
-    /**
      * Updates the preview from a file. {@link #onPreviewAvailable()} will be
      * invoked upon completion.
      *
@@ -185,11 +180,6 @@ public interface CaptureSession {
     public void updatePreview(String previewPath);
 
     /**
-     * Called when the preview is already available.
-     */
-    public void onPreviewAvailable();
-
-    /**
      * Adds a progress listener to this session.
      */
     public void addProgressListener(ProgressListener listener);
index de9cf09..0c4f01c 100644 (file)
@@ -68,13 +68,7 @@ public interface CaptureSessionManager {
      * @param sessionStartMillis the start time of the new session (millis since epoch).
      * @param location the location of the new session.
      */
-    CaptureSession createNewSession(String title, long sessionStartMillis, Location location);
-
-    /**
-     * Creates a session based on an existing URI in the filmstrip and media
-     * store. This can be used to re-process an image.
-     */
-    CaptureSession createSession();
+    CaptureSession createNewSession(String title, long sessionStartMillis, Location location) throws IOException;
 
     /**
      * Returns a session by session Uri or null if it is not found.
index ddef25a..c417fa5 100644 (file)
@@ -22,15 +22,15 @@ import android.graphics.BitmapFactory;
 import android.location.Location;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.Looper;
 
 import com.android.camera.app.MediaSaver;
 import com.android.camera.app.MediaSaver.OnMediaSavedListener;
+import com.android.camera.async.MainThread;
 import com.android.camera.data.FilmstripItemData;
 import com.android.camera.debug.Log;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.util.FileUtil;
+import com.android.camera.util.Size;
 
 import java.io.File;
 import java.io.IOException;
@@ -41,39 +41,61 @@ import java.util.Map;
 
 /**
  * Implementation for the {@link CaptureSessionManager}.
+ * <p>
+ * Basic usage:
+ * <ul>
+ * <li>Create a new capture session.</li>
+ * <li>Pass it around to anywhere where the status of a session needs to be
+ * updated.</li>
+ * <li>If this is a longer operation, use one of the start* methods to indicate
+ * that processing of this session has started. The Camera app right now will
+ * use this to add a new item to the filmstrip and indicate the current
+ * progress.</li>
+ * <li>If the final result is already available and no processing is required,
+ * store the final image using saveAndFinish</li>
+ * <li>For longer operations, update the thumbnail and status message using the
+ * provided methods.</li>
+ * <li>For longer operations, update the thumbnail and status message using the
+ * provided methods.</li>
+ * <li>Once processing is done, the final image can be saved using saveAndFinish
+ * </li>
+ * </ul>
+ * </p>
+ * It's OK to call saveAndFinish either before or after the session has been
+ * started.
+ * <p>
+ * If startSession is called after the session has been finished, it will be
+ * treated as a no-op.
+ * </p>
  */
 public class CaptureSessionManagerImpl implements CaptureSessionManager {
 
-    private static final Log.Tag TAG = new Log.Tag("CaptureSessMgrImpl");
-    public static final String TEMP_SESSIONS = "TEMP_SESSIONS";
-
     private class CaptureSessionImpl implements CaptureSession {
-        /** A URI of the item being processed. */
-        private Uri mUri;
         /** The title of the item being processed. */
         private final String mTitle;
-        /** The location this session was created at. Used for media store. */
-        private Location mLocation;
-        /** The current progress of this session in percent. */
-        private int mProgressPercent = 0;
-        /** A message ID for the current progress state. */
-        private CharSequence mProgressMessage;
-        /** A place holder for this capture session. */
-        private PlaceholderManager.Session mPlaceHolderSession;
-        private boolean mNoPlaceHolderRequired = false;
-        private Uri mContentUri;
         /** These listeners get informed about progress updates. */
-        private final HashSet<ProgressListener> mProgressListeners =
-                new HashSet<ProgressListener>();
+        private final HashSet<ProgressListener> mProgressListeners = new HashSet<>();
         private final long mSessionStartMillis;
         /**
          * The path that can be used to write the final JPEG output temporarily,
          * before it is copied to the final location.
          */
         private final String mTempOutputPath;
-
         /** Saver that is used to store a stack of images. */
         private final StackSaver mStackSaver;
+        /** A URI of the item being processed. */
+        private Uri mUri;
+        /** The location this session was created at. Used for media store. */
+        private Location mLocation;
+        /** The current progress of this session in percent. */
+        private int mProgressPercent = 0;
+        /** A message ID for the current progress state. */
+        private CharSequence mProgressMessage;
+        /** A place holder for this capture session. */
+        private PlaceholderManager.Session mPlaceHolderSession;
+        private Uri mContentUri;
+        /** Whether this image was finished. */
+        private volatile boolean mIsFinished;
 
         /**
          * Creates a new {@link CaptureSession}.
@@ -82,14 +104,18 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
          * @param sessionStartMillis the timestamp of this capture session
          *            (since epoch).
          * @param location the location of this session, used for media store.
+         * @throws IOException in case the storage required to store session
+         *             data is not available.
          */
         private CaptureSessionImpl(String title, long sessionStartMillis, Location location,
-                StackSaver stackSaver) {
+                StackSaver stackSaver) throws IOException {
             mTitle = title;
             mSessionStartMillis = sessionStartMillis;
             mLocation = location;
-            mTempOutputPath = createTempOutputPath(mTitle);
+            mTempOutputPath = mSessionStorageManager.createTemporaryOutputPath(TEMP_SESSIONS,
+                    mTitle);
             mStackSaver = stackSaver;
+            mIsFinished = false;
         }
 
         @Override
@@ -108,6 +134,11 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
         }
 
         @Override
+        public synchronized int getProgress() {
+            return mProgressPercent;
+        }
+
+        @Override
         public synchronized void setProgress(int percent) {
             mProgressPercent = percent;
             notifyTaskProgress(mUri, mProgressPercent);
@@ -117,11 +148,6 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
         }
 
         @Override
-        public synchronized int getProgress() {
-            return mProgressPercent;
-        }
-
-        @Override
         public synchronized CharSequence getProgressMessage() {
             return mProgressMessage;
         }
@@ -136,23 +162,48 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
         }
 
         @Override
-        public void startEmpty() {
-            mNoPlaceHolderRequired = true;
+        public void updateThumbnail(Bitmap bitmap) {
+            mPlaceholderManager.replacePlaceholder(mPlaceHolderSession, bitmap);
+            notifyTaskQueued(mUri);
+            onPreviewAvailable();
+        }
+
+        @Override
+        public synchronized void startEmpty(Size pictureSize) {
+            if (mIsFinished) {
+                return;
+            }
+
+            mProgressMessage = "";
+            mPlaceHolderSession = mPlaceholderManager.insertEmptyPlaceholder(mTitle, pictureSize,
+                    mSessionStartMillis);
+            mUri = mPlaceHolderSession.outputUri;
+            putSession(mUri, this);
+            notifyTaskQueued(mUri);
+            onPreviewAvailable();
         }
 
         @Override
         public synchronized void startSession(Bitmap placeholder, CharSequence progressMessage) {
-            mProgressMessage = progressMessage;
+            if (mIsFinished) {
+                return;
+            }
 
+            mProgressMessage = progressMessage;
             mPlaceHolderSession = mPlaceholderManager.insertPlaceholder(mTitle, placeholder,
                     mSessionStartMillis);
             mUri = mPlaceHolderSession.outputUri;
             putSession(mUri, this);
             notifyTaskQueued(mUri);
+            onPreviewAvailable();
         }
 
         @Override
         public synchronized void startSession(byte[] placeholder, CharSequence progressMessage) {
+            if (mIsFinished) {
+                return;
+            }
+
             mProgressMessage = progressMessage;
 
             mPlaceHolderSession = mPlaceholderManager.insertPlaceholder(mTitle, placeholder,
@@ -160,6 +211,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
             mUri = mPlaceHolderSession.outputUri;
             putSession(mUri, this);
             notifyTaskQueued(mUri);
+            onPreviewAvailable();
         }
 
         @Override
@@ -182,19 +234,13 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
         @Override
         public synchronized void saveAndFinish(byte[] data, int width, int height, int orientation,
                 ExifInterface exif, final OnMediaSavedListener listener) {
-            if (mNoPlaceHolderRequired) {
+            mIsFinished = true;
+            if (mPlaceHolderSession == null) {
                 mMediaSaver.addImage(
-                        data, mTitle, mSessionStartMillis, null, width, height,
+                        data, mTitle, mSessionStartMillis, mLocation, width, height,
                         orientation, exif, listener, mContentResolver);
                 return;
             }
-
-            if (mPlaceHolderSession == null) {
-                throw new IllegalStateException(
-                        "Cannot call saveAndFinish without calling startSession first.");
-            }
-
-            // TODO: This needs to happen outside the UI thread.
             mContentUri = mPlaceholderManager.finishPlaceholder(mPlaceHolderSession, mLocation,
                     orientation, exif, data, width, height, FilmstripItemData.MIME_TYPE_JPEG);
 
@@ -214,6 +260,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
                         "Cannot call finish without calling startSession first.");
             }
 
+            mIsFinished = true;
             AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
                 @Override
                 public void run() {
@@ -254,70 +301,12 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
             return mTempOutputPath;
         }
 
-        /**
-         * Initializes the directories for storing the final output temporarily
-         * before it is copied to the final location after calling
-         * {@link #finish()}.
-         * <p>
-         * This method will make sure the directories and file exists and is
-         * writeable, otherwise it will throw an exception.
-         *
-         * @param title the title of this session. Will be used to create a
-         *            unique sub-directory.
-         * @return The path to a JPEG file which can be used to write the final
-         *         output to.
-         */
-        private String createTempOutputPath(String title) {
-            File tempDirectory = null;
-            try {
-                tempDirectory = new File(
-                        getSessionDirectory(TEMP_SESSIONS), title);
-            } catch (IOException e) {
-                Log.e(TAG, "Could not get temp session directory", e);
-                throw new RuntimeException("Could not get temp session directory", e);
-            }
-            if (!tempDirectory.mkdirs()) {
-                throw new IllegalStateException("Could not create output data directory.");
-            }
-            File tempFile = new File(tempDirectory, mTitle + ".jpg");
-            try {
-                if (!tempFile.exists()) {
-                    if (!tempFile.createNewFile()) {
-                        throw new IllegalStateException("Could not create output data file.");
-                    }
-                }
-            } catch (IOException e) {
-                Log.e(TAG, "Could not create temp session file", e);
-                throw new RuntimeException("Could not create temp session file", e);
-            }
-
-            if (!tempFile.canWrite()) {
-                throw new RuntimeException("Temporary output file is not writeable.");
-            }
-            return tempFile.getPath();
-        }
-
         @Override
         public Uri getUri() {
             return mUri;
         }
 
         @Override
-        public Uri getContentUri() {
-            return mContentUri;
-        }
-
-        @Override
-        public boolean isStarted() {
-            return mUri != null;
-        }
-
-        @Override
-        public void onPreviewAvailable() {
-            notifySessionPreviewAvailable(mPlaceHolderSession.outputUri);
-        }
-
-        @Override
         public void updatePreview(String previewPath) {
             final String path = this.getTempOutputPath();
             AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@@ -364,13 +353,23 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
         public void removeProgressListener(ProgressListener listener) {
             mProgressListeners.remove(listener);
         }
-    }
 
+        private void onPreviewAvailable() {
+            notifySessionPreviewAvailable(mPlaceHolderSession.outputUri);
+        }
+
+        private boolean isStarted() {
+            return mUri != null;
+        }
+    }
+    public static final String TEMP_SESSIONS = "TEMP_SESSIONS";
+    private static final Log.Tag TAG = new Log.Tag("CaptureSessMgrImpl");
     private final MediaSaver mMediaSaver;
     private final ContentResolver mContentResolver;
     private final PlaceholderManager mPlaceholderManager;
     private final SessionStorageManager mSessionStorageManager;
     private final StackSaverFactory mStackSaverFactory;
+    private static final Bitmap NO_PREVIEW_BITMAP = null;
 
     /** Failed session messages. Uri -> message. */
     private final HashMap<Uri, CharSequence> mFailedSessionMessages =
@@ -379,7 +378,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
     /**
      * We use this to fire events to the session listeners from the main thread.
      */
-    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+    private final MainThread mMainHandler;
 
     /** Sessions in progress, keyed by URI. */
     private final Map<String, CaptureSession> mSessions;
@@ -399,27 +398,23 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      */
     public CaptureSessionManagerImpl(MediaSaver mediaSaver, ContentResolver contentResolver,
             PlaceholderManager placeholderManager, SessionStorageManager sessionStorageManager,
-            StackSaverFactory stackSaverProvider) {
+            StackSaverFactory stackSaverProvider, MainThread mainHandler) {
         mSessions = new HashMap<String, CaptureSession>();
         mMediaSaver = mediaSaver;
         mContentResolver = contentResolver;
         mPlaceholderManager = placeholderManager;
         mSessionStorageManager = sessionStorageManager;
         mStackSaverFactory = stackSaverProvider;
+        mMainHandler = mainHandler;
     }
 
     @Override
-    public CaptureSession createNewSession(String title, long sessionStartTime, Location location) {
+    public CaptureSession createNewSession(String title, long sessionStartTime, Location location) throws IOException {
         return new CaptureSessionImpl(title, sessionStartTime, location, mStackSaverFactory.create(
                 title, location));
     }
 
     @Override
-    public CaptureSession createSession() {
-        return new CaptureSessionImpl(null, System.currentTimeMillis(), null, null);
-    }
-
-    @Override
     public void putSession(Uri sessionUri, CaptureSession session) {
         synchronized (mSessions) {
             mSessions.put(sessionUri.toString(), session);
@@ -463,7 +458,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      * queued.
      */
     private void notifyTaskQueued(final Uri uri) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mTaskListeners) {
@@ -480,7 +475,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      * finished.
      */
     private void notifyTaskDone(final Uri uri) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mTaskListeners) {
@@ -497,7 +492,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      * failed to process.
      */
     private void notifyTaskFailed(final Uri uri, final CharSequence reason) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mTaskListeners) {
@@ -514,7 +509,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      * progressed to the given state.
      */
     private void notifyTaskProgress(final Uri uri, final int progressPercent) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mTaskListeners) {
@@ -531,7 +526,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      * its progress message.
      */
     private void notifyTaskProgressText(final Uri uri, final CharSequence message) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mTaskListeners) {
@@ -548,7 +543,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
      * its media.
      */
     private void notifySessionPreviewAvailable(final Uri uri) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mTaskListeners) {
@@ -577,7 +572,7 @@ public class CaptureSessionManagerImpl implements CaptureSessionManager {
 
     @Override
     public void fillTemporarySession(final SessionListener listener) {
-        mMainHandler.post(new Runnable() {
+        mMainHandler.execute(new Runnable() {
             @Override
             public void run() {
                 synchronized (mSessions) {
index ab1b1c8..4f283ee 100644 (file)
@@ -29,6 +29,7 @@ import com.android.camera.Storage;
 import com.android.camera.debug.Log;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.util.CameraUtil;
+import com.android.camera.util.Size;
 
 /**
  * Handles placeholders in filmstrip that show up temporarily while a final
@@ -56,6 +57,20 @@ public class PlaceholderManager {
     }
 
     /**
+     * Adds an empty placeholder.
+     *
+     * @param title the title of the item
+     * @param size the size of the placeholder in pixels.
+     * @param timestamp the timestamp of the placeholder (used for ordering
+     *            within the filmstrip). Millis since epoch.
+     * @return A session instance representing the new placeholder.
+     */
+    public Session insertEmptyPlaceholder(String title, Size size, long timestamp) {
+        Uri uri =  Storage.addEmptyPlaceholder(size);
+        return new Session(title, uri, timestamp);
+    }
+
+    /**
      * Inserts a new placeholder into the filmstrip.
      *
      * @param title the title of the item
index 8520c1a..33082fd 100644 (file)
@@ -32,10 +32,26 @@ public interface SessionStorageManager {
      * Before returning, this method will make sure the returned directory is
      * clean of expired session data.
      *
-     * @param subFolder The subfolder to use/create within the temporary session
-     *            space, e.g. "foo".
+     * @param subDirectory The subdirectory to use/create within the temporary
+     *            session space, e.g. "foo".
      * @return A valid file object that points to an existing directory.
      * @throws IOException If the directory could not be made available.
      */
     public File getSessionDirectory(String subDirectory) throws IOException;
+
+    /**
+     * Initializes the directories for storing the final session output
+     * temporarily before it is copied to the final location after calling
+     * {@link #finish()}.
+     * <p>
+     * This method will make sure the directories and file exists and is
+     * writeable, otherwise it will throw an exception.
+     *
+     * @param title the title of this session. Will be used to create a unique
+     *            sub-directory.
+     * @return The path to a JPEG file which can be used to write the final
+     *         output to.
+     * @throws IOException If the directory could not be created.
+     */
+    public String createTemporaryOutputPath(String subDirectory, String title) throws IOException;
 }
index ea7a907..d2e7fbf 100644 (file)
@@ -81,6 +81,37 @@ public class SessionStorageManagerImpl implements SessionStorageManager {
         return sessionDirectory;
     }
 
+    @Override
+    public String createTemporaryOutputPath(String subDirectory, String title) throws IOException {
+        File tempDirectory = null;
+        try {
+            tempDirectory = new File(
+                    getSessionDirectory(subDirectory), title);
+        } catch (IOException e) {
+            Log.e(TAG, "Could not get temp session directory", e);
+            throw new IOException("Could not get temp session directory", e);
+        }
+        if (!tempDirectory.mkdirs()) {
+            throw new IOException("Could not create output data directory.");
+        }
+        File tempFile = new File(tempDirectory, title + ".jpg");
+        try {
+            if (!tempFile.exists()) {
+                if (!tempFile.createNewFile()) {
+                    throw new IOException("Could not create output data file.");
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Could not create temp session file", e);
+            throw new IOException("Could not create temp session file", e);
+        }
+
+        if (!tempFile.canWrite()) {
+            throw new IOException("Temporary output file is not writeable.");
+        }
+        return tempFile.getPath();
+    }
+
     /**
      * Goes through all temporary sessions and deletes the ones that are older
      * than a certain age.