Add a capture-session API to be used by all modules.

  Bug: 11747284

This refactors the way we think of sessions. Instead of
having multiple session and notification APIs being used
by different modules, we create on central capture session
API to be used by all modules.

This also adds a first implementation of a Memory API which
tells modules about the current memory situation.

Change-Id: I7f030e32fc2f70f4007825ba7bbbdce9521a2bd9
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
index fe4545c..094e2bc 100644
--- a/src/com/android/camera/CameraActivity.java
+++ b/src/com/android/camera/CameraActivity.java
@@ -65,22 +65,19 @@
 import android.widget.PopupWindow;
 import android.widget.ProgressBar;
 import android.widget.ShareActionProvider;
+import android.widget.TextView;
 
 import com.android.camera.app.AppController;
-import com.android.camera.app.AppManagerFactory;
 import com.android.camera.app.CameraAppUI;
 import com.android.camera.app.CameraController;
 import com.android.camera.app.CameraManager;
 import com.android.camera.app.CameraManagerFactory;
 import com.android.camera.app.CameraProvider;
 import com.android.camera.app.CameraServices;
-import com.android.camera.app.ImageTaskManager;
 import com.android.camera.app.MediaSaver;
 import com.android.camera.app.ModuleManagerImpl;
 import com.android.camera.app.OrientationManager;
 import com.android.camera.app.OrientationManagerImpl;
-import com.android.camera.app.PanoramaStitchingManager;
-import com.android.camera.app.PlaceholderManager;
 import com.android.camera.crop.CropActivity;
 import com.android.camera.data.CameraDataAdapter;
 import com.android.camera.data.FixedLastDataAdapter;
@@ -93,11 +90,13 @@
 import com.android.camera.filmstrip.FilmstripContentPanel;
 import com.android.camera.filmstrip.FilmstripController;
 import com.android.camera.module.ModulesInfo;
+import com.android.camera.session.CaptureSessionManager;
+import com.android.camera.session.CaptureSessionManager.SessionListener;
+import com.android.camera.session.PlaceholderManager;
 import com.android.camera.settings.SettingsManager;
 import com.android.camera.settings.SettingsManager.SettingsCapabilities;
 import com.android.camera.tinyplanet.TinyPlanetFragment;
 import com.android.camera.ui.DetailsDialog;
-import com.android.camera.widget.FilmstripView;
 import com.android.camera.ui.MainActivityLayout;
 import com.android.camera.ui.ModeListView;
 import com.android.camera.ui.PreviewStatusListener;
@@ -109,6 +108,7 @@
 import com.android.camera.util.IntentHelper;
 import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper;
 import com.android.camera.util.UsageStatistics;
+import com.android.camera.widget.FilmstripView;
 import com.android.camera2.R;
 
 import java.io.File;
@@ -170,10 +170,15 @@
      */
     private LocalDataAdapter mDataAdapter;
 
+    /**
+     * TODO: This should be moved to the app level.
+     */
     private SettingsManager mSettingsManager;
+
+    /**
+     * TODO: This should be moved to the app level.
+     */
     private SettingsController mSettingsController;
-    private PanoramaStitchingManager mPanoramaManager;
-    private PlaceholderManager mPlaceholderManager;
     private ModeListView mModeListView;
     private int mCurrentModeIndex;
     private CameraModule mCurrentModule;
@@ -181,8 +186,9 @@
     private FrameLayout mAboveFilmstripControlLayout;
     private FilmstripController mFilmstripController;
     private boolean mFilmstripVisible;
-    private ProgressBar mBottomProgress;
-    private View mPanoStitchingPanel;
+    private TextView mBottomProgressText;
+    private ProgressBar mBottomProgressBar;
+    private View mSessionProgressPanel;
     private int mResultCodeForTesting;
     private Intent mResultDataForTesting;
     private OnScreenHint mStorageHint;
@@ -237,7 +243,7 @@
      */
     private boolean mKeepScreenOn;
     private int mLastLayoutOrientation;
-    private CameraAppUI.BottomControls.Listener mMyFilmstripBottomControlListener =
+    private final CameraAppUI.BottomControls.Listener mMyFilmstripBottomControlListener =
             new CameraAppUI.BottomControls.Listener() {
 
                 /**
@@ -446,7 +452,6 @@
                         }
                     });
                 }
-
             };
 
     public void gotoGallery() {
@@ -482,16 +487,17 @@
         }
     }
 
-    private void hidePanoStitchingProgress() {
-        mPanoStitchingPanel.setVisibility(View.GONE);
+    private void hideSessionProgress() {
+        mSessionProgressPanel.setVisibility(View.GONE);
     }
 
-    private void showPanoStitchingProgress() {
-        mPanoStitchingPanel.setVisibility(View.VISIBLE);
+    private void showSessionProgress(CharSequence message) {
+        mBottomProgressText.setText(message);
+        mSessionProgressPanel.setVisibility(View.VISIBLE);
     }
 
-    private void updateStitchingProgress(int progress) {
-        mBottomProgress.setProgress(progress);
+    private void updateSessionProgress(int progress) {
+        mBottomProgressBar.setProgress(progress);
     }
 
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@@ -694,50 +700,15 @@
         }
     }
 
-    private final ImageTaskManager.TaskListener mPlaceholderListener =
-            new ImageTaskManager.TaskListener() {
-
+    private final SessionListener mSessionListener =
+            new SessionListener() {
                 @Override
-                public void onTaskQueued(String filePath, final Uri imageUri) {
+                public void onSessionQueued(final Uri uri) {
                     mMainHandler.post(new Runnable() {
                         @Override
                         public void run() {
-                            notifyNewMedia(imageUri);
-                            int dataID = mDataAdapter.findDataByContentUri(imageUri);
-                            if (dataID != -1) {
-                                LocalData d = mDataAdapter.getLocalData(dataID);
-                                InProgressDataWrapper newData = new InProgressDataWrapper(d, true);
-                                mDataAdapter.updateData(dataID, newData);
-                            }
-                        }
-                    });
-                }
-
-                @Override
-                public void onTaskDone(String filePath, final Uri imageUri) {
-                    mMainHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            mDataAdapter.refresh(getContentResolver(), imageUri);
-                        }
-                    });
-                }
-
-                @Override
-                public void onTaskProgress(String filePath, Uri imageUri, int progress) {
-                    // Do nothing
-                }
-            };
-
-    private final ImageTaskManager.TaskListener mStitchingListener =
-            new ImageTaskManager.TaskListener() {
-                @Override
-                public void onTaskQueued(String filePath, final Uri imageUri) {
-                    mMainHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            notifyNewMedia(imageUri);
-                            int dataID = mDataAdapter.findDataByContentUri(imageUri);
+                            notifyNewMedia(uri);
+                            int dataID = mDataAdapter.findDataByContentUri(uri);
                             if (dataID != -1) {
                                 // Don't allow special UI actions (swipe to
                                 // delete, for example) on in-progress data.
@@ -750,27 +721,29 @@
                 }
 
                 @Override
-                public void onTaskDone(String filePath, final Uri imageUri) {
-                    Log.v(TAG, "onTaskDone:" + filePath);
+                public void onSessionDone(final Uri uri) {
+                    Log.v(TAG, "onSessionDone:" + uri);
                     mMainHandler.post(new Runnable() {
                         @Override
                         public void run() {
-                            int doneID = mDataAdapter.findDataByContentUri(imageUri);
+                            int doneID = mDataAdapter.findDataByContentUri(uri);
                             int currentDataId = mFilmstripController.getCurrentId();
 
                             if (currentDataId == doneID) {
-                                hidePanoStitchingProgress();
-                                updateStitchingProgress(0);
+                                hideSessionProgress();
+                                updateSessionProgress(0);
                             }
-
-                            mDataAdapter.refresh(getContentResolver(), imageUri);
+                            mDataAdapter.refresh(getContentResolver(), uri);
                         }
                     });
                 }
 
                 @Override
-                public void onTaskProgress(
-                        String filePath, final Uri imageUri, final int progress) {
+                public void onSessionProgress(final Uri uri, final int progress) {
+                    if (progress < 0) {
+                        // Do nothing, there is no task for this URI.
+                        return;
+                    }
                     mMainHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -778,9 +751,9 @@
                             if (currentDataId == -1) {
                                 return;
                             }
-                            if (imageUri.equals(
+                            if (uri.equals(
                                     mDataAdapter.getLocalData(currentDataId).getContentUri())) {
-                                updateStitchingProgress(progress);
+                                updateSessionProgress(progress);
                             }
                         }
                     });
@@ -889,8 +862,6 @@
         } else if (mimeType.startsWith("image/")) {
             CameraUtil.broadcastNewPicture(this, uri);
             mDataAdapter.addNewPhoto(cr, uri);
-        } else if (mimeType.startsWith("application/stitching-preview")) {
-            mDataAdapter.addNewPhoto(cr, uri);
         } else if (mimeType.startsWith(PlaceholderManager.PLACEHOLDER_MIME_TYPE)) {
             mDataAdapter.addNewPhoto(cr, uri);
         } else {
@@ -1120,14 +1091,12 @@
 
         mAboveFilmstripControlLayout =
                 (FrameLayout) findViewById(R.id.camera_filmstrip_content_layout);
-        mPanoramaManager = AppManagerFactory.getInstance(this)
-                .getPanoramaStitchingManager();
-        mPlaceholderManager = AppManagerFactory.getInstance(this)
-                .getGcamProcessingManager();
-        mPanoramaManager.addTaskListener(mStitchingListener);
-        mPlaceholderManager.addTaskListener(mPlaceholderListener);
-        mPanoStitchingPanel = findViewById(R.id.pano_stitching_progress_panel);
-        mBottomProgress = (ProgressBar) findViewById(R.id.pano_stitching_progress_bar);
+
+        // Add the session listener so we can track the session progress updates.
+        getServices().getCaptureSessionManager().addSessionListener(mSessionListener);
+        mSessionProgressPanel = findViewById(R.id.pano_session_progress_panel);
+        mBottomProgressBar = (ProgressBar) findViewById(R.id.pano_session_progress_bar);
+        mBottomProgressText = (TextView) findViewById(R.id.pano_session_progress_text);
         mFilmstripController = ((FilmstripView) findViewById(R.id.filmstrip_view)).getController();
         mFilmstripController.setImageGap(
                 getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap));
@@ -1533,9 +1502,6 @@
 
         openModule(mCurrentModule);
         mCurrentModule.onOrientationChanged(mLastRawOrientation);
-        if (mMediaSaver != null) {
-            mCurrentModule.onMediaSaverAvailable(mMediaSaver);
-        }
         // Store the module index so we can use it the next time the Camera
         // starts up.
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
@@ -1838,7 +1804,7 @@
         final LocalData currentData = mDataAdapter.getLocalData(dataId);
         if (currentData == null) {
             Log.w(TAG, "Current data ID not found.");
-            hidePanoStitchingProgress();
+            hideSessionProgress();
             return;
         }
 
@@ -1853,13 +1819,18 @@
 
         /* Progress bar */
 
-        final int panoStitchingProgress =
-                mPanoramaManager.getTaskProgress(currentData.getContentUri());
-        if (panoStitchingProgress < 0) {
-            hidePanoStitchingProgress();
+        Uri contentUri = currentData.getContentUri();
+        CaptureSessionManager sessionManager = getServices()
+                .getCaptureSessionManager();
+        int sessionProgress = sessionManager.getSessionProgress(contentUri);
+
+        if (sessionProgress < 0) {
+            hideSessionProgress();
         } else {
-            showPanoStitchingProgress();
-            updateStitchingProgress(panoStitchingProgress);
+            CharSequence progressMessage = sessionManager
+                    .getSessionProgressMessage(contentUri);
+            showSessionProgress(progressMessage);
+            updateSessionProgress(sessionProgress);
         }
 
         /* View button */
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
index b6be301..2c6a8d7 100644
--- a/src/com/android/camera/CameraModule.java
+++ b/src/com/android/camera/CameraModule.java
@@ -16,15 +16,12 @@
 
 package com.android.camera;
 
-import android.content.Intent;
-import android.content.res.Configuration;
 import android.view.KeyEvent;
 import android.view.View;
 
 import com.android.camera.app.AppController;
 import com.android.camera.app.CameraProvider;
 import com.android.camera.app.CameraServices;
-import com.android.camera.app.MediaSaver;
 import com.android.camera.module.ModuleController;
 
 public abstract class CameraModule implements ModuleController {
@@ -57,9 +54,6 @@
     @Deprecated
     public abstract void onSingleTapUp(View view, int x, int y);
 
-    @Deprecated
-    public abstract void onMediaSaverAvailable(MediaSaver s);
-
     /**
      * @return An instance containing common services to be used by the module.
      */
diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java
index 8c20b76..2f3b2f6 100644
--- a/src/com/android/camera/FocusOverlayManager.java
+++ b/src/com/android/camera/FocusOverlayManager.java
@@ -76,22 +76,22 @@
     private boolean mMeteringAreaSupported;
     private boolean mLockAeAwbNeeded;
     private boolean mAeAwbLock;
-    private Matrix mMatrix;
+    private final Matrix mMatrix;
 
     private boolean mMirror; // true if the camera is front-facing.
     private int mDisplayOrientation;
-    private List<Object> mFocusArea; // focus area in driver format
-    private List<Object> mMeteringArea; // metering area in driver format
+    private List<Area> mFocusArea; // focus area in driver format
+    private List<Area> mMeteringArea; // metering area in driver format
     private String mFocusMode;
-    private String[] mDefaultFocusModes;
+    private final String[] mDefaultFocusModes;
     private String mOverrideFocusMode;
     private Parameters mParameters;
-    private SettingsManager mSettingsManager;
-    private Handler mHandler;
+    private final SettingsManager mSettingsManager;
+    private final Handler mHandler;
     Listener mListener;
     private boolean mPreviousMoving;
 
-    private FocusUI mUI;
+    private final FocusUI mUI;
     private final Rect mPreviewRect = new Rect(0, 0, 0, 0);
 
     public  interface FocusUI {
@@ -330,25 +330,25 @@
     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
     private void initializeFocusAreas(int x, int y) {
         if (mFocusArea == null) {
-            mFocusArea = new ArrayList<Object>();
+            mFocusArea = new ArrayList<Area>();
             mFocusArea.add(new Area(new Rect(), 1));
         }
 
         // Convert the coordinates to driver format.
-        calculateTapArea(x, y, 1f, ((Area) mFocusArea.get(0)).rect);
+        calculateTapArea(x, y, 1f, mFocusArea.get(0).rect);
     }
 
     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
     private void initializeMeteringAreas(int x, int y) {
         if (mMeteringArea == null) {
-            mMeteringArea = new ArrayList<Object>();
+            mMeteringArea = new ArrayList<Area>();
             mMeteringArea.add(new Area(new Rect(), 1));
         }
 
         // Convert the coordinates to driver format.
         // AE area is bigger because exposure is sensitive and
         // easy to over- or underexposure if area is too small.
-        calculateTapArea(x, y, 1.5f, ((Area) mMeteringArea.get(0)).rect);
+        calculateTapArea(x, y, 1.5f, mMeteringArea.get(0).rect);
     }
 
     private void resetMeteringAreas() {
@@ -477,11 +477,11 @@
         return mFocusMode;
     }
 
-    public List getFocusAreas() {
+    public List<Area> getFocusAreas() {
         return mFocusArea;
     }
 
-    public List getMeteringAreas() {
+    public List<Area> getMeteringAreas() {
         return mMeteringArea;
     }
 
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index f9d38a6..8776f6b 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -56,6 +56,8 @@
 import com.android.camera.app.CameraManager.CameraProxy;
 import com.android.camera.app.CameraManager.CameraShutterCallback;
 import com.android.camera.app.MediaSaver;
+import com.android.camera.app.MemoryManager;
+import com.android.camera.app.MemoryManager.MemoryListener;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.exif.ExifTag;
 import com.android.camera.exif.Rational;
@@ -82,8 +84,9 @@
         extends CameraModule
         implements PhotoController,
         ModuleController,
+        MemoryListener,
         FocusOverlayManager.Listener,
-        ShutterButton.OnShutterButtonListener, MediaSaver.QueueListener,
+        ShutterButton.OnShutterButtonListener,
         SensorEventListener {
 
     private static final String TAG = "CAM_PhotoModule";
@@ -137,7 +140,7 @@
 
     private static final int SCREEN_DELAY = 2 * 60 * 1000;
 
-    private int mZoomValue;  // The current zoom value.
+    private int mZoomValue; // The current zoom value.
 
     private Parameters mInitialParams;
     private boolean mFocusAreaSupported;
@@ -171,10 +174,8 @@
     };
 
     /**
-     * An unpublished intent flag requesting to return as soon as capturing
-     * is completed.
-     *
-     * TODO: consider publishing by moving into MediaStore.
+     * An unpublished intent flag requesting to return as soon as capturing is
+     * completed. TODO: consider publishing by moving into MediaStore.
      */
     private static final String EXTRA_QUICK_CAPTURE =
             "android.intent.extra.quickCapture";
@@ -244,7 +245,10 @@
     private final float[] mR = new float[16];
     private int mHeading = -1;
 
-    // True if all the parameters needed to start preview is ready.
+    /** Whether shutter is enabled. */
+    private boolean mShutterEnabled;
+
+    /** True if all the parameters needed to start preview is ready. */
     private boolean mCameraPreviewParamsReady = false;
 
     private final MediaSaver.OnMediaSavedListener mOnMediaSavedListener =
@@ -315,7 +319,8 @@
 
                 case MSG_SWITCH_CAMERA_START_ANIMATION: {
                     // TODO: Need to revisit
-                    // ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+                    // ((CameraScreenNail)
+                    // mActivity.mCameraScreenNail).animateSwitchCamera();
                     break;
                 }
 
@@ -373,7 +378,7 @@
         initializeControlByIntent();
         mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
         mLocationManager = mActivity.getLocationManager();
-        mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE));
+        mSensorManager = (SensorManager) (mActivity.getSystemService(Context.SENSOR_SERVICE));
         mAppController = app;
     }
 
@@ -399,7 +404,9 @@
         if (settingsManager.isSet(SettingsManager.SETTING_RECORD_LOCATION)) {
             return;
         }
-        if (mActivity.isSecureCamera()) return;
+        if (mActivity.isSecureCamera()) {
+            return;
+        }
         // Check if the back camera exists
         int backCameraId = CameraHolder.instance().getBackCameraId();
         if (backCameraId == -1) {
@@ -434,7 +441,9 @@
     }
 
     private void switchCamera() {
-        if (mPaused) return;
+        if (mPaused) {
+            return;
+        }
         SettingsManager settingsManager = mActivity.getSettingsManager();
 
         Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId);
@@ -443,7 +452,9 @@
         settingsManager.set(SettingsManager.SETTING_CAMERA_ID, "" + mCameraId);
         mActivity.getCameraProvider().requestCamera(mCameraId);
         mUI.clearFaces();
-        if (mFocusManager != null) mFocusManager.removeMessages();
+        if (mFocusManager != null) {
+            mFocusManager.removeMessages();
+        }
 
         // TODO: this needs to be brought into onCameraAvailable();
         CameraInfo info = mActivity.getCameraProvider().getCameraInfo()[mCameraId];
@@ -492,7 +503,8 @@
 
     @Override
     public void onPreviewRectChanged(Rect previewRect) {
-        if (mFocusManager != null) mFocusManager.setPreviewRect(previewRect);
+        if (mFocusManager != null)
+            mFocusManager.setPreviewRect(previewRect);
     }
 
     private void resetExposureCompensation() {
@@ -517,12 +529,10 @@
         settingsController.syncLocationManager();
 
         mUI.initializeFirstTime();
-        MediaSaver s = getServices().getMediaSaver();
+
         // We set the listener only when both service and shutterbutton
         // are initialized.
-        if (s != null) {
-            s.setQueueListener(this);
-        }
+        getServices().getMemoryManager().addListener(this);
 
         mNamedImages = new NamedImages();
 
@@ -539,10 +549,7 @@
         SettingsController settingsController = mActivity.getSettingsController();
         settingsController.syncLocationManager();
 
-        MediaSaver s = getServices().getMediaSaver();
-        if (s != null) {
-            s.setQueueListener(this);
-        }
+        getServices().getMemoryManager().addListener(this);
         mNamedImages = new NamedImages();
         mUI.initializeSecondTime(mParameters);
     }
@@ -569,7 +576,9 @@
 
     @Override
     public void startFaceDetection() {
-        if (mFaceDetectionStarted) return;
+        if (mFaceDetectionStarted) {
+            return;
+        }
         if (mParameters.getMaxNumDetectedFaces() > 0) {
             mFaceDetectionStarted = true;
             CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
@@ -582,7 +591,9 @@
 
     @Override
     public void stopFaceDetection() {
-        if (!mFaceDetectionStarted) return;
+        if (!mFaceDetectionStarted) {
+            return;
+        }
         if (mParameters.getMaxNumDetectedFaces() > 0) {
             mFaceDetectionStarted = false;
             mCameraDevice.setFaceDetectionCallback(null, null);
@@ -619,7 +630,7 @@
     private final class PostViewPictureCallback
             implements CameraPictureCallback {
         @Override
-        public void onPictureTaken(byte [] data, CameraProxy camera) {
+        public void onPictureTaken(byte[] data, CameraProxy camera) {
             mPostViewPictureCallbackTime = System.currentTimeMillis();
             Log.v(TAG, "mShutterToPostViewCallbackTime = "
                     + (mPostViewPictureCallbackTime - mShutterCallbackTime)
@@ -630,7 +641,7 @@
     private final class RawPictureCallback
             implements CameraPictureCallback {
         @Override
-        public void onPictureTaken(byte [] rawData, CameraProxy camera) {
+        public void onPictureTaken(byte[] rawData, CameraProxy camera) {
             mRawPictureCallbackTime = System.currentTimeMillis();
             Log.v(TAG, "mShutterToRawCallbackTime = "
                     + (mRawPictureCallbackTime - mShutterCallbackTime) + "ms");
@@ -646,8 +657,8 @@
         }
 
         @Override
-        public void onPictureTaken(final byte [] jpegData, CameraProxy camera) {
-            mUI.enableShutter(true);
+        public void onPictureTaken(final byte[] jpegData, CameraProxy camera) {
+            setShutterEnabled(true);
             if (mPaused) {
                 return;
             }
@@ -713,7 +724,8 @@
                 if (title == null) {
                     Log.e(TAG, "Unbalanced name/data pair");
                 } else {
-                    if (date == -1) date = mCaptureStartTime;
+                    if (date == -1)
+                        date = mCaptureStartTime;
                     if (mHeading >= 0) {
                         // heading direction has been updated by the sensor.
                         ExifTag directionRefTag = exif.buildTag(
@@ -729,7 +741,8 @@
                             jpegData, title, date, mLocation, width, height,
                             orientation, exif, mOnMediaSavedListener, mContentResolver);
                 }
-                // Animate capture with real jpeg data instead of a preview frame.
+                // Animate capture with real jpeg data instead of a preview
+                // frame.
                 mUI.animateCapture(jpegData, orientation, mMirror);
             } else {
                 mJpegImageData = jpegData;
@@ -741,9 +754,9 @@
             }
 
             // Check this in advance of each shot so we don't add to shutter
-            // latency. It's true that someone else could write to the SD card in
-            // the mean time and fill it, but that could have happened between the
-            // shutter press and saving the JPEG too.
+            // latency. It's true that someone else could write to the SD card
+            // in the mean time and fill it, but that could have happened
+            // between the shutter press and saving the JPEG too.
             mActivity.updateStorageSpaceAndHint();
 
             long now = System.currentTimeMillis();
@@ -758,7 +771,9 @@
         @Override
         public void onAutoFocus(
                 boolean focused, CameraProxy camera) {
-            if (mPaused) return;
+            if (mPaused) {
+                return;
+            }
 
             mAutoFocusTime = System.currentTimeMillis() - mFocusStartTime;
             Log.v(TAG, "mAutoFocusTime = " + mAutoFocusTime + "ms");
@@ -795,7 +810,7 @@
         }
 
         public NamedEntity getNextNameEntity() {
-            synchronized(mQueue) {
+            synchronized (mQueue) {
                 if (!mQueue.isEmpty()) {
                     return mQueue.remove(0);
                 }
@@ -834,12 +849,10 @@
 
     @Override
     public boolean capture() {
-        // If we are already in the middle of taking a snapshot or the image save request
-        // is full then ignore.
+        // If we are already in the middle of taking a snapshot or the image
+        // save request is full then ignore.
         if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS
-                || mCameraState == SWITCHING_CAMERA
-                || getServices().getMediaSaver() == null
-                || getServices().getMediaSaver().isQueueFull()) {
+                || mCameraState == SWITCHING_CAMERA || !mShutterEnabled) {
             return false;
         }
         mCaptureStartTime = System.currentTimeMillis();
@@ -854,6 +867,7 @@
 
         // Set rotation and gps data.
         int orientation;
+
         // We need to be consistent with the framework orientation (i.e. the
         // orientation of the UI.) when the auto-rotate screen setting is on.
         if (mActivity.isAutoRotateScreen()) {
@@ -869,7 +883,7 @@
 
         // We don't want user to press the button again while taking a
         // multi-second HDR photo.
-        mUI.enableShutter(false);
+        setShutterEnabled(false);
         mCameraDevice.takePicture(mHandler,
                 new ShutterCallback(!animateBefore),
                 mRawPictureCallback, mPostViewPictureCallback,
@@ -902,7 +916,7 @@
     }
 
     private void overrideCameraSettings(final String flashMode,
-                                        final String whiteBalance, final String focusMode) {
+            final String whiteBalance, final String focusMode) {
         SettingsManager settingsManager = mActivity.getSettingsManager();
         settingsManager.set(SettingsManager.SETTING_FLASH_MODE, flashMode);
         settingsManager.set(SettingsManager.SETTING_WHITE_BALANCE, whiteBalance);
@@ -914,7 +928,9 @@
         // We keep the last known orientation. So if the user first orient
         // the camera then point the camera to floor or sky, we still have
         // the correct orientation.
-        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
+        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+            return;
+        }
         mOrientation = CameraUtil.roundOrientation(orientation, mOrientation);
 
         // Show the toast after getting the first orientation changed.
@@ -972,7 +988,7 @@
         byte[] data = mJpegImageData;
 
         if (mCropValue == null) {
-            // First handle the no crop case -- just return the value.  If the
+            // First handle the no crop case -- just return the value. If the
             // caller specifies a "save uri" then write the data to its
             // stream. Otherwise, pass back a scaled down version of the bitmap
             // directly in the extras.
@@ -1049,10 +1065,14 @@
     @Override
     public void onShutterButtonFocus(boolean pressed) {
         if (mPaused || (mCameraState == SNAPSHOT_IN_PROGRESS)
-                || (mCameraState == PREVIEW_STOPPED)) return;
+                || (mCameraState == PREVIEW_STOPPED)) {
+            return;
+        }
 
         // Do not do focus if there is not enough storage.
-        if (pressed && !canTakePicture()) return;
+        if (pressed && !canTakePicture()) {
+            return;
+        }
 
         if (pressed) {
             mFocusManager.onShutterDown();
@@ -1064,7 +1084,9 @@
     @Override
     public void onShutterButtonClick() {
         if (mPaused || (mCameraState == SWITCHING_CAMERA)
-                || (mCameraState == PREVIEW_STOPPED)) return;
+                || (mCameraState == PREVIEW_STOPPED)) {
+            return;
+        }
 
         // Do not take the picture if there is not enough storage.
         if (mActivity.getStorageSpaceBytes() <= Storage.LOW_STORAGE_THRESHOLD_BYTES) {
@@ -1094,7 +1116,9 @@
 
     private void onResumeTasks() {
         Log.v(TAG, "Executing onResumeTasks.");
-        if (mOpenCameraFail || mCameraDisabled) return;
+        if (mOpenCameraFail || mCameraDisabled) {
+            return;
+        }
 
         mActivity.getCameraProvider().requestCamera(mCameraId);
 
@@ -1198,7 +1222,9 @@
 
         mNamedImages = null;
 
-        if (mLocationManager != null) mLocationManager.recordLocation(false);
+        if (mLocationManager != null) {
+            mLocationManager.recordLocation(false);
+        }
 
         // If we are in an image capture intent and has taken
         // a picture, we just clear it in onPause.
@@ -1212,11 +1238,10 @@
         mUI.onPause();
 
         mPendingSwitchCameraId = -1;
-        if (mFocusManager != null) mFocusManager.removeMessages();
-        MediaSaver s = getServices().getMediaSaver();
-        if (s != null) {
-            s.setQueueListener(null);
+        if (mFocusManager != null) {
+            mFocusManager.removeMessages();
         }
+        getServices().getMemoryManager().removeListener(this);
     }
 
     @Override
@@ -1242,7 +1267,8 @@
     }
 
     private boolean canTakePicture() {
-        return isCameraIdle() && (mActivity.getStorageSpaceBytes() > Storage.LOW_STORAGE_THRESHOLD_BYTES);
+        return isCameraIdle()
+                && (mActivity.getStorageSpaceBytes() > Storage.LOW_STORAGE_THRESHOLD_BYTES);
     }
 
     @Override
@@ -1282,7 +1308,7 @@
             case KeyEvent.KEYCODE_VOLUME_UP:
             case KeyEvent.KEYCODE_VOLUME_DOWN:
             case KeyEvent.KEYCODE_FOCUS:
-                if (/*TODO: mActivity.isInCameraApp() &&*/ mFirstTimeInitialized) {
+                if (/* TODO: mActivity.isInCameraApp() && */mFirstTimeInitialized) {
                     if (event.getRepeatCount() == 0) {
                         onShutterButtonFocus(true);
                     }
@@ -1314,7 +1340,7 @@
         switch (keyCode) {
             case KeyEvent.KEYCODE_VOLUME_UP:
             case KeyEvent.KEYCODE_VOLUME_DOWN:
-                if (/*mActivity.isInCameraApp() && */ mFirstTimeInitialized) {
+                if (/* mActivity.isInCameraApp() && */mFirstTimeInitialized) {
                     onShutterButtonClick();
                     return true;
                 }
@@ -1397,7 +1423,7 @@
         }
 
         mCameraDevice.setErrorCallback(mErrorCallback);
-        // ICS camera frameworks has a bug. Face detection state is not cleared 1589
+        // ICS camera frameworks has a bug. Face detection state is not cleared
         // after taking a picture. Stop the preview to work around it. The bug
         // was fixed in JB.
         if (mCameraState != PREVIEW_STOPPED) {
@@ -1407,8 +1433,8 @@
         setDisplayOrientation();
 
         if (!mSnapshotOnIdle) {
-            // If the focus mode is continuous autofocus, call cancelAutoFocus to
-            // resume it because it may have been paused by autoFocus call.
+            // If the focus mode is continuous autofocus, call cancelAutoFocus
+            // to resume it because it may have been paused by autoFocus call.
             String focusMode = mFocusManager.getFocusMode();
             if (CameraUtil.FOCUS_MODE_CONTINUOUS_PICTURE.equals(focusMode)) {
                 mCameraDevice.cancelAutoFocus();
@@ -1438,11 +1464,12 @@
             mFaceDetectionStarted = false;
         }
         setCameraState(PREVIEW_STOPPED);
-        if (mFocusManager != null) mFocusManager.onPreviewStopped();
+        if (mFocusManager != null) {
+            mFocusManager.onPreviewStopped();
+        }
         stopSmartCamera();
     }
 
-    @SuppressWarnings("deprecation")
     private void updateCameraParametersInitialize() {
         // Reset preview frame rate to the maximum because it may be lowered by
         // video camera application.
@@ -1504,7 +1531,7 @@
         setFocusAreasIfSupported();
         setMeteringAreasIfSupported();
 
-        // initialize focus mode
+        // Initialize focus mode.
         mFocusManager.overrideFocusMode(null);
         mParameters.setFocusMode(mFocusManager.getFocusMode());
 
@@ -1541,7 +1568,7 @@
             mParameters = mCameraDevice.getParameters();
         }
 
-        if(optimalSize.width != 0 && optimalSize.height != 0) {
+        if (optimalSize.width != 0 && optimalSize.height != 0) {
             mUI.updatePreviewAspectRatio((float) optimalSize.width
                     / (float) optimalSize.height);
         }
@@ -1723,7 +1750,9 @@
 
     public void onSharedPreferenceChanged() {
         // ignore the events after "onPause()"
-        if (mPaused) return;
+        if (mPaused) {
+            return;
+        }
 
         SettingsController settingsController = mActivity.getSettingsController();
         settingsController.syncLocationManager();
@@ -1750,18 +1779,29 @@
                 CameraUtil.FOCUS_MODE_CONTINUOUS_PICTURE);
     }
 
+    private void setShutterEnabled(boolean enabled) {
+        mShutterEnabled = enabled;
+        mUI.enableShutter(enabled);
+    }
+
     // TODO: Remove this
     @Override
     public int onZoomChanged(int index) {
         // Not useful to change zoom value when the activity is paused.
-        if (mPaused) return index;
+        if (mPaused) {
+            return index;
+        }
         mZoomValue = index;
-        if (mParameters == null || mCameraDevice == null) return index;
+        if (mParameters == null || mCameraDevice == null) {
+            return index;
+        }
         // Set zoom parameters asynchronously
         mParameters.setZoom(mZoomValue);
         mCameraDevice.setParameters(mParameters);
         Parameters p = mCameraDevice.getParameters();
-        if (p != null) return p.getZoom();
+        if (p != null) {
+            return p.getZoom();
+        }
         return index;
     }
 
@@ -1771,17 +1811,13 @@
     }
 
     @Override
-    public void onQueueStatus(boolean full) {
-        mUI.enableShutter(!full);
+    public void onMemoryStateChanged(int state) {
+        setShutterEnabled(state == MemoryManager.STATE_OK);
     }
 
     @Override
-    public void onMediaSaverAvailable(MediaSaver s) {
-        // We set the listener only when both service and shutterbutton
-        // are initialized.
-        if (mFirstTimeInitialized) {
-            s.setQueueListener(this);
-        }
+    public void onLowMemory() {
+        // Not much we can do in the photo module.
     }
 
     @Override
@@ -1800,7 +1836,7 @@
             // we should not be here.
             return;
         }
-        for (int i = 0; i < 3 ; i++) {
+        for (int i = 0; i < 3; i++) {
             data[i] = event.values[i];
         }
         float[] orientation = new float[3];
diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java
index 499d030..a080a8e 100644
--- a/src/com/android/camera/Storage.java
+++ b/src/com/android/camera/Storage.java
@@ -16,9 +16,6 @@
 
 package com.android.camera;
 
-import java.io.File;
-import java.io.FileOutputStream;
-
 import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -32,10 +29,14 @@
 import android.provider.MediaStore.MediaColumns;
 import android.util.Log;
 
+import com.android.camera.crop.ImageLoader;
 import com.android.camera.data.LocalData;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.util.ApiHelper;
 
+import java.io.File;
+import java.io.FileOutputStream;
+
 public class Storage {
     private static final String TAG = "CameraStorage";
 
@@ -186,6 +187,50 @@
         }
     }
 
+    /**
+     * Update the image from the file that has changed.
+     * <p>
+     * Note: This will update the DATE_TAKEN to right now. We could consider not
+     * changing it to preserve the original timestamp.
+     * <p>
+     * TODO: MIME type for videos, location
+     */
+    public static void updateImageFromChangedFile(Uri mediaUri, ContentResolver resolver) {
+        File mediaFile = new File(ImageLoader.getLocalPathFromUri(resolver, mediaUri));
+        if (!mediaFile.exists()) {
+            throw new IllegalArgumentException("Provided URI is not an existent file: "
+                    + mediaUri.getPath());
+        }
+
+        ContentValues values = new ContentValues();
+        // TODO: Read the date from file.
+        values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
+        values.put(Images.Media.MIME_TYPE, LocalData.MIME_TYPE_JPEG);
+        values.put(Images.Media.SIZE, mediaFile.length());
+
+        resolver.update(mediaUri, values, null, null);
+    }
+
+    /**
+     * Updates the item's mime type to the given one. This is useful e.g. when
+     * switching an image to an in-progress type for re-processing.
+     *
+     * @param uri the URI of the item to change
+     * @param mimeeType the new mime type of the item
+     */
+    public static void updateItemMimeType(Uri uri, String mimeType, ContentResolver resolver) {
+        ContentValues values = new ContentValues(1);
+        values.put(ImageColumns.MIME_TYPE, mimeType);
+
+        // Update the MediaStore
+        int rowsModified = resolver.update(uri, values, null, null);
+        if (rowsModified != 1) {
+            // This should never happen
+            throw new IllegalStateException("Bad number of rows (" + rowsModified
+                    + ") updated for uri: " + uri);
+        }
+    }
+
     public static void deleteImage(ContentResolver resolver, Uri uri) {
         try {
             resolver.delete(uri, null, null);
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
index c7ff694..63fcffa 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -55,6 +55,8 @@
 import com.android.camera.app.CameraManager.CameraPictureCallback;
 import com.android.camera.app.CameraManager.CameraProxy;
 import com.android.camera.app.MediaSaver;
+import com.android.camera.app.MemoryManager;
+import com.android.camera.app.MemoryManager.MemoryListener;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.module.ModuleController;
 import com.android.camera.settings.SettingsManager;
@@ -74,11 +76,12 @@
 public class VideoModule extends CameraModule
     implements ModuleController,
     VideoController,
+    MemoryListener,
     ShutterButton.OnShutterButtonListener,
     MediaRecorder.OnErrorListener,
     MediaRecorder.OnInfoListener {
 
-    private static final String TAG = "CAM_VideoModule";
+    private static final String TAG = "VideoModule";
 
     // Messages defined for the UI thread handler.
     private static final int MSG_CHECK_DISPLAY_ROTATION = 4;
@@ -123,7 +126,7 @@
     private boolean mRecordingTimeCountsDown = false;
     private long mOnResumeTime;
     // The video file that the hardware camera is about to record into
-    // (or is recording into.)
+    // (or is recording into.
     private String mVideoFilename;
     private ParcelFileDescriptor mVideoFileDescriptor;
 
@@ -136,7 +139,7 @@
 
     private CamcorderProfile mProfile;
 
-    // The video duration limit. 0 menas no limit.
+    // The video duration limit. 0 means no limit.
     private int mMaxVideoDurationInMs;
 
     // Time Lapse parameters.
@@ -190,21 +193,10 @@
                 }
             };
 
-    private void openCamera() {
-        if (mCameraDevice == null) {
-            mCameraDevice = CameraUtil.openCamera(
-                    mActivity, mCameraId, mHandler,
-                    mActivity.getCameraOpenErrorCallback());
-        }
-        if (mCameraDevice == null) {
-            // Error.
-            return;
-        }
-        mParameters = mCameraDevice.getParameters();
-    }
-
-    // This Handler is used to post message back onto the main thread of the
-    // application
+    /**
+     * This Handler is used to post message back onto the main thread of the
+     * application.
+     */
     private class MainHandler extends Handler {
         @Override
         public void handleMessage(Message msg) {
@@ -258,6 +250,9 @@
 
     private BroadcastReceiver mReceiver = null;
 
+    /** Whether shutter is enabled. */
+    private boolean mShutterEnabled;
+
     private class MyBroadcastReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -336,11 +331,7 @@
     private void takeASnapshot() {
         // Only take snapshots if video snapshot is supported by device
         if (CameraUtil.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
-            if (!mMediaRecorderRecording || mPaused || mSnapshotInProgress) {
-                return;
-            }
-            MediaSaver s = getServices().getMediaSaver();
-            if (s == null || s.isQueueFull()) {
+            if (!mMediaRecorderRecording || mPaused || mSnapshotInProgress || mShutterEnabled) {
                 return;
             }
 
@@ -375,7 +366,7 @@
 
     }
 
-    private ButtonManager.ButtonCallback mCameraButtonCallback =
+    private final ButtonManager.ButtonCallback mCameraButtonCallback =
         new ButtonManager.ButtonCallback() {
             @Override
             public void onStateChanged(int state) {
@@ -1396,6 +1387,7 @@
 
         UsageStatistics.onContentViewChanged(
                 UsageStatistics.COMPONENT_CAMERA, "VideoModule");
+        getServices().getMemoryManager().addListener(this);
     }
 
     @Override
@@ -1428,6 +1420,7 @@
         mPendingSwitchCameraId = -1;
         mSwitchingCamera = false;
         mPreferenceRead = false;
+        getServices().getMemoryManager().removeListener(this);
     }
 
     @Override
@@ -1610,11 +1603,6 @@
     }
 
     @Override
-    public void onMediaSaverAvailable(MediaSaver s) {
-        // do nothing.
-    }
-
-    @Override
     public void onPreviewUIReady() {
         startPreview();
     }
@@ -1627,4 +1615,19 @@
     private void requestCamera(int id) {
         mActivity.getCameraProvider().requestCamera(id);
     }
+
+    @Override
+    public void onMemoryStateChanged(int state) {
+        setShutterEnabled(state == MemoryManager.STATE_OK);
+    }
+
+    @Override
+    public void onLowMemory() {
+        // Not much we can do in the video module.
+    }
+
+    private void setShutterEnabled(boolean enabled) {
+        mShutterEnabled = enabled;
+        mUI.enableShutter(enabled);
+    }
 }
diff --git a/src/com/android/camera/app/AppManagerFactory.java b/src/com/android/camera/app/AppManagerFactory.java
deleted file mode 100644
index 43d2a00..0000000
--- a/src/com/android/camera/app/AppManagerFactory.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.app;
-
-import android.content.Context;
-
-
-/**
- * A singleton class which provides application level utility
- * classes.
- */
-public class AppManagerFactory {
-
-    private static AppManagerFactory sFactory;
-
-    public static synchronized AppManagerFactory getInstance(Context ctx) {
-        if (sFactory == null) {
-            sFactory = new AppManagerFactory(ctx);
-        }
-        return sFactory;
-    }
-
-    private PanoramaStitchingManager mPanoramaStitchingManager;
-    private PlaceholderManager mGcamProcessingManager;
-
-    /** No public constructor. */
-    private AppManagerFactory(Context ctx) {
-        mPanoramaStitchingManager = new PanoramaStitchingManager(ctx);
-        mGcamProcessingManager = new PlaceholderManager(ctx);
-    }
-
-    public PanoramaStitchingManager getPanoramaStitchingManager() {
-        return mPanoramaStitchingManager;
-    }
-
-    public PlaceholderManager getGcamProcessingManager() {
-        return mGcamProcessingManager;
-    }
-}
diff --git a/src/com/android/camera/app/CameraApp.java b/src/com/android/camera/app/CameraApp.java
index b106f74..808bdce 100644
--- a/src/com/android/camera/app/CameraApp.java
+++ b/src/com/android/camera/app/CameraApp.java
@@ -17,10 +17,16 @@
 package com.android.camera.app;
 
 import android.app.Application;
+import android.content.Context;
 
 import com.android.camera.MediaSaverImpl;
+import com.android.camera.session.CaptureSessionManager;
+import com.android.camera.session.CaptureSessionManagerImpl;
+import com.android.camera.session.PlaceholderManager;
+import com.android.camera.session.ProcessingNotificationManager;
 import com.android.camera.util.CameraUtil;
 import com.android.camera.util.UsageStatistics;
+import com.android.camera2.R;
 
 /**
  * The Camera application class containing important services and functionality
@@ -28,6 +34,10 @@
  */
 public class CameraApp extends Application implements CameraServices {
     private MediaSaver mMediaSaver;
+    private CaptureSessionManager mSessionManager;
+    private MemoryManagerImpl mMemoryManager;
+    private ProcessingNotificationManager mNotificationManager;
+    private PlaceholderManager mPlaceHolderManager;
 
     @Override
     public void onCreate() {
@@ -35,10 +45,29 @@
         UsageStatistics.initialize(this);
         CameraUtil.initialize(this);
 
+        Context context = getApplicationContext();
         mMediaSaver = new MediaSaverImpl();
+        mNotificationManager = new ProcessingNotificationManager(this);
+        mPlaceHolderManager = new PlaceholderManager(context);
+        CharSequence defaultProgressMessage = getText(R.string.processing);
+
+        mSessionManager = new CaptureSessionManagerImpl(mMediaSaver, getContentResolver(),
+                mNotificationManager, mPlaceHolderManager, defaultProgressMessage);
+        mMemoryManager = MemoryManagerImpl.create(getApplicationContext(), mMediaSaver);
     }
 
     @Override
+    public CaptureSessionManager getCaptureSessionManager() {
+        return mSessionManager;
+    }
+
+    @Override
+    public MemoryManager getMemoryManager() {
+        return mMemoryManager;
+    }
+
+    @Override
+    @Deprecated
     public MediaSaver getMediaSaver() {
         return mMediaSaver;
     }
diff --git a/src/com/android/camera/app/CameraServices.java b/src/com/android/camera/app/CameraServices.java
index ce6ef8e..2c0216e 100644
--- a/src/com/android/camera/app/CameraServices.java
+++ b/src/com/android/camera/app/CameraServices.java
@@ -16,12 +16,30 @@
 
 package com.android.camera.app;
 
+import com.android.camera.session.CaptureSessionManager;
+
+/**
+ * Functionality available to all modules and services.
+ */
 public interface CameraServices {
+
+    /**
+     * Returns the capture session manager instance that modules use to store
+     * temporary or final capture results.
+     */
+    public CaptureSessionManager getCaptureSessionManager();
+
+    /**
+     * Returns the memory manager which can be used to get informed about memory
+     * status updates.
+     */
+    public MemoryManager getMemoryManager();
+
     /**
      * Returns the media saver instance.
      * <p>
-     * Deprecated. Use {@link #getSessionManager()} whenever possible. This
-     * direct access to media saver will go away.
+     * Deprecated. Use {@link #getCaptureSessionManager()} whenever possible.
+     * This direct access to media saver will go away.
      */
     @Deprecated
     public MediaSaver getMediaSaver();
diff --git a/src/com/android/camera/app/ImageTaskManager.java b/src/com/android/camera/app/ImageTaskManager.java
deleted file mode 100644
index 570e97d..0000000
--- a/src/com/android/camera/app/ImageTaskManager.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.app;
-
-import android.net.Uri;
-
-/**
- * The interface for background image processing task manager.
- */
-public interface ImageTaskManager {
-
-    /**
-     * Callback interface for task events.
-     */
-    public interface TaskListener {
-        public void onTaskQueued(String filePath, Uri imageUri);
-        public void onTaskDone(String filePath, Uri imageUri);
-        public void onTaskProgress(
-                String filePath, Uri imageUri, int progress);
-    }
-
-    public void addTaskListener(TaskListener l);
-
-    public void removeTaskListener(TaskListener l);
-
-    /**
-     * Get task progress by Uri.
-     *
-     * @param uri         The Uri of the final image file to identify the task.
-     * @return            Integer from 0 to 100, or -1. The percentage of the task done
-     *                    so far. -1 means not found.
-     */
-    public int getTaskProgress(Uri uri);
-}
diff --git a/src/com/android/camera/app/MemoryManager.java b/src/com/android/camera/app/MemoryManager.java
new file mode 100644
index 0000000..52f6e04
--- /dev/null
+++ b/src/com/android/camera/app/MemoryManager.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.app;
+
+/**
+ * Keeps track of memory used by the app and informs modules and services if
+ * memory gets low.
+ */
+public interface MemoryManager {
+    /**
+     * Classes implementing this interface will be able to get updates about
+     * memory status changes.
+     */
+    public static interface MemoryListener {
+        /**
+         * Called when the app is experiencing a change in memory state. Modules
+         * should listen to these to not exceed the available memory.
+         *
+         * @param state the new state, one of {@link MemoryManager#STATE_OK},
+         *            {@link MemoryManager#STATE_LOW_MEMORY},
+         */
+        public void onMemoryStateChanged(int state);
+
+        /**
+         * Called when the system is about to kill our app due to high memory
+         * load.
+         */
+        public void onLowMemory();
+    }
+
+    /** The memory status is OK. The app can function as normal. */
+    public static final int STATE_OK = 0;
+
+    /** The memory is running low. E.g. no new media should be captured. */
+    public static final int STATE_LOW_MEMORY = 1;
+
+    /**
+     * Add a new listener that is informed about upcoming memory events.
+     */
+    public void addListener(MemoryListener listener);
+
+    /**
+     * Removes an already registered listener.
+     */
+    public void removeListener(MemoryListener listener);
+}
diff --git a/src/com/android/camera/app/MemoryManagerImpl.java b/src/com/android/camera/app/MemoryManagerImpl.java
new file mode 100644
index 0000000..1b6c012
--- /dev/null
+++ b/src/com/android/camera/app/MemoryManagerImpl.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.app;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+
+import com.android.camera.app.MediaSaver.QueueListener;
+
+import java.util.LinkedList;
+
+/**
+ * Default implementation of the {@link MemoryManager}.
+ * <p>
+ * TODO: Add GCam signals.
+ */
+public class MemoryManagerImpl implements MemoryManager, QueueListener, ComponentCallbacks2 {
+    private static final int[] sCriticalStates = new int[] {
+            ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
+            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL
+    };
+
+    private final LinkedList<MemoryListener> mListeners = new LinkedList<MemoryListener>();
+
+    /**
+     * Use this to create a wired-up memory manager.
+     *
+     * @param context this is used to register for system memory events.
+     * @param mediaSaver this used to check if the saving queue is full.
+     * @return A wired-up memory manager instance.
+     */
+    public static MemoryManagerImpl create(Context context, MediaSaver mediaSaver) {
+        MemoryManagerImpl memoryManager = new MemoryManagerImpl();
+        context.registerComponentCallbacks(memoryManager);
+        mediaSaver.setQueueListener(memoryManager);
+        return memoryManager;
+    }
+
+    /**
+     * Use {@link #create(Context, MediaSaver)} to make sure it's wired up
+     * correctly.
+     */
+    private MemoryManagerImpl() {
+    }
+
+    @Override
+    public void addListener(MemoryListener listener) {
+        synchronized (mListeners) {
+            if (mListeners.contains(listener)) {
+                throw new IllegalStateException("Listener already added.");
+            }
+            mListeners.add(listener);
+        }
+    }
+
+    @Override
+    public void removeListener(MemoryListener listener) {
+        synchronized (mListeners) {
+            if (!mListeners.contains(listener)) {
+                throw new IllegalStateException("Listener was never added.");
+            }
+            mListeners.remove(listener);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+    }
+
+    @Override
+    public void onLowMemory() {
+        notifyLowMemory();
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        for (int i = 0; i < sCriticalStates.length; ++i) {
+            if (level == sCriticalStates[i]) {
+                notifyLowMemory();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void onQueueStatus(boolean full) {
+        notifyCaptureStateUpdate(full ? STATE_LOW_MEMORY : STATE_OK);
+    }
+
+    /** Notify our listener that memory is running low. */
+    private void notifyLowMemory() {
+        synchronized (mListeners) {
+            for (MemoryListener listener : mListeners) {
+                listener.onLowMemory();
+            }
+        }
+    }
+
+    private void notifyCaptureStateUpdate(int captureState) {
+        synchronized (mListeners) {
+            for (MemoryListener listener : mListeners) {
+                listener.onMemoryStateChanged(captureState);
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/app/PlaceholderManager.java b/src/com/android/camera/app/PlaceholderManager.java
deleted file mode 100644
index f85490f..0000000
--- a/src/com/android/camera/app/PlaceholderManager.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.app;
-
-import android.content.Context;
-import android.graphics.BitmapFactory;
-import android.location.Location;
-import android.net.Uri;
-
-import com.android.camera.Storage;
-import com.android.camera.exif.ExifInterface;
-import com.android.camera.util.CameraUtil;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-
-public class PlaceholderManager implements ImageTaskManager {
-    private static final String TAG = "PlaceholderManager";
-
-    public static final String PLACEHOLDER_MIME_TYPE = "application/placeholder-image";
-    private final Context mContext;
-
-    final private ArrayList<WeakReference<TaskListener>> mListenerRefs;
-
-    public static class Session {
-        String outputTitle;
-        Uri outputUri;
-        long time;
-
-        Session(String title, Uri uri, long timestamp) {
-            outputTitle = title;
-            outputUri = uri;
-            time = timestamp;
-        }
-    }
-
-    public PlaceholderManager(Context context) {
-        mContext = context;
-        mListenerRefs = new ArrayList<WeakReference<TaskListener>>();
-    }
-
-    @Override
-    public void addTaskListener(TaskListener l) {
-        synchronized (mListenerRefs) {
-            if (findTaskListener(l) == -1) {
-                mListenerRefs.add(new WeakReference<TaskListener>(l));
-            }
-        }
-    }
-
-    @Override
-    public void removeTaskListener(TaskListener l) {
-        synchronized (mListenerRefs) {
-            int i = findTaskListener(l);
-            if (i != -1) {
-                mListenerRefs.remove(i);
-            }
-        }
-    }
-
-    @Override
-    public int getTaskProgress(Uri uri) {
-        return 0;
-    }
-
-    private int findTaskListener(TaskListener listener) {
-        int index = -1;
-        for (int i = 0; i < mListenerRefs.size(); i++) {
-            TaskListener l = mListenerRefs.get(i).get();
-            if (l != null && l == listener) {
-                index = i;
-                break;
-            }
-        }
-        return index;
-    }
-
-    private Iterable<TaskListener> getListeners() {
-        return new Iterable<TaskListener>() {
-            @Override
-            public Iterator<TaskListener> iterator() {
-                return new ListenerIterator();
-            }
-        };
-    }
-
-    private class ListenerIterator implements Iterator<TaskListener> {
-        private int mIndex = 0;
-        private TaskListener mNext = null;
-
-        @Override
-        public boolean hasNext() {
-            while (mNext == null && mIndex < mListenerRefs.size()) {
-                mNext = mListenerRefs.get(mIndex).get();
-                if (mNext == null) {
-                    mListenerRefs.remove(mIndex);
-                }
-            }
-            return mNext != null;
-        }
-
-        @Override
-        public TaskListener next() {
-            hasNext(); // Populates mNext
-            mIndex++;
-            TaskListener next = mNext;
-            mNext = null;
-            return next;
-        }
-
-        @Override
-        public void remove() {
-            throw new UnsupportedOperationException();
-        }
-    }
-
-    public Session insertPlaceholder(String title, byte[] placeholder, long timestamp) {
-        if (title == null || placeholder == null) {
-            throw new IllegalArgumentException("Null argument passed to insertPlaceholder");
-        }
-
-        // Decode bounds
-        BitmapFactory.Options options = new BitmapFactory.Options();
-        options.inJustDecodeBounds = true;
-        BitmapFactory.decodeByteArray(placeholder, 0, placeholder.length, options);
-        int width = options.outWidth;
-        int height = options.outHeight;
-
-        if (width <= 0 || height <= 0) {
-            throw new IllegalArgumentException("Image had bad height/width");
-        }
-
-        Uri uri =
-                Storage.addImage(mContext.getContentResolver(), title, timestamp, null, 0, null,
-                        placeholder, width, height, PLACEHOLDER_MIME_TYPE);
-
-        if (uri == null) {
-            return null;
-        }
-
-        String filePath = uri.getPath();
-        synchronized (mListenerRefs) {
-            for (TaskListener l : getListeners()) {
-                l.onTaskQueued(filePath, uri);
-            }
-        }
-
-        return new Session(title, uri, timestamp);
-    }
-
-    public void replacePlaceholder(Session session, Location location, int orientation,
-            ExifInterface exif, byte[] jpeg, int width, int height, String mimeType) {
-
-        Storage.updateImage(session.outputUri, mContext.getContentResolver(), session.outputTitle,
-                session.time, location, orientation, exif, jpeg, width, height, mimeType);
-
-        synchronized (mListenerRefs) {
-            for (TaskListener l : getListeners()) {
-                l.onTaskDone(session.outputUri.getPath(), session.outputUri);
-            }
-        }
-        CameraUtil.broadcastNewPicture(mContext, session.outputUri);
-    }
-
-    public void removePlaceholder(Session session) {
-        Storage.deleteImage(mContext.getContentResolver(), session.outputUri);
-    }
-
-}
diff --git a/src/com/android/camera/crop/ImageLoader.java b/src/com/android/camera/crop/ImageLoader.java
index 9eae63e..9062e85 100644
--- a/src/com/android/camera/crop/ImageLoader.java
+++ b/src/com/android/camera/crop/ImageLoader.java
@@ -71,8 +71,8 @@
         return ret;
     }
 
-    public static String getLocalPathFromUri(Context context, Uri uri) {
-        Cursor cursor = context.getContentResolver().query(uri,
+    public static String getLocalPathFromUri(ContentResolver resolver, Uri uri) {
+        Cursor cursor = resolver.query(uri,
                 new String[]{MediaStore.Images.Media.DATA}, null, null, null);
         if (cursor == null) {
             return null;
@@ -304,7 +304,7 @@
 
         // Make sure sample size is reasonable
         if (sampleSize <= 0 ||
-                0 >= (int) (Math.min(w, h) / sampleSize)) {
+                0 >= (Math.min(w, h) / sampleSize)) {
             return null;
         }
         return loadDownsampledBitmap(context, uri, sampleSize);
@@ -410,8 +410,8 @@
         return bmap;
     }
 
-    public static List<ExifTag> getExif(Context context, Uri uri) {
-        String path = getLocalPathFromUri(context, uri);
+    public static List<ExifTag> getExif(ContentResolver resolver, Uri uri) {
+        String path = getLocalPathFromUri(resolver, uri);
         if (path != null) {
             Uri localUri = Uri.parse(path);
             String mimeType = getMimeType(localUri);
diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java
index 1185bfa..006a57c 100644
--- a/src/com/android/camera/data/CameraDataAdapter.java
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -27,8 +27,8 @@
 import android.view.View;
 
 import com.android.camera.Storage;
-import com.android.camera.app.PlaceholderManager;
 import com.android.camera.filmstrip.ImageData;
+import com.android.camera.session.PlaceholderManager;
 
 import java.util.ArrayList;
 import java.util.Comparator;
diff --git a/src/com/android/camera/module/ModuleController.java b/src/com/android/camera/module/ModuleController.java
index ac53b3b..664d13e 100644
--- a/src/com/android/camera/module/ModuleController.java
+++ b/src/com/android/camera/module/ModuleController.java
@@ -18,9 +18,8 @@
 
 import android.content.res.Configuration;
 
-import com.android.camera.app.CameraManager;
 import com.android.camera.app.AppController;
-import com.android.camera.app.MediaSaver;
+import com.android.camera.app.CameraManager;
 
 /**
  * The controller at app level.
@@ -104,12 +103,4 @@
      * @param cameraProxy The camera device proxy.
      */
     public void onCameraAvailable(CameraManager.CameraProxy cameraProxy);
-
-    /**
-     * Called by the app when the {@link com.android.camera.app.MediaSaver} is
-     * available.
-     *
-     * @param mediaSaver The {@link com.android.camera.app.MediaSaver} to use.
-     */
-    public void onMediaSaverAvailable(MediaSaver mediaSaver);
 }
diff --git a/src/com/android/camera/session/CaptureSession.java b/src/com/android/camera/session/CaptureSession.java
new file mode 100644
index 0000000..990f3b5
--- /dev/null
+++ b/src/com/android/camera/session/CaptureSession.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.session;
+
+import android.location.Location;
+import android.net.Uri;
+
+import com.android.camera.app.MediaSaver.OnMediaSavedListener;
+import com.android.camera.exif.ExifInterface;
+
+/**
+ * A session is an item that is in progress of being created and saved, such as
+ * a photo sphere or HDR+ photo.
+ */
+public interface CaptureSession {
+
+    /**
+     * Set the progress in percent for the current session. If set to or left at
+     * 0, no progress bar is shown.
+     */
+    public void setProgress(int percent);
+
+    /**
+     * Returns the progress of this session in percent.
+     */
+    public int getProgress();
+
+    /**
+     * Returns the current progress message.
+     */
+    public CharSequence getProgressMessage();
+
+    /**
+     * Starts the session by adding a placeholder to the filmstrip and adding
+     * notifications.
+     *
+     * @param placeholder a valid encoded bitmap to be used as the placeholder.
+     * @param progressMessage the message to be used to the progress
+     *            notification initially. This can later be changed using
+     *            {@link #setProgressMessage(CharSequence)}.
+     */
+    public void startSession(byte[] placeholder, CharSequence progressMessage);
+
+    /**
+     * Starts the session by marking the item as in-progress and adding
+     * notifications.
+     *
+     * @param uri the URI of the item to be re-processed.
+     * @param progressMessage the message to be used to the progress
+     *            notification initially. This can later be changed using
+     *            {@link #setProgressMessage(CharSequence)}.
+     */
+    public void startSession(Uri uri, CharSequence progressMessage);
+
+    /**
+     * 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.
+     */
+    public void saveAndFinish(byte[] data, Location loc, int width, int height, int orientation,
+            ExifInterface exif, OnMediaSavedListener listener);
+
+    /**
+     * Finishes the session.
+     */
+    public void finish();
+
+    /**
+     * Returns the path to the final output of this session. This is only
+     * available after startSession has been called.
+     */
+    public String getPath();
+
+    /**
+     * Whether this session already has a path. This is the case once it has
+     * been started. False is returned, if the session has not been started yet
+     * and no path is available
+     */
+    public boolean hasPath();
+
+    /**
+     * Called when the underlying media file has been changed and the session
+     * should update itself.
+     */
+    public void onMediaChanged();
+}
diff --git a/src/com/android/camera/session/CaptureSessionManager.java b/src/com/android/camera/session/CaptureSessionManager.java
new file mode 100644
index 0000000..89105e5
--- /dev/null
+++ b/src/com/android/camera/session/CaptureSessionManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.session;
+
+import android.location.Location;
+import android.net.Uri;
+
+import com.android.camera.app.MediaSaver.OnMediaSavedListener;
+import com.android.camera.exif.ExifInterface;
+
+/**
+ * Modules use this manager to store capture results.
+ */
+public interface CaptureSessionManager {
+    /**
+     * Callback interface for session events.
+     */
+    public interface SessionListener {
+        /**
+         * Called when the session with the given Uri was queued and will be
+         * processed.
+         */
+        public void onSessionQueued(Uri imageUri);
+
+        /** Called when the session with the given Uri finished. */
+        public void onSessionDone(Uri imageUri);
+
+        /** Called when the session with the given Uri was progressed. */
+        public void onSessionProgress(Uri imageUri, int progress);
+    }
+
+    /**
+     * Creates a new capture session.
+     *
+     * @param title the title of the new session.
+     */
+    CaptureSession createNewSession(String title);
+
+    /**
+     * 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();
+
+    /**
+     * Save an image without creating a session that includes progress.
+     *
+     * @param data the image data to be saved.
+     * @param title the title of the media item.
+     * @param date the timestamp of the capture.
+     * @param loc the capture location.
+     * @param width the width of the captured image.
+     * @param height the height of the captured image.
+     * @param orientation the orientatio of the captured image.
+     * @param exif the EXIF data of the captured image.
+     * @param listener called when saving is complete.
+     */
+    void saveImage(byte[] data, String title, long date, Location loc, int width, int height,
+            int orientation, ExifInterface exif, OnMediaSavedListener listener);
+
+    /**
+     * Add a listener to be informed about capture session updates.
+     * <p>
+     * Note: It is guaranteed that the callbacks will happen on the main thread,
+     * so callers have to make sure to not block execution.
+     */
+    public void addSessionListener(SessionListener listener);
+
+    /**
+     * Removes a previously added listener from receiving further capture
+     * session updates.
+     */
+    public void removeSessionListener(SessionListener listener);
+
+    /**
+     * Get session progress by URI.
+     *
+     * @param uri The URI of the final media file to identify the session.
+     * @return Integer from 0 to 100, or -1. The percentage of the session done
+     *         so far. -1 means not found.
+     */
+    public int getSessionProgress(Uri uri);
+
+    /**
+     * Get the string ID for the progress message of the the session with the
+     * given URI.
+     *
+     * @param uri The URI of the final image file to identify the session.
+     * @return The current progress message.
+     */
+    public CharSequence getSessionProgressMessage(Uri uri);
+}
diff --git a/src/com/android/camera/session/CaptureSessionManagerImpl.java b/src/com/android/camera/session/CaptureSessionManagerImpl.java
new file mode 100644
index 0000000..4230393
--- /dev/null
+++ b/src/com/android/camera/session/CaptureSessionManagerImpl.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.session;
+
+import android.content.ContentResolver;
+import android.location.Location;
+import android.net.Uri;
+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.crop.ImageLoader;
+import com.android.camera.data.LocalData;
+import com.android.camera.exif.ExifInterface;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+/**
+ * Implementation for the {@link CaptureSessionManager}.
+ */
+public class CaptureSessionManagerImpl implements CaptureSessionManager {
+
+    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 current progress of this session in percent. */
+        private int mProgressPercent = 0;
+        /** An associated notification ID, else -1. */
+        private int mNotificationId = -1;
+        /** A message ID for the current progress state. */
+        private CharSequence mProgressMessage;
+        /** A place holder for this capture session. */
+        private PlaceholderManager.Session mPlaceHolderSession;
+
+        private CaptureSessionImpl(String title) {
+            mTitle = title;
+            mProgressMessage = mDefaultProgressMessage;
+        }
+
+        @Override
+        public synchronized void setProgress(int percent) {
+            mProgressPercent = percent;
+            notifyTaskProgress(mUri, mProgressPercent);
+            mNotificationManager.setProgress(mProgressPercent, mNotificationId);
+        }
+
+        @Override
+        public synchronized int getProgress() {
+            return mProgressPercent;
+        }
+
+        @Override
+        public synchronized CharSequence getProgressMessage() {
+            return mProgressMessage;
+        }
+
+        @Override
+        public synchronized void setProgressMessage(CharSequence message) {
+            mProgressMessage = message;
+            mNotificationManager.setStatus(mProgressMessage, mNotificationId);
+        }
+
+        @Override
+        public synchronized void startSession(byte[] placeholder, CharSequence progressMessage) {
+            if (mNotificationId > 0) {
+                throw new RuntimeException("startSession cannot be called a second time.");
+            }
+
+            mProgressMessage = progressMessage;
+            mNotificationId = mNotificationManager.notifyStart(mProgressMessage);
+
+            final long now = System.currentTimeMillis();
+            // TODO: This needs to happen outside the UI thread.
+            mPlaceHolderSession = mPlaceholderManager.insertPlaceholder(mTitle, placeholder, now);
+            mUri = mPlaceHolderSession.outputUri;
+            mSessions.put(mUri.toString(), this);
+            notifyTaskQueued(mUri);
+        }
+
+        @Override
+        public synchronized void startSession(Uri uri, CharSequence progressMessage) {
+            if (mNotificationId > 0) {
+                throw new RuntimeException("startSession cannot be called a second time.");
+            }
+            mUri = uri;
+            mProgressMessage = progressMessage;
+            mNotificationId = mNotificationManager.notifyStart(mProgressMessage);
+            mPlaceHolderSession = mPlaceholderManager.convertToPlaceholder(uri);
+
+            mSessions.put(mUri.toString(), this);
+            notifyTaskQueued(mUri);
+        }
+
+        @Override
+        public synchronized void cancel() {
+            if (mUri != null) {
+                removeSession(mUri.toString());
+            }
+        }
+
+        @Override
+        public synchronized void saveAndFinish(byte[] data, Location loc, int width, int height,
+                int orientation, ExifInterface exif, OnMediaSavedListener listener) {
+            if (mPlaceHolderSession == null) {
+                throw new IllegalStateException(
+                        "Cannot call saveAndFinish without calling startSession first.");
+            }
+
+            // TODO: This needs to happen outside the UI thread.
+            mPlaceholderManager.replacePlaceholder(mPlaceHolderSession, loc, orientation, exif,
+                    data, width, height, LocalData.MIME_TYPE_JPEG);
+
+            mNotificationManager.notifyCompletion(mNotificationId);
+            removeSession(mUri.toString());
+            notifyTaskDone(mPlaceHolderSession.outputUri);
+        }
+
+        @Override
+        public void finish() {
+            if (mPlaceHolderSession == null) {
+                throw new IllegalStateException(
+                        "Cannot call finish without calling startSession first.");
+            }
+
+            // Set final values in media store, such as mime type and size.
+            mPlaceholderManager.replacePlaceHolder(mPlaceHolderSession);
+            mNotificationManager.notifyCompletion(mNotificationId);
+            removeSession(mUri.toString());
+            notifyTaskDone(mPlaceHolderSession.outputUri);
+        }
+
+        @Override
+        public String getPath() {
+            if (mUri == null) {
+                throw new IllegalStateException("Cannot retrieve URI of not started session.");
+            }
+            return ImageLoader.getLocalPathFromUri(mContentResolver, mUri);
+        }
+
+        @Override
+        public boolean hasPath() {
+            return mUri != null;
+        }
+
+        @Override
+        public void onMediaChanged() {
+            // TODO: Refresh filmstrip et al.
+        }
+    }
+
+    private final MediaSaver mMediaSaver;
+    private final ProcessingNotificationManager mNotificationManager;
+    private final PlaceholderManager mPlaceholderManager;
+    private final ContentResolver mContentResolver;
+    private final CharSequence mDefaultProgressMessage;
+
+    /**
+     * We use this to fire events to the session listeners from the main thread.
+     */
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+    /** Sessions in progress, keyed by URI. */
+    private final Map<String, CaptureSession> mSessions;
+
+    /** Listeners interested in task update events. */
+    private final LinkedList<SessionListener> mTaskListeners = new LinkedList<SessionListener>();
+
+    /**
+     * Initializes a new {@link CaptureSessionManager} implementation.
+     *
+     * @param mediaSaver used to store the resulting media item
+     * @param contentResolver required by the media saver
+     * @param notificationManager used to update system notifications about the
+     *            progress
+     * @param placeholderManager used to manage placeholders in the filmstrip
+     *            before the final result is ready
+     * @param defaultProgressMessage message shown as the current progress
+     *            status by default
+     */
+    public CaptureSessionManagerImpl(MediaSaver mediaSaver,
+            ContentResolver contentResolver, ProcessingNotificationManager notificationManager,
+            PlaceholderManager placeholderManager, CharSequence defaultProgressMessage) {
+        mSessions = new HashMap<String, CaptureSession>();
+        mMediaSaver = mediaSaver;
+        mContentResolver = contentResolver;
+        mNotificationManager = notificationManager;
+        mPlaceholderManager = placeholderManager;
+        mDefaultProgressMessage = defaultProgressMessage;
+    }
+
+    @Override
+    public CaptureSession createNewSession(String title) {
+        return new CaptureSessionImpl(title);
+    }
+
+    @Override
+    public CaptureSession createSession() {
+        return new CaptureSessionImpl(null);
+    }
+
+    @Override
+    public void saveImage(byte[] data, String title, long date, Location loc,
+            int width, int height, int orientation, ExifInterface exif,
+            OnMediaSavedListener listener) {
+        mMediaSaver.addImage(data, title, date, loc, width, height, orientation, exif,
+                listener, mContentResolver);
+    }
+
+    @Override
+    public void addSessionListener(SessionListener listener) {
+        synchronized (mTaskListeners) {
+            mTaskListeners.add(listener);
+        }
+    }
+
+    @Override
+    public void removeSessionListener(SessionListener listener) {
+        synchronized (mTaskListeners) {
+            mTaskListeners.remove(listener);
+        }
+    }
+
+    @Override
+    public int getSessionProgress(Uri uri) {
+        CaptureSession session = mSessions.get(uri.toString());
+        if (session != null) {
+            return session.getProgress();
+        }
+
+        // Return -1 to indicate we don't have progress for the given session.
+        return -1;
+    }
+
+    @Override
+    public CharSequence getSessionProgressMessage(Uri uri) {
+        CaptureSession session = mSessions.get(uri.toString());
+        if (session == null) {
+            throw new IllegalArgumentException("Session with given URI does not exist: " + uri);
+        }
+        return session.getProgressMessage();
+    }
+
+    private void removeSession(String sessionUri) {
+        mSessions.remove(sessionUri);
+    }
+
+    /**
+     * Notifies all task listeners that the task with the given URI has been
+     * queued.
+     */
+    private void notifyTaskQueued(final Uri uri) {
+        mMainHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (mTaskListeners) {
+                    for (SessionListener listener : mTaskListeners) {
+                        listener.onSessionQueued(uri);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Notifies all task listeners that the task with the given URI has been
+     * finished.
+     */
+    private void notifyTaskDone(final Uri uri) {
+        mMainHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (mTaskListeners) {
+                    for (SessionListener listener : mTaskListeners) {
+                        listener.onSessionDone(uri);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Notifies all task listeners that the task with the given URI has
+     * progressed to the given state.
+     */
+    private void notifyTaskProgress(final Uri uri, final int progressPercent) {
+        mMainHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (mTaskListeners) {
+                    for (SessionListener listener : mTaskListeners) {
+                        listener.onSessionProgress(uri, progressPercent);
+                    }
+                }
+            }
+        });
+    }
+}
diff --git a/src/com/android/camera/session/PlaceholderManager.java b/src/com/android/camera/session/PlaceholderManager.java
new file mode 100644
index 0000000..818afe4
--- /dev/null
+++ b/src/com/android/camera/session/PlaceholderManager.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.session;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.location.Location;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.camera.Storage;
+import com.android.camera.exif.ExifInterface;
+import com.android.camera.util.CameraUtil;
+
+/**
+ * Handles placeholders in filmstrip that show up temporarily while a final
+ * output media item is being produced.
+ */
+public class PlaceholderManager {
+    private static final String TAG = "PlaceholderManager";
+
+    public static final String PLACEHOLDER_MIME_TYPE = "application/placeholder-image";
+    private final Context mContext;
+
+    public static class Session {
+        final String outputTitle;
+        final Uri outputUri;
+        final long time;
+
+        Session(String title, Uri uri, long timestamp) {
+            outputTitle = title;
+            outputUri = uri;
+            time = timestamp;
+        }
+    }
+
+    public PlaceholderManager(Context context) {
+        mContext = context;
+    }
+
+    public Session insertPlaceholder(String title, byte[] placeholder, long timestamp) {
+        if (title == null || placeholder == null) {
+            throw new IllegalArgumentException("Null argument passed to insertPlaceholder");
+        }
+
+        // Decode bounds
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeByteArray(placeholder, 0, placeholder.length, options);
+        int width = options.outWidth;
+        int height = options.outHeight;
+
+        if (width <= 0 || height <= 0) {
+            throw new IllegalArgumentException("Image had bad height/width");
+        }
+
+        Uri uri =
+                Storage.addImage(mContext.getContentResolver(), title, timestamp, null, 0, null,
+                        placeholder, width, height, PLACEHOLDER_MIME_TYPE);
+
+        if (uri == null) {
+            return null;
+        }
+        return new Session(title, uri, timestamp);
+    }
+
+    /**
+     * Converts an existing item into a placeholder for re-processing.
+     *
+     * @param uri the URI of an existing media item.
+     * @return A session that can be used to update the progress of the new
+     *         session.
+     */
+    public Session convertToPlaceholder(Uri uri) {
+        Storage.updateItemMimeType(uri, PLACEHOLDER_MIME_TYPE, mContext.getContentResolver());
+        return createSessionFromUri(uri);
+    }
+
+    public void replacePlaceholder(Session session, Location location, int orientation,
+            ExifInterface exif, byte[] jpeg, int width, int height, String mimeType) {
+
+        Storage.updateImage(session.outputUri, mContext.getContentResolver(), session.outputTitle,
+                session.time, location, orientation, exif, jpeg, width, height, mimeType);
+        CameraUtil.broadcastNewPicture(mContext, session.outputUri);
+    }
+
+    /**
+     * Replace the placeholder with the final image which has been stored on
+     * disk.
+     */
+    public void replacePlaceHolder(Session session) {
+        Storage.updateImageFromChangedFile(session.outputUri, mContext.getContentResolver());
+        CameraUtil.broadcastNewPicture(mContext, session.outputUri);
+    }
+
+    /**
+     * Removes the placeholder for the given session.
+     */
+    public void removePlaceholder(Session session) {
+        Storage.deleteImage(mContext.getContentResolver(), session.outputUri);
+    }
+
+    /**
+     * Create a new session instance from the given URI by querying the media
+     * store.
+     * <p>
+     * TODO: Make sure this works with types other than images when needed.
+     */
+    private Session createSessionFromUri(Uri uri) {
+        ContentResolver resolver = mContext.getContentResolver();
+
+        Cursor cursor = resolver.query(uri,
+                new String[] {
+                        MediaStore.Images.Media.DATE_TAKEN, MediaStore.Images.Media.DISPLAY_NAME,
+                }, null, null, null);
+        if (cursor == null) {
+            return null;
+        }
+        int dateIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN);
+        int nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
+
+        cursor.moveToFirst();
+        long date = cursor.getLong(dateIndex);
+        String name = cursor.getString(nameIndex);
+
+        if (name.toLowerCase().endsWith(Storage.JPEG_POSTFIX)) {
+            name = name.substring(0, name.length() - Storage.JPEG_POSTFIX.length());
+        }
+
+        return new Session(name, uri, date);
+    }
+}
diff --git a/src/com/android/camera/session/ProcessingNotificationManager.java b/src/com/android/camera/session/ProcessingNotificationManager.java
new file mode 100644
index 0000000..0bbca9a
--- /dev/null
+++ b/src/com/android/camera/session/ProcessingNotificationManager.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.session;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+
+import com.android.camera2.R;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A notification manager for computation events.
+ * <p>
+ * It maintains a single in-progress notification that indicates processing
+ * progress and status. Once processing is complete, a new completion
+ * notification is added. The completion notification triggers an intent on
+ * click.
+ * <p>
+ * Calling {@link #notifyStart(int)} while there is a current notification is a
+ * no-op. Calling {@link #notifyCompletion}, {@link #setProgress} or
+ * {@link #setStatus} when there is no current notification is a no-op.
+ * <p>
+ * The expected use case is as follows:
+ *
+ * <pre>
+ * {@code
+ * manager = new ProcessingNotificationManager(context);
+ * manager.notifyStart(resourceId);
+ * manager.setProgress(...);  // Can be called multiple times.
+ * manager.setStatus(...);  // Can be called multiple times.
+ * manager.notifyCompletion();
+ * }
+ * </pre>
+ */
+public class ProcessingNotificationManager {
+    private static final int FIRST_NOTIFICATION_ID = 0;
+    private static AtomicInteger sUniqueNotificationId;
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private Notification.Builder mInProgressNotificationBuilder;
+
+    /**
+     * Creates a new {@code ProcessingNotificationManager} with a
+     * {@link Context}.
+     */
+    public ProcessingNotificationManager(Context context) {
+        if (sUniqueNotificationId == null) {
+            sUniqueNotificationId = new AtomicInteger(FIRST_NOTIFICATION_ID);
+        }
+        this.mContext = context;
+        this.mNotificationManager = (NotificationManager) context.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+    }
+
+    /**
+     * @param progress sets the progress in the in-progress notification. The
+     *            expected value is in [0, 100]
+     * @return whether the update was successful
+     */
+    public boolean setProgress(int progress, int notificationId) {
+        if (mInProgressNotificationBuilder == null) {
+            return false;
+        }
+        mInProgressNotificationBuilder.setProgress(100, progress, false);
+        mNotificationManager.notify(
+                notificationId, buildNotification(mInProgressNotificationBuilder));
+        return true;
+    }
+
+    /**
+     * @param status sets the status message in the in-progress notification
+     * @return whether the update was successful
+     */
+    public boolean setStatus(CharSequence status, int notificationId) {
+        if (mInProgressNotificationBuilder == null) {
+            return false;
+        }
+        mInProgressNotificationBuilder.setContentText(status);
+        mNotificationManager.notify(
+                notificationId, buildNotification(mInProgressNotificationBuilder));
+        return true;
+    }
+
+    /**
+     * Creates a new notification indicating that a new computation has started.
+     * It will initialize the in-progress notification and add it to the
+     * notification bar.
+     *
+     * @param statusMessage the status message to show on start
+     * @return The ID of the notification.
+     */
+    public int notifyStart(CharSequence statusMessage) {
+        if (mInProgressNotificationBuilder != null) {
+            return -1;
+        }
+        mInProgressNotificationBuilder = createInProgressNotificationBuilder(statusMessage);
+        // Increment the global notification id to make sure we have a unique
+        // id.
+        int notificationId = sUniqueNotificationId.incrementAndGet();
+        mNotificationManager.notify(
+                notificationId, buildNotification(mInProgressNotificationBuilder));
+        return notificationId;
+    }
+
+    /**
+     * Notify a computation is completed. It will remove the in-progress
+     * notification.
+     */
+    public void notifyCompletion(int notificationId) {
+        if (mInProgressNotificationBuilder == null) {
+            return;
+        }
+        mNotificationManager.cancel(notificationId);
+        mInProgressNotificationBuilder = null;
+    }
+
+    /**
+     * Cancel the in-progress notification. Any completion notification will be
+     * left intact.
+     */
+    public void cancel(int notificationId) {
+        if (mInProgressNotificationBuilder == null) {
+            return;
+        }
+        mNotificationManager.cancel(notificationId);
+        mInProgressNotificationBuilder = null;
+    }
+
+    /**
+     * Creates a notification to indicate that a computation is in progress.
+     *
+     * @param statusMessage a human readable message indicating the current
+     *            progress status.
+     */
+    Notification.Builder createInProgressNotificationBuilder(CharSequence statusMessage) {
+        return new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.ic_notification)
+                .setWhen(System.currentTimeMillis())
+                .setOngoing(true)
+                .setContentTitle(mContext.getText(R.string.app_name))
+                .setProgress(100, 0, false)
+                .setTicker(statusMessage);
+    }
+
+    @SuppressLint("NewApi")
+    @SuppressWarnings("deprecation")
+    static Notification buildNotification(Notification.Builder builder) {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+            return builder.build();
+        }
+        return builder.getNotification();
+    }
+}
diff --git a/src/com/android/camera/settings/SettingsManager.java b/src/com/android/camera/settings/SettingsManager.java
index 2feb18c..e850004 100644
--- a/src/com/android/camera/settings/SettingsManager.java
+++ b/src/com/android/camera/settings/SettingsManager.java
@@ -16,17 +16,12 @@
 
 package com.android.camera.settings;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.hardware.Camera.Size;
-import android.location.Location;
 import android.preference.PreferenceManager;
-import android.util.Log;
 
-import com.android.camera.CameraActivity;
 import com.android.camera.ListPreference;
 import com.android.camera.util.SettingsHelper;
 import com.android.camera2.R;
@@ -38,15 +33,15 @@
  * global and local SharedPreferences.
  */
 public class SettingsManager {
-    private static final String TAG = "CAM_SettingsManager";
+    private static final String TAG = "SettingsManager";
 
-    private Context mContext;
-    private SharedPreferences mDefaultSettings;
+    private final Context mContext;
+    private final SharedPreferences mDefaultSettings;
+    private final SettingsCache mSettingsCache;
     private SharedPreferences mGlobalSettings;
     private SharedPreferences mCameraSettings;
     private OnSharedPreferenceChangeListener mListener;
     private SettingsCapabilities mCapabilities;
-    private SettingsCache mSettingsCache;
 
     private int mCameraId = -1;
 
diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java
index 5cbe4e7..0c53a7b 100644
--- a/src/com/android/camera/ui/RenderOverlay.java
+++ b/src/com/android/camera/ui/RenderOverlay.java
@@ -43,12 +43,12 @@
 
     }
 
-    private RenderView mRenderView;
-    private List<Renderer> mClients;
+    private final RenderView mRenderView;
+    private final List<Renderer> mClients;
     private PreviewGestures mGestures;
     // reverse list of touch clients
-    private List<Renderer> mTouchClients;
-    private int[] mPosition = new int[2];
+    private final List<Renderer> mTouchClients;
+    private final int[] mPosition = new int[2];
 
     public RenderOverlay(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -130,7 +130,7 @@
     // TODO: migrate all modes to PreviewOverlay.
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        if (mTapListener != null && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+        if (mTapListener != null && ev.getActionMasked() == MotionEvent.ACTION_UP) {
             mTapListener.onSingleTapUp(null, ((int) ev.getX()), ((int) ev.getY()));
         }
         return true;