Merge "Animate between photo modules" into gb-ub-photos-bryce
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 360128c..d497283 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -390,6 +390,7 @@
                 android:theme="@style/Theme.ProxyLauncher">
         </activity>
         <service android:name="com.android.gallery3d.app.BatchService" />
+        <service android:name="com.android.camera.MediaSaveService" />
         <!-- canvas -->
         <provider
             android:name="com.android.gallery3d.provider.CanvasProvider"
diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java
new file mode 100644
index 0000000..f871975
--- /dev/null
+++ b/src/com/android/camera/MediaSaveService.java
@@ -0,0 +1,155 @@
+/*
+ * 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;
+
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.location.Location;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class MediaSaveService extends Service {
+    private static final int SAVE_TASK_LIMIT = 3;
+    private static final String TAG = MediaSaveService.class.getSimpleName();
+
+    private final IBinder mBinder = new LocalBinder();
+    private int mTaskNumber;
+    private Listener mListener;
+
+    interface Listener {
+        public void onQueueAvailable();
+        public void onQueueFull();
+    }
+
+    interface OnMediaSavedListener {
+        public void onMediaSaved(Uri uri);
+    }
+
+    class LocalBinder extends Binder {
+        public MediaSaveService getService() {
+            return MediaSaveService.this;
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flag, int startId) {
+        return START_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+    }
+
+    @Override
+    public void onCreate() {
+        mTaskNumber = 0;
+    }
+
+    public boolean isQueueFull() {
+        return (mTaskNumber >= SAVE_TASK_LIMIT);
+    }
+
+    // Runs in main thread
+    public void addImage(final byte[] data, String title, long date, Location loc,
+            int width, int height, int orientation,
+            OnMediaSavedListener l, ContentResolver resolver) {
+        if (isQueueFull()) {
+            Log.e(TAG, "Cannot add image when the queue is full");
+            return;
+        }
+        SaveTask t = new SaveTask(data, title, date, (loc == null) ? null : new Location(loc),
+                width, height, orientation, resolver, l);
+
+        mTaskNumber++;
+        if (isQueueFull()) {
+            onQueueFull();
+        }
+        t.execute();
+    }
+
+    public void setListener(Listener l) {
+        mListener = l;
+        if (l == null) return;
+        if (isQueueFull()) {
+            l.onQueueFull();
+        } else {
+            l.onQueueAvailable();
+        }
+    }
+
+    private void onQueueFull() {
+        if (mListener != null) mListener.onQueueFull();
+    }
+
+    private void onQueueAvailable() {
+        if (mListener != null) mListener.onQueueAvailable();
+    }
+
+    private class SaveTask extends AsyncTask <Void, Void, Uri> {
+        private byte[] data;
+        private String title;
+        private long date;
+        private Location loc;
+        private int width, height;
+        private int orientation;
+        private ContentResolver resolver;
+        private OnMediaSavedListener listener;
+
+        public SaveTask(byte[] data, String title, long date, Location loc,
+                int width, int height, int orientation, ContentResolver resolver,
+                OnMediaSavedListener listener) {
+            this.data = data;
+            this.title = title;
+            this.date = date;
+            this.loc = loc;
+            this.width = width;
+            this.height = height;
+            this.orientation = orientation;
+            this.resolver = resolver;
+            this.listener = listener;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            // do nothing.
+        }
+
+        @Override
+        protected Uri doInBackground(Void... v) {
+            return Storage.addImage(
+                    resolver, title, date, loc, orientation, data, width, height);
+        }
+
+        @Override
+        protected void onPostExecute(Uri uri) {
+            listener.onMediaSaved(uri);
+            mTaskNumber--;
+            if (mTaskNumber == SAVE_TASK_LIMIT - 1) onQueueAvailable();
+        }
+    }
+}
diff --git a/src/com/android/camera/MediaSaver.java b/src/com/android/camera/MediaSaver.java
deleted file mode 100644
index a3d582e..0000000
--- a/src/com/android/camera/MediaSaver.java
+++ /dev/null
@@ -1,149 +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;
-
-import android.content.ContentResolver;
-import android.location.Location;
-import android.net.Uri;
-import android.util.Log;
-
-import java.util.ArrayList;
-
-// We use a queue to store the SaveRequests that have not been completed
-// yet. The main thread puts the request into the queue. The saver thread
-// gets it from the queue, does the work, and removes it from the queue.
-//
-// The main thread needs to wait for the saver thread to finish all the work
-// in the queue, when the activity's onPause() is called, we need to finish
-// all the work, so other programs (like Gallery) can see all the images.
-//
-// If the queue becomes too long, adding a new request will block the main
-// thread until the queue length drops below the threshold (QUEUE_LIMIT).
-// If we don't do this, we may face several problems: (1) We may OOM
-// because we are holding all the jpeg data in memory. (2) We may ANR
-// when we need to wait for saver thread finishing all the work (in
-// onPause() or gotoGallery()) because the time to finishing a long queue
-// of work may be too long.
-class MediaSaver extends Thread {
-    private static final int SAVE_QUEUE_LIMIT = 3;
-    private static final String TAG = "MediaSaver";
-
-    private ArrayList<SaveRequest> mQueue;
-    private boolean mStop;
-    private ContentResolver mContentResolver;
-
-    public interface OnMediaSavedListener {
-        public void onMediaSaved(Uri uri);
-    }
-
-    public MediaSaver(ContentResolver resolver) {
-        mContentResolver = resolver;
-        mQueue = new ArrayList<SaveRequest>();
-        start();
-    }
-
-    // Runs in main thread
-    public synchronized boolean queueFull() {
-        return (mQueue.size() >= SAVE_QUEUE_LIMIT);
-    }
-
-    // Runs in main thread
-    public void addImage(final byte[] data, String title, long date, Location loc,
-                         int width, int height, int orientation, OnMediaSavedListener l) {
-        SaveRequest r = new SaveRequest();
-        r.data = data;
-        r.date = date;
-        r.title = title;
-        r.loc = (loc == null) ? null : new Location(loc);  // make a copy
-        r.width = width;
-        r.height = height;
-        r.orientation = orientation;
-        r.listener = l;
-        synchronized (this) {
-            while (mQueue.size() >= SAVE_QUEUE_LIMIT) {
-                try {
-                    wait();
-                } catch (InterruptedException ex) {
-                    // ignore.
-                }
-            }
-            mQueue.add(r);
-            notifyAll();  // Tell saver thread there is new work to do.
-        }
-    }
-
-    // Runs in saver thread
-    @Override
-    public void run() {
-        while (true) {
-            SaveRequest r;
-            synchronized (this) {
-                if (mQueue.isEmpty()) {
-                    notifyAll();  // notify main thread in waitDone
-
-                    // Note that we can only stop after we saved all images
-                    // in the queue.
-                    if (mStop) break;
-
-                    try {
-                        wait();
-                    } catch (InterruptedException ex) {
-                        // ignore.
-                    }
-                    continue;
-                }
-                if (mStop) break;
-                r = mQueue.remove(0);
-                notifyAll();  // the main thread may wait in addImage
-            }
-            Uri uri = storeImage(r.data, r.title, r.date, r.loc, r.width, r.height,
-                    r.orientation);
-            r.listener.onMediaSaved(uri);
-        }
-        if (!mQueue.isEmpty()) {
-            Log.e(TAG, "Media saver thread stopped with " + mQueue.size() + " images unsaved");
-            mQueue.clear();
-        }
-    }
-
-    // Runs in main thread
-    public void finish() {
-        synchronized (this) {
-            mStop = true;
-            notifyAll();
-        }
-    }
-
-    // Runs in saver thread
-    private Uri storeImage(final byte[] data, String title, long date,
-                           Location loc, int width, int height, int orientation) {
-        Uri uri = Storage.addImage(mContentResolver, title, date, loc,
-                                   orientation, data, width, height);
-        return uri;
-    }
-
-    // Each SaveRequest remembers the data needed to save an image.
-    private static class SaveRequest {
-        byte[] data;
-        String title;
-        long date;
-        Location loc;
-        int width, height;
-        int orientation;
-        OnMediaSavedListener listener;
-    }
-}
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index a283e59..14477b7 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -19,10 +19,13 @@
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.content.ComponentName;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.SharedPreferences.Editor;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
@@ -39,6 +42,7 @@
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
@@ -95,7 +99,8 @@
     ShutterButton.OnShutterButtonListener,
     SurfaceHolder.Callback,
     PieRenderer.PieListener,
-    CountDownView.OnCountDownFinishedListener {
+    CountDownView.OnCountDownFinishedListener,
+    MediaSaveService.Listener {
 
     private static final String TAG = "CAM_PhotoModule";
 
@@ -114,7 +119,6 @@
     private static final int START_PREVIEW_DONE = 10;
     private static final int OPEN_CAMERA_FAIL = 11;
     private static final int CAMERA_DISABLED = 12;
-    private static final int UPDATE_SECURE_ALBUM_ITEM = 13;
 
     // The subset of parameters we need to update in setCameraParameters().
     private static final int UPDATE_PARAM_INITIALIZE = 1;
@@ -200,11 +204,21 @@
     // A view group that contains all the small indicators.
     private View mOnScreenIndicators;
 
-    // We use a thread in MediaSaver to do the work of saving images. This
+    // We use MediaSaveService to do the work of saving images in background. This
     // reduces the shot-to-shot time.
-    private MediaSaver mMediaSaver;
-    // Similarly, we use a thread to generate the name of the picture and insert
-    // it into MediaStore while picture taking is still in progress.
+    private MediaSaveService mMediaSaveService;
+    private ServiceConnection mConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className, IBinder b) {
+                mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService();
+                mMediaSaveService.setListener(PhotoModule.this);
+            }
+            @Override
+            public void onServiceDisconnected(ComponentName className) {
+                mMediaSaveService = null;
+            }};
+    // We use a queue to generated names of the images to be used later
+    // when the image is ready to be saved.
     private NamedImages mNamedImages;
 
     private Runnable mDoSnapRunnable = new Runnable() {
@@ -304,16 +318,16 @@
 
     private PreviewGestures mGestures;
 
-    private MediaSaver.OnMediaSavedListener mOnMediaSavedListener = new MediaSaver.OnMediaSavedListener() {
-        @Override
-
-        public void onMediaSaved(Uri uri) {
-            if (uri != null) {
-                mHandler.obtainMessage(UPDATE_SECURE_ALBUM_ITEM, uri).sendToTarget();
-                Util.broadcastNewPicture(mActivity, uri);
-            }
-        }
-    };
+    private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
+            new MediaSaveService.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        mActivity.addSecureAlbumItemIfNeeded(false, uri);
+                        Util.broadcastNewPicture(mActivity, uri);
+                    }
+                }
+            };
 
     // The purpose is not to block the main thread in onCreate and onResume.
     private class CameraStartUpThread extends Thread {
@@ -449,11 +463,6 @@
                             R.string.camera_disabled);
                     break;
                 }
-
-                case UPDATE_SECURE_ALBUM_ITEM: {
-                    mActivity.addSecureAlbumItemIfNeeded(false, (Uri) msg.obj);
-                    break;
-                }
             }
         }
     }
@@ -648,9 +657,15 @@
         mShutterButton = mActivity.getShutterButton();
         mShutterButton.setImageResource(R.drawable.btn_new_shutter);
         mShutterButton.setOnShutterButtonListener(this);
+        if (mMediaSaveService != null) {
+            if (mMediaSaveService.isQueueFull()) {
+                mShutterButton.enableTouch(false);
+            } else {
+                mShutterButton.enableTouch(true);
+            }
+        }
         mShutterButton.setVisibility(View.VISIBLE);
 
-        mMediaSaver = new MediaSaver(mContentResolver);
         mNamedImages = new NamedImages();
 
         mFirstTimeInitialized = true;
@@ -688,7 +703,6 @@
                 mPreferences, mContentResolver);
         mLocationManager.recordLocation(recordLocation);
 
-        mMediaSaver = new MediaSaver(mContentResolver);
         mNamedImages = new NamedImages();
         initializeZoom();
         keepMediaProviderInstance();
@@ -994,8 +1008,8 @@
                     Log.e(TAG, "Unbalanced name/data pair");
                 } else {
                     if (date == -1) date = mCaptureStartTime;
-                    mMediaSaver.addImage(jpegData, title, date, mLocation, width, height,
-                            orientation, mOnMediaSavedListener);
+                    mMediaSaveService.addImage(jpegData, title, date, mLocation, width, height,
+                            orientation, mOnMediaSavedListener, mContentResolver);
                 }
             } else {
                 mJpegImageData = jpegData;
@@ -1117,7 +1131,7 @@
         // 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 || mMediaSaver.queueFull()) {
+                || mCameraState == SWITCHING_CAMERA || mMediaSaveService.isQueueFull()) {
             return false;
         }
         mCaptureStartTime = System.currentTimeMillis();
@@ -1517,6 +1531,7 @@
             mCameraStartUpThread.start();
         }
 
+        bindMediaSaveService();
         // If first time initialization is not finished, put it in the
         // message queue.
         if (!mFirstTimeInitialized) {
@@ -1530,6 +1545,18 @@
         PopupManager.getInstance(mActivity).notifyShowPopup(null);
     }
 
+    private void bindMediaSaveService() {
+        Intent intent = new Intent(mActivity, MediaSaveService.class);
+        mActivity.startService(intent);  // start service before binding it so the
+                                         // service won't be killed if we unbind it.
+        mActivity.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    private void unbindMediaSaveService() {
+        mMediaSaveService.setListener(null);
+        mActivity.unbindService(mConnection);
+    }
+
     void waitCameraStartUpThread() {
         try {
             if (mCameraStartUpThread != null) {
@@ -1576,18 +1603,13 @@
             mSurfaceTexture = null;
         }
         resetScreenOn();
+        unbindMediaSaveService();
 
         // Clear UI.
         collapseCameraControls();
         if (mFaceView != null) mFaceView.clear();
 
-        if (mFirstTimeInitialized) {
-            if (mMediaSaver != null) {
-                mMediaSaver.finish();
-                mMediaSaver = null;
-                mNamedImages = null;
-            }
-        }
+        mNamedImages = null;
 
         if (mLocationManager != null) mLocationManager.recordLocation(false);
 
@@ -2478,4 +2500,13 @@
         }
     }
 
+    @Override
+    public void onQueueAvailable() {
+        if (mShutterButton != null) mShutterButton.enableTouch(true);
+    }
+
+    @Override
+    public void onQueueFull() {
+        if (mShutterButton != null) mShutterButton.enableTouch(false);
+    }
 }