Refactor MediaSaveService for future test needs.

Added interface MediaSaver as the abstract layer to make the implementation
independent of the clients.

Change-Id: I98db3f732e4ed1c87da005b9c18e888682a5e6a5
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
index 52792ba..b923395 100644
--- a/src/com/android/camera/CameraActivity.java
+++ b/src/com/android/camera/CameraActivity.java
@@ -65,6 +65,7 @@
 
 import com.android.camera.CameraManager.CameraOpenErrorCallback;
 import com.android.camera.app.AppManagerFactory;
+import com.android.camera.app.MediaSaver;
 import com.android.camera.app.PlaceholderManager;
 import com.android.camera.app.PanoramaStitchingManager;
 import com.android.camera.crop.CropActivity;
@@ -207,19 +208,19 @@
         }
     }
 
-    private MediaSaveService mMediaSaveService;
+    private MediaSaver mMediaSaver;
     private ServiceConnection mConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName className, IBinder b) {
-            mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService();
-            mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+            mMediaSaver = ((MediaSaveService.LocalBinder) b).getService();
+            mCurrentModule.onMediaSaverAvailable(mMediaSaver);
         }
 
         @Override
         public void onServiceDisconnected(ComponentName className) {
-            if (mMediaSaveService != null) {
-                mMediaSaveService.setListener(null);
-                mMediaSaveService = null;
+            if (mMediaSaver != null) {
+                mMediaSaver.setQueueListener(null);
+                mMediaSaver = null;
             }
         }
     };
@@ -789,8 +790,8 @@
                 }
             };
 
-    public MediaSaveService getMediaSaveService() {
-        return mMediaSaveService;
+    public MediaSaver getMediaSaver() {
+        return mMediaSaver;
     }
 
     public void notifyNewMedia(Uri uri) {
@@ -1342,8 +1343,8 @@
 
         openModule(mCurrentModule);
         mCurrentModule.onOrientationChanged(mLastRawOrientation);
-        if (mMediaSaveService != null) {
-            mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+        if (mMediaSaver != null) {
+            mCurrentModule.onMediaSaverAvailable(mMediaSaver);
         }
 
         // Store the module index so we can use it the next time the Camera
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
index 55cae9f..b6fb0e6 100644
--- a/src/com/android/camera/CameraModule.java
+++ b/src/com/android/camera/CameraModule.java
@@ -21,6 +21,8 @@
 import android.view.KeyEvent;
 import android.view.View;
 
+import com.android.camera.app.MediaSaver;
+
 public interface CameraModule {
 
     public void init(CameraActivity activity, View frame);
@@ -63,7 +65,7 @@
 
     public void onShowSwitcherPopup();
 
-    public void onMediaSaveServiceConnected(MediaSaveService s);
+    public void onMediaSaverAvailable(MediaSaver s);
 
     public boolean arePreviewControlsVisible();
 }
diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java
index e8ec08d..1bcbcf2 100644
--- a/src/com/android/camera/MediaSaveService.java
+++ b/src/com/android/camera/MediaSaveService.java
@@ -29,14 +29,15 @@
 import android.provider.MediaStore.Video;
 import android.util.Log;
 
+import com.android.camera.app.MediaSaver;
 import com.android.camera.exif.ExifInterface;
 
 import java.io.File;
 
-/*
- * Service for saving images in the background thread.
+/**
+ * A class implementing {@link com.android.camera.app.MediaSaver}.
  */
-public class MediaSaveService extends Service {
+public class MediaSaveService extends Service implements MediaSaver {
     public static final String VIDEO_BASE_URI = "content://media/external/video/media";
 
     // The memory limit for unsaved image is 20MB.
@@ -44,20 +45,12 @@
     private static final String TAG = "CAM_" + MediaSaveService.class.getSimpleName();
 
     private final IBinder mBinder = new LocalBinder();
-    private Listener mListener;
+    private QueueListener mQueueListener;
     // Memory used by the total queued save request, in bytes.
     private long mMemoryUse;
 
-    public interface Listener {
-        public void onQueueStatus(boolean full);
-    }
-
-    public interface OnMediaSavedListener {
-        public void onMediaSaved(Uri uri);
-    }
-
     class LocalBinder extends Binder {
-        public MediaSaveService getService() {
+        public MediaSaver getService() {
             return MediaSaveService.this;
         }
     }
@@ -81,13 +74,15 @@
         mMemoryUse = 0;
     }
 
+    @Override
     public boolean isQueueFull() {
         return (mMemoryUse >= SAVE_TASK_MEMORY_LIMIT);
     }
 
-    public void addImage(final byte[] data, String title, long date, Location loc,
-            int width, int height, int orientation, ExifInterface exif,
-            OnMediaSavedListener l, ContentResolver resolver) {
+    @Override
+    public void addImage(final byte[] data, String title, long date, Location loc, int width,
+            int height, int orientation, ExifInterface exif, OnMediaSavedListener l,
+            ContentResolver resolver) {
         if (isQueueFull()) {
             Log.e(TAG, "Cannot add image when the queue is full");
             return;
@@ -103,39 +98,41 @@
         t.execute();
     }
 
-    public void addImage(final byte[] data, String title, long date, Location loc,
-                         int orientation, ExifInterface exif,
-                         OnMediaSavedListener l, ContentResolver resolver) {
+    @Override
+    public void addImage(final byte[] data, String title, long date, Location loc, int orientation,
+            ExifInterface exif, OnMediaSavedListener l, ContentResolver resolver) {
         // When dimensions are unknown, pass 0 as width and height,
         // and decode image for width and height later in a background thread
         addImage(data, title, date, loc, 0, 0, orientation, exif, l, resolver);
     }
-    public void addImage(final byte[] data, String title, Location loc,
-            int width, int height, int orientation, ExifInterface exif,
-            OnMediaSavedListener l, ContentResolver resolver) {
+    @Override
+    public void addImage(final byte[] data, String title, Location loc, int width, int height,
+            int orientation, ExifInterface exif, OnMediaSavedListener l, ContentResolver resolver) {
         addImage(data, title, System.currentTimeMillis(), loc, width, height,
                 orientation, exif, l, resolver);
     }
 
-    public void addVideo(String path, long duration, ContentValues values,
-            OnMediaSavedListener l, ContentResolver resolver) {
+    @Override
+    public void addVideo(String path, long duration, ContentValues values, OnMediaSavedListener l,
+            ContentResolver resolver) {
         // We don't set a queue limit for video saving because the file
         // is already in the storage. Only updating the database.
         new VideoSaveTask(path, duration, values, l, resolver).execute();
     }
 
-    public void setListener(Listener l) {
-        mListener = l;
+    @Override
+    public void setQueueListener(QueueListener l) {
+        mQueueListener = l;
         if (l == null) return;
         l.onQueueStatus(isQueueFull());
     }
 
     private void onQueueFull() {
-        if (mListener != null) mListener.onQueueStatus(true);
+        if (mQueueListener != null) mQueueListener.onQueueStatus(true);
     }
 
     private void onQueueAvailable() {
-        if (mListener != null) mListener.onQueueStatus(false);
+        if (mQueueListener != null) mQueueListener.onQueueStatus(false);
     }
 
     private class ImageSaveTask extends AsyncTask <Void, Void, Uri> {
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index 347244e..5174825 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -58,6 +58,7 @@
 import com.android.camera.CameraManager.CameraProxy;
 import com.android.camera.CameraManager.CameraShutterCallback;
 import com.android.camera.PhotoModule.NamedImages.NamedEntity;
+import com.android.camera.app.MediaSaver;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.exif.ExifTag;
 import com.android.camera.exif.Rational;
@@ -83,8 +84,7 @@
         PhotoController,
         FocusOverlayManager.Listener,
         CameraPreference.OnPreferenceChangedListener,
-        ShutterButton.OnShutterButtonListener,
-        MediaSaveService.Listener,
+        ShutterButton.OnShutterButtonListener, MediaSaver.QueueListener,
         OnCountDownFinishedListener,
         SensorEventListener {
 
@@ -263,8 +263,8 @@
     // True if all the parameters needed to start preview is ready.
     private boolean mCameraPreviewParamsReady = false;
 
-    private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
-            new MediaSaveService.OnMediaSavedListener() {
+    private MediaSaver.OnMediaSavedListener mOnMediaSavedListener =
+            new MediaSaver.OnMediaSavedListener() {
                 @Override
                 public void onMediaSaved(Uri uri) {
                     if (uri != null) {
@@ -645,11 +645,11 @@
         keepMediaProviderInstance();
 
         mUI.initializeFirstTime();
-        MediaSaveService s = mActivity.getMediaSaveService();
+        MediaSaver s = mActivity.getMediaSaver();
         // We set the listener only when both service and shutterbutton
         // are initialized.
         if (s != null) {
-            s.setListener(this);
+            s.setQueueListener(this);
         }
 
         mNamedImages = new NamedImages();
@@ -667,9 +667,9 @@
         boolean recordLocation = RecordLocationPreference.get(
                 mPreferences, mContentResolver);
         mLocationManager.recordLocation(recordLocation);
-        MediaSaveService s = mActivity.getMediaSaveService();
+        MediaSaver s = mActivity.getMediaSaver();
         if (s != null) {
-            s.setListener(this);
+            s.setQueueListener(this);
         }
         mNamedImages = new NamedImages();
         mUI.initializeSecondTime(mParameters);
@@ -855,7 +855,7 @@
                         exif.setTag(directionRefTag);
                         exif.setTag(directionTag);
                     }
-                    mActivity.getMediaSaveService().addImage(
+                    mActivity.getMediaSaver().addImage(
                             jpegData, title, date, mLocation, width, height,
                             orientation, exif, mOnMediaSavedListener, mContentResolver);
                 }
@@ -968,8 +968,8 @@
         // is full then ignore.
         if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS
                 || mCameraState == SWITCHING_CAMERA
-                || mActivity.getMediaSaveService() == null
-                || mActivity.getMediaSaveService().isQueueFull()) {
+                || mActivity.getMediaSaver() == null
+                || mActivity.getMediaSaver().isQueueFull()) {
             return false;
         }
         mCaptureStartTime = System.currentTimeMillis();
@@ -1370,9 +1370,9 @@
 
         mPendingSwitchCameraId = -1;
         if (mFocusManager != null) mFocusManager.removeMessages();
-        MediaSaveService s = mActivity.getMediaSaveService();
+        MediaSaver s = mActivity.getMediaSaver();
         if (s != null) {
-            s.setListener(null);
+            s.setQueueListener(null);
         }
         mUI.removeDisplayChangeListener();
     }
@@ -2030,11 +2030,11 @@
     }
 
     @Override
-    public void onMediaSaveServiceConnected(MediaSaveService s) {
+    public void onMediaSaverAvailable(MediaSaver s) {
         // We set the listener only when both service and shutterbutton
         // are initialized.
         if (mFirstTimeInitialized) {
-            s.setListener(this);
+            s.setQueueListener(this);
         }
     }
 
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
index 0ee62bb..1159879 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -55,6 +55,7 @@
 
 import com.android.camera.CameraManager.CameraPictureCallback;
 import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.app.MediaSaver;
 import com.android.camera.app.OrientationManager;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.ui.RotateTextToast;
@@ -172,8 +173,8 @@
 
     private int mZoomValue;  // The current zoom value.
 
-    private final MediaSaveService.OnMediaSavedListener mOnVideoSavedListener =
-            new MediaSaveService.OnMediaSavedListener() {
+    private final MediaSaver.OnMediaSavedListener mOnVideoSavedListener =
+            new MediaSaver.OnMediaSavedListener() {
                 @Override
                 public void onMediaSaved(Uri uri) {
                     if (uri != null) {
@@ -185,8 +186,8 @@
                 }
             };
 
-    private final MediaSaveService.OnMediaSavedListener mOnPhotoSavedListener =
-            new MediaSaveService.OnMediaSavedListener() {
+    private final MediaSaver.OnMediaSavedListener mOnPhotoSavedListener =
+            new MediaSaver.OnMediaSavedListener() {
                 @Override
                 public void onMediaSaved(Uri uri) {
                     if (uri != null) {
@@ -387,7 +388,7 @@
             if (!mMediaRecorderRecording || mPaused || mSnapshotInProgress) {
                 return;
             }
-            MediaSaveService s = mActivity.getMediaSaveService();
+            MediaSaver s = mActivity.getMediaSaver();
             if (s == null || s.isQueueFull()) {
                 return;
             }
@@ -1095,7 +1096,7 @@
             } else {
                 Log.w(TAG, "Video duration <= 0 : " + duration);
             }
-            mActivity.getMediaSaveService().addVideo(mCurrentVideoFilename,
+            mActivity.getMediaSaver().addVideo(mCurrentVideoFilename,
                     duration, mCurrentVideoValues,
                     mOnVideoSavedListener, mContentResolver);
         }
@@ -1731,7 +1732,7 @@
         ExifInterface exif = Exif.getExif(data);
         int orientation = Exif.getOrientation(exif);
 
-        mActivity.getMediaSaveService().addImage(
+        mActivity.getMediaSaver().addImage(
                 data, title, dateTaken, loc, orientation,
                 exif, mOnPhotoSavedListener, mContentResolver);
     }
@@ -1797,7 +1798,7 @@
     }
 
     @Override
-    public void onMediaSaveServiceConnected(MediaSaveService s) {
+    public void onMediaSaverAvailable(MediaSaver s) {
         // do nothing.
     }
 
diff --git a/src/com/android/camera/WideAnglePanoramaModule.java b/src/com/android/camera/WideAnglePanoramaModule.java
index 8d00c70..e29fcb6 100644
--- a/src/com/android/camera/WideAnglePanoramaModule.java
+++ b/src/com/android/camera/WideAnglePanoramaModule.java
@@ -45,6 +45,7 @@
 import android.view.WindowManager;
 
 import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.app.MediaSaver;
 import com.android.camera.app.OrientationManager;
 import com.android.camera.data.LocalData;
 import com.android.camera.exif.ExifInterface;
@@ -1099,7 +1100,7 @@
     }
 
     @Override
-    public void onMediaSaveServiceConnected(MediaSaveService s) {
+    public void onMediaSaverAvailable(MediaSaver s) {
         // do nothing.
     }
 }
diff --git a/src/com/android/camera/app/MediaSaver.java b/src/com/android/camera/app/MediaSaver.java
new file mode 100644
index 0000000..ded7bf6
--- /dev/null
+++ b/src/com/android/camera/app/MediaSaver.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.app;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.location.Location;
+import android.net.Uri;
+
+import com.android.camera.exif.ExifInterface;
+
+/**
+ * An interface defining the media saver which saves media files in the
+ * background.
+ */
+public interface MediaSaver {
+
+    /**
+     * An interface defining the callback for task queue status changes.
+     */
+    public interface QueueListener {
+        /**
+         * The callback when the queue status changes. Every time a new
+         * {@link com.android.camera.app.MediaSaver.QueueListener} is set by
+         * {@link #setQueueListener(com.android.camera.app.MediaSaver.QueueListener)}
+         * this callback will be invoked to notify the current status of the
+         * queue.
+         *
+         * @param full Whether the queue is full.
+         */
+        public void onQueueStatus(boolean full);
+    }
+
+    /**
+     * An interface defining the callback when a media is saved.
+     */
+    public interface OnMediaSavedListener {
+        /**
+         * The callback when the saving is done in the background.
+         * @param uri The final content Uri of the saved media.
+         */
+        public void onMediaSaved(Uri uri);
+    }
+
+    /**
+     * Checks whether the queue is full.
+     */
+    boolean isQueueFull();
+
+    /**
+     * Adds an image into {@link android.content.ContentResolver} and also
+     * saves the file to the storage in the background.
+     *
+     * @param data The JPEG image data.
+     * @param title The title of the image.
+     * @param date The date when the image is created.
+     * @param loc The location where the image is created. Can be {@code null}.
+     * @param width The width of the image data before the orientation is
+     *              applied.
+     * @param height The height of the image data before the orientation is
+     *               applied.
+     * @param orientation The orientation of the image. The value should be a
+     *                    degree of rotation in clockwise. Valid values are
+     *                    0, 90, 180 and 270.
+     * @param exif The EXIF data of this image.
+     * @param l A callback object used when the saving is done.
+     * @param resolver The {@link android.content.ContentResolver} to be
+     *                 updated.
+     */
+    void addImage(byte[] data, String title, long date, Location loc, int width, int height,
+            int orientation, ExifInterface exif, OnMediaSavedListener l, ContentResolver resolver);
+
+    /**
+     * Adds an image into {@link android.content.ContentResolver} and also
+     * saves the file to the storage in the background. The width and height
+     * will be obtained directly from the image data.
+     *
+     * @param data The JPEG image data.
+     * @param title The title of the image.
+     * @param date The date when the image is created.
+     * @param loc The location where the image is created. Can be {@code null}.
+     * @param orientation The orientation of the image. The value should be a
+     *                    degree of rotation in clockwise. Valid values are
+     *                    0, 90, 180 and 270.
+     * @param exif The EXIF data of this image.
+     * @param l A callback object used when the saving is done.
+     * @param resolver The {@link android.content.ContentResolver} to be
+     *                 updated.
+     */
+    void addImage(byte[] data, String title, long date, Location loc, int orientation,
+            ExifInterface exif, OnMediaSavedListener l, ContentResolver resolver);
+
+    /**
+     * Adds an image into {@link android.content.ContentResolver} and also
+     * saves the file to the storage in the background. The time will be set by
+     * {@link System#currentTimeMillis()}.
+     * will be obtained directly from the image data.
+     *
+     * @param data The JPEG image data.
+     * @param title The title of the image.
+     * @param loc The location where the image is created. Can be {@code null}.
+     * @param width The width of the image data before the orientation is
+     *              applied.
+     * @param height The height of the image data before the orientation is
+     *               applied.
+     * @param orientation
+     * @param exif The EXIF data of this image.
+     * @param l A callback object used when the saving is done.
+     * @param resolver The {@link android.content.ContentResolver} to be
+     *                 updated.
+     */
+    void addImage(byte[] data, String title, Location loc, int width, int height, int orientation,
+            ExifInterface exif, OnMediaSavedListener l, ContentResolver resolver);
+
+    /**
+     * Adds the video data into the {@link android.content.ContentResolver} in
+     * the background. Only the database is updated here. The file should
+     * already be created by {@link android.media.MediaRecorder}.
+     *
+     * @param path The path of the video file on the storage.
+     * @param duration The length of the video in millisecond.
+     * @param values The values to be stored in the database.
+     * @param l A callback object used when the saving is done.
+     * @param resolver The {@link android.content.ContentResolver} to be
+     *                 updated.
+     */
+    void addVideo(String path, long duration, ContentValues values, OnMediaSavedListener l,
+            ContentResolver resolver);
+
+    /**
+     * Sets the queue listener.
+     */
+    void setQueueListener(QueueListener l);
+}
diff --git a/src/com/android/camera/tinyplanet/TinyPlanetFragment.java b/src/com/android/camera/tinyplanet/TinyPlanetFragment.java
index 9cde87b..f1a1082 100644
--- a/src/com/android/camera/tinyplanet/TinyPlanetFragment.java
+++ b/src/com/android/camera/tinyplanet/TinyPlanetFragment.java
@@ -42,8 +42,8 @@
 import com.adobe.xmp.XMPException;
 import com.adobe.xmp.XMPMeta;
 import com.android.camera.CameraActivity;
-import com.android.camera.MediaSaveService;
-import com.android.camera.MediaSaveService.OnMediaSavedListener;
+import com.android.camera.app.MediaSaver.OnMediaSavedListener;
+import com.android.camera.app.MediaSaver;
 import com.android.camera.exif.ExifInterface;
 import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener;
 import com.android.camera.util.XmpUtil;
@@ -53,7 +53,6 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.Date;
 import java.util.TimeZone;
 import java.util.concurrent.locks.Lock;
@@ -305,7 +304,7 @@
             protected void onPostExecute(TinyPlanetImage image) {
                 // Once created, store the new file and add it to the filmstrip.
                 final CameraActivity activity = (CameraActivity) getActivity();
-                MediaSaveService mediaSaveService = activity.getMediaSaveService();
+                MediaSaver mediaSaver = activity.getMediaSaver();
                 OnMediaSavedListener doneListener =
                         new OnMediaSavedListener() {
                             @Override
@@ -318,7 +317,7 @@
                             }
                         };
                 String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle;
-                mediaSaveService.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
+                mediaSaver.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
                         null,
                         image.mSize, image.mSize, 0, null, doneListener, getActivity()
                                 .getContentResolver());