OSDN Git Service

Hook burst controller to the shutter button long press.
authorShashi Shekhar <shashishekhar@google.com>
Wed, 12 Nov 2014 17:52:19 +0000 (09:52 -0800)
committerShashi Shekhar <shashishekhar@google.com>
Wed, 19 Nov 2014 23:55:21 +0000 (15:55 -0800)
Hooks up the shutter button long press to start the burst.
Introduces a BurstManager to interact with burst.
Provides an implementation of BurstManager that listens to burst and
saves results when burst is complete.

The burst eviction handler is now installed on the ring buffer
on shutter button press and uninstalled on shutter button release.
The ring buffer is cleared before starting and after completion of the
burst.

Also provides a stub implementation of the BurstController that has a
static method which controls if burst is enabled or not.

Bug: 18332704

Change-Id: I1098937bf348af7acbf55da1a5eeb423c30fb901

19 files changed:
src/com/android/camera/CameraModule.java
src/com/android/camera/CaptureModule.java
src/com/android/camera/MediaSaverImpl.java
src/com/android/camera/ShutterButton.java
src/com/android/camera/Storage.java
src/com/android/camera/app/CameraAppUI.java
src/com/android/camera/app/MediaSaver.java
src/com/android/camera/burst/BurstController.java
src/com/android/camera/burst/BurstFacade.java [new file with mode: 0644]
src/com/android/camera/burst/BurstFacadeFactory.java [new file with mode: 0644]
src/com/android/camera/burst/BurstFacadeImpl.java [new file with mode: 0644]
src/com/android/camera/data/LocalData.java
src/com/android/camera/one/AbstractOneCamera.java
src/com/android/camera/one/OneCamera.java
src/com/android/camera/one/v2/ImageCaptureManager.java
src/com/android/camera/one/v2/OneCameraZslImpl.java
src/com/android/camera/util/ConcurrentSharedRingBuffer.java
src/com/android/camera/widget/ModeOptionsOverlay.java
src_pd/com/android/camera/burst/BurstControllerImpl.java [new file with mode: 0644]

index 5049cab..7073ee4 100644 (file)
@@ -102,4 +102,9 @@ public abstract class CameraModule implements ModuleController {
      * @return An accessibility String to be announced during the peek animation.
      */
     public abstract String getPeekAccessibilityString();
+
+    @Override
+    public void onShutterButtonLongPressed() {
+        // noop
+    }
 }
index 9cfa359..33f3551 100644 (file)
@@ -44,6 +44,8 @@ import com.android.camera.app.CameraAppUI.BottomBarUISpec;
 import com.android.camera.app.LocationManager;
 import com.android.camera.app.MediaSaver;
 import com.android.camera.app.OrientationManager;
+import com.android.camera.burst.BurstFacade;
+import com.android.camera.burst.BurstFacadeFactory;
 import com.android.camera.debug.DebugPropertyHelper;
 import com.android.camera.debug.Log;
 import com.android.camera.debug.Log.Tag;
@@ -77,9 +79,9 @@ import com.android.camera2.R;
 import com.android.ex.camera2.portability.CameraAgent.CameraProxy;
 
 import java.io.File;
-import java.util.concurrent.Semaphore;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -141,6 +143,7 @@ public class CaptureModule extends CameraModule
         }
     };
 
+
     private static final Tag TAG = new Tag("CaptureModule");
     private static final String PHOTO_MODULE_STRING_ID = "PhotoModule";
     /** Enable additional debug output. */
@@ -225,7 +228,7 @@ public class CaptureModule extends CameraModule
     private int mHeading = -1;
 
     /** Used to fetch and embed the location into captured images. */
-    private LocationManager mLocationManager;
+    private final LocationManager mLocationManager;
     /** Plays sounds for countdown timer. */
     private SoundPlayer mCountdownSoundPlayer;
 
@@ -264,6 +267,9 @@ public class CaptureModule extends CameraModule
     /** The frame consumer that renders frames to the preview. */
     private final SurfaceTextureConsumer mPreviewConsumer;
 
+    /** The burst manager for controlling the burst. */
+    private final BurstFacade mBurstController;
+
     /** CLEAN UP START */
     // private boolean mFirstLayout;
     // private int[] mTargetFPSRanges;
@@ -286,9 +292,12 @@ public class CaptureModule extends CameraModule
         mSettingsManager.addListener(this);
         mDebugDataDir = mContext.getExternalCacheDir();
         mStickyGcamCamera = stickyHdr;
+        mLocationManager = mAppController.getLocationManager();
 
         mPreviewConsumer = new SurfaceTextureConsumer();
         mFrameDistributor = new FrameDistributorWrapper();
+        mBurstController = BurstFacadeFactory.create(mAppController, getServices().getMediaSaver(),
+                mLocationManager, mAppController.getOrientationManager(), mDebugDataDir);
     }
 
     @Override
@@ -299,7 +308,6 @@ public class CaptureModule extends CameraModule
         thread.start();
         mCameraHandler = new Handler(thread.getLooper());
         mCameraManager = mAppController.getCameraManager();
-        mLocationManager = mAppController.getLocationManager();
         mDisplayRotation = CameraUtil.getDisplayRotation(mContext);
         mCameraFacing = getFacingFromCameraId(mSettingsManager.getInteger(
                 mAppController.getModuleScope(),
@@ -308,6 +316,7 @@ public class CaptureModule extends CameraModule
                 mLayoutListener);
         mAppController.setPreviewStatusListener(mUI);
 
+        mBurstController.setContentResolver(activity.getContentResolver());
         // Set the preview texture from UI for the SurfaceTextureConsumer.
         mPreviewConsumer.setSurfaceTexture(
                 mAppController.getCameraAppUI().getSurfaceTexture(),
@@ -332,8 +341,16 @@ public class CaptureModule extends CameraModule
     }
 
     @Override
+    public void onShutterButtonLongPressed() {
+        mBurstController.startBurst();
+    }
+
+    @Override
     public void onShutterButtonFocus(boolean pressed) {
-        // TODO Auto-generated method stub
+        if (!pressed) {
+            // the shutter button was released, stop any bursts.
+            mBurstController.stopBurst();
+        }
     }
 
     @Override
@@ -524,8 +541,8 @@ public class CaptureModule extends CameraModule
     private void initializeFrameDistributor() {
         // Currently, there is only one consumer to FrameDistributor for
         // rendering the frames to the preview texture.
-        // TODO: Add burst as a consumer as well.
         List<FrameConsumer> frameConsumers = new ArrayList<FrameConsumer>();
+        frameConsumers.add(mBurstController.getPreviewFrameConsumer());
         frameConsumers.add(mPreviewConsumer);
         mFrameDistributor.start(frameConsumers);
     }
@@ -581,6 +598,7 @@ public class CaptureModule extends CameraModule
     public void pause() {
         mPaused = true;
         getServices().getRemoteShutterListener().onModuleExit();
+        mBurstController.stopBurst();
         cancelCountDown();
         closeCamera();
         resetTextureBufferSize();
@@ -608,6 +626,7 @@ public class CaptureModule extends CameraModule
     @Override
     public void onLayoutOrientationChanged(boolean isLandscape) {
         Log.d(TAG, "onLayoutOrientationChanged");
+        mBurstController.stopBurst();
     }
 
     @Override
@@ -856,6 +875,10 @@ public class CaptureModule extends CameraModule
 
     @Override
     public void onReadyStateChanged(boolean readyForCapture) {
+        if (mBurstController.isBurstRunning()) {
+            return;
+        }
+
         if (readyForCapture) {
             mAppController.getCameraAppUI().enableModeOptions();
         }
@@ -1233,6 +1256,7 @@ public class CaptureModule extends CameraModule
                     @Override
                     public void onCameraClosed() {
                         mCamera = null;
+                        mBurstController.onCameraDetached();
                         mCameraOpenCloseLock.release();
                     }
 
@@ -1240,7 +1264,7 @@ public class CaptureModule extends CameraModule
                     public void onCameraOpened(final OneCamera camera) {
                         Log.d(TAG, "onCameraOpened: " + camera);
                         mCamera = camera;
-
+                        mBurstController.onCameraAttached(mCamera);
                         updatePreviewBufferDimension();
 
                         // If the surface texture is not destroyed, it may have
index c5c4c0f..ad28ca5 100644 (file)
@@ -25,6 +25,7 @@ import android.os.AsyncTask;
 import android.provider.MediaStore.Video;
 
 import com.android.camera.app.MediaSaver;
+import com.android.camera.data.LocalData;
 import com.android.camera.debug.Log;
 import com.android.camera.exif.ExifInterface;
 
@@ -58,13 +59,21 @@ public class MediaSaverImpl implements MediaSaver {
     public void addImage(final byte[] data, String title, long date, Location loc, int width,
             int height, int orientation, ExifInterface exif, OnMediaSavedListener l,
             ContentResolver resolver) {
+        addImage(data, title, date, loc, width, height, orientation, exif, l,
+                resolver, LocalData.MIME_TYPE_JPEG);
+    }
+
+    @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, String mimeType) {
         if (isQueueFull()) {
             Log.e(TAG, "Cannot add image when the queue is full");
             return;
         }
         ImageSaveTask t = new ImageSaveTask(data, title, date,
                 (loc == null) ? null : new Location(loc),
-                width, height, orientation, exif, resolver, l);
+                width, height, orientation, mimeType, exif, resolver, l);
 
         mMemoryUse += data.length;
         if (isQueueFull()) {
@@ -78,14 +87,15 @@ public class MediaSaverImpl implements MediaSaver {
             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);
+        addImage(data, title, date, loc, 0, 0, orientation, exif, l, resolver,
+                LocalData.MIME_TYPE_JPEG);
     }
     @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);
+                orientation, exif, l, resolver, LocalData.MIME_TYPE_JPEG);
     }
 
     @Override
@@ -124,13 +134,15 @@ public class MediaSaverImpl implements MediaSaver {
         private final Location loc;
         private int width, height;
         private final int orientation;
+        private final String mimeType;
         private final ExifInterface exif;
         private final ContentResolver resolver;
         private final OnMediaSavedListener listener;
 
         public ImageSaveTask(byte[] data, String title, long date, Location loc,
-                             int width, int height, int orientation, ExifInterface exif,
-                             ContentResolver resolver, OnMediaSavedListener listener) {
+                             int width, int height, int orientation, String mimeType,
+                             ExifInterface exif, ContentResolver resolver,
+                             OnMediaSavedListener listener) {
             this.data = data;
             this.title = title;
             this.date = date;
@@ -138,6 +150,7 @@ public class MediaSaverImpl implements MediaSaver {
             this.width = width;
             this.height = height;
             this.orientation = orientation;
+            this.mimeType = mimeType;
             this.exif = exif;
             this.resolver = resolver;
             this.listener = listener;
@@ -159,7 +172,8 @@ public class MediaSaverImpl implements MediaSaver {
                 height = options.outHeight;
             }
             return Storage.addImage(
-                    resolver, title, date, loc, orientation, exif, data, width, height);
+                    resolver, title, date, loc, orientation, exif, data, width, height,
+                    mimeType);
         }
 
         @Override
index 05a6126..e80bfcb 100755 (executable)
@@ -17,8 +17,9 @@
 package com.android.camera;
 
 import android.content.Context;
-import android.text.method.Touch;
 import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ImageView;
@@ -26,8 +27,8 @@ import android.widget.ImageView;
 import com.android.camera.debug.Log;
 import com.android.camera.ui.TouchCoordinate;
 
-import java.util.List;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A button designed to be used for the on-screen shutter button.
@@ -40,6 +41,8 @@ public class ShutterButton extends ImageView {
     public static final float ALPHA_WHEN_DISABLED = 0.2f;
     private boolean mTouchEnabled = true;
     private TouchCoordinate mTouchCoordinate;
+    private final GestureDetector mGestureDetector;
+
     /**
      * A callback to be invoked when a ShutterButton's pressed state changes.
      */
@@ -52,6 +55,23 @@ public class ShutterButton extends ImageView {
         void onShutterButtonFocus(boolean pressed);
         void onShutterCoordinate(TouchCoordinate coord);
         void onShutterButtonClick();
+
+        /**
+         * Called when shutter button is held down for a long press.
+         */
+        void onShutterButtonLongPressed();
+    }
+
+    /**
+     * A gesture listener to detect long presses.
+     */
+    private class LongPressGestureListener extends SimpleOnGestureListener {
+        @Override
+        public void onLongPress(MotionEvent event) {
+            for (OnShutterButtonListener listener : mListeners) {
+                listener.onShutterButtonLongPressed();
+            }
+        }
     }
 
     private List<OnShutterButtonListener> mListeners
@@ -60,6 +80,8 @@ public class ShutterButton extends ImageView {
 
     public ShutterButton(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mGestureDetector = new GestureDetector(context, new LongPressGestureListener());
+        mGestureDetector.setIsLongpressEnabled(true);
     }
 
     /**
@@ -83,6 +105,7 @@ public class ShutterButton extends ImageView {
     @Override
     public boolean dispatchTouchEvent(MotionEvent m) {
         if (mTouchEnabled) {
+            mGestureDetector.onTouchEvent(m);
             if (m.getActionMasked() == MotionEvent.ACTION_UP) {
                 mTouchCoordinate = new TouchCoordinate(m.getX(), m.getY(), this.getMeasuredWidth(),
                         this.getMeasuredHeight());
index d4a1790..3616bc1 100644 (file)
@@ -45,6 +45,7 @@ public class Storage {
             Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
     public static final String DIRECTORY = DCIM + "/Camera";
     public static final String JPEG_POSTFIX = ".jpg";
+    public static final String GIF_POSTFIX = ".gif";
     // Match the code in MediaProvider.computeBucketValues().
     public static final String BUCKET_ID =
             String.valueOf(DIRECTORY.toLowerCase().hashCode());
@@ -106,11 +107,11 @@ public class Storage {
      * @return The URI of the added image, or null if the image could not be
      *         added.
      */
-    private static Uri addImage(ContentResolver resolver, String title, long date,
+    static Uri addImage(ContentResolver resolver, String title, long date,
             Location location, int orientation, ExifInterface exif, byte[] data, int width,
             int height, String mimeType) {
 
-        String path = generateFilepath(title);
+        String path = generateFilepath(title, mimeType);
         long fileLength = writeFile(path, data, exif);
         if (fileLength >= 0) {
             return addImageToMediaStore(resolver, title, date, location, orientation, fileLength,
@@ -238,7 +239,7 @@ public class Storage {
     public static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date,
            Location location, int orientation, ExifInterface exif,
            byte[] jpeg, int width, int height, String mimeType) {
-        String path = generateFilepath(title);
+        String path = generateFilepath(title, mimeType);
         writeFile(path, jpeg, exif);
         return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
                 width, height, mimeType);
@@ -328,8 +329,16 @@ public class Storage {
         return resultUri;
     }
 
-    private static String generateFilepath(String title) {
-        return DIRECTORY + '/' + title + ".jpg";
+    private static String generateFilepath(String title, String mimeType) {
+        String extension = null;
+        if (LocalData.MIME_TYPE_JPEG.equals(mimeType)) {
+            extension = JPEG_POSTFIX;
+        } else if (LocalData.MIME_TYPE_GIF.equals(mimeType)) {
+            extension = GIF_POSTFIX;
+        } else {
+            throw new IllegalArgumentException("Invalid mimeType: " + mimeType);
+        }
+        return DIRECTORY + '/' + title + extension;
     }
 
     /**
index 2362e69..673134b 100644 (file)
@@ -1372,6 +1372,11 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener,
         // noop
     }
 
+    @Override
+    public void onShutterButtonLongPressed() {
+        // noop
+    }
+
     /**
      * Set the mode options toggle clickable.
      */
index 4387c98..459a4e9 100644 (file)
@@ -64,6 +64,11 @@ public interface MediaSaver {
     /**
      * Adds an image into {@link android.content.ContentResolver} and also
      * saves the file to the storage in the background.
+     * <p/>
+     * Equivalent to calling
+     * {@link #addImage(byte[], String, long, Location, int, int, int,
+     * ExifInterface, OnMediaSavedListener, ContentResolver, String)}
+     * with <code>image/jpeg</code> as <code>mimeType</code>.
      *
      * @param data The JPEG image data.
      * @param title The title of the image.
@@ -86,6 +91,31 @@ public interface MediaSaver {
 
     /**
      * Adds an image into {@link android.content.ContentResolver} and also
+     * saves the file to the storage in the background.
+     *
+     * @param data The 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.
+     * @param mimeType The mimeType of the image.
+     */
+    void addImage(byte[] data, String title, long date, Location loc, int width, int height,
+            int orientation, ExifInterface exif, OnMediaSavedListener l, ContentResolver resolver,
+            String mimeType);
+
+    /**
+     * 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.
      *
index bba7dd9..c501906 100644 (file)
@@ -44,7 +44,7 @@ import com.android.camera.gl.FrameDistributor.FrameConsumer;
  * Once post processing is complete, the burst module returns the final results
  * by calling {@link BurstResultsListener#onBurstCompleted(BurstResult)} method.
  */
-public interface BurstController {
+interface BurstController {
 
     /**
      * Starts the burst.
diff --git a/src/com/android/camera/burst/BurstFacade.java b/src/com/android/camera/burst/BurstFacade.java
new file mode 100644 (file)
index 0000000..27c0c00
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 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.burst;
+
+import android.content.ContentResolver;
+
+import com.android.camera.gl.FrameDistributor.FrameConsumer;
+import com.android.camera.one.OneCamera;
+
+/**
+ * Facade for {@link BurstController} provides a simpler interface.
+ */
+public interface BurstFacade {
+    /**
+     * Set the content resolver to be updated when saving burst results.
+     *
+     * @param contentResolver to be updated when burst results are saved.
+     */
+    public void setContentResolver(ContentResolver contentResolver);
+
+    /**
+     * Called when camera is available.
+     *
+     * @param camera an instance of {@link OneCamera} that is used to start or
+     *            stop the burst.
+     */
+    public void onCameraAttached(OneCamera camera);
+
+    /**
+     * Called when camera becomes unavailable.
+     */
+    public void onCameraDetached();
+
+    /**
+     * Returns the frame consumer to use for preview frames.
+     */
+    public FrameConsumer getPreviewFrameConsumer();
+
+    /**
+     * Starts the burst.
+     */
+    public void startBurst();
+
+    /**
+     * Returns true if burst is running.
+     */
+    public boolean isBurstRunning();
+
+    /**
+     * Stops the burst.
+     */
+    public void stopBurst();
+}
diff --git a/src/com/android/camera/burst/BurstFacadeFactory.java b/src/com/android/camera/burst/BurstFacadeFactory.java
new file mode 100644 (file)
index 0000000..1129d1e
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 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.burst;
+
+import android.content.ContentResolver;
+
+import com.android.camera.app.AppController;
+import com.android.camera.app.LocationManager;
+import com.android.camera.app.MediaSaver;
+import com.android.camera.app.OrientationManager;
+import com.android.camera.gl.FrameDistributor;
+import com.android.camera.gl.FrameDistributor.FrameConsumer;
+import com.android.camera.one.OneCamera;
+
+import java.io.File;
+
+/**
+ * Factory for creating burst manager objects.
+ */
+public class BurstFacadeFactory {
+    private BurstFacadeFactory() {/* cannot be instantiated */}
+
+    /**
+     * An empty burst manager that is instantiated when burst is not supported.
+     */
+    private static class BurstFacadeStub implements BurstFacade {
+        @Override
+        public void setContentResolver(ContentResolver contentResolver) {}
+
+        @Override
+        public void onCameraAttached(OneCamera camera) {}
+
+        @Override
+        public void onCameraDetached() {}
+
+        @Override
+        public FrameConsumer getPreviewFrameConsumer() {
+            return new FrameConsumer() {
+
+                @Override
+                public void onStop() {}
+
+                @Override
+                public void onStart() {}
+
+                @Override
+                public void onNewFrameAvailable(FrameDistributor frameDistributor,
+                        long timestampNs) {}
+            };
+        }
+
+        @Override
+        public void startBurst() {}
+
+        @Override
+        public boolean isBurstRunning() {
+            return false;
+        }
+
+        @Override
+        public void stopBurst() {}
+    }
+
+    /**
+     * Creates and returns an instance of {@link BurstFacade}
+     *
+     * @param appController the app level controller for controlling the shutter
+     *            button.
+     * @param mediaSaver the {@link MediaSaver} instance for saving results of
+     *            burst.
+     * @param locationManager for querying location of burst.
+     * @param orientationManager for querying orientation of burst.
+     * @param debugDataDir the debug directory to use for burst.
+     */
+    public static BurstFacade create(AppController appController,
+            MediaSaver mediaSaver,
+            LocationManager locationManager,
+            OrientationManager orientationManager,
+            File debugDataDir) {
+        if (BurstControllerImpl.isBurstModeSupported()) {
+            return new BurstFacadeImpl(appController, mediaSaver,
+                    locationManager, orientationManager,
+                    debugDataDir);
+        } else {
+            // Burst is not supported return a stub instance.
+            return new BurstFacadeStub();
+        }
+    }
+}
diff --git a/src/com/android/camera/burst/BurstFacadeImpl.java b/src/com/android/camera/burst/BurstFacadeImpl.java
new file mode 100644 (file)
index 0000000..f3c0527
--- /dev/null
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2014 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.burst;
+
+import android.content.ContentResolver;
+import android.location.Location;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+
+import com.android.camera.app.AppController;
+import com.android.camera.app.LocationManager;
+import com.android.camera.app.MediaSaver;
+import com.android.camera.app.OrientationManager;
+import com.android.camera.data.LocalData;
+import com.android.camera.debug.Log;
+import com.android.camera.debug.Log.Tag;
+import com.android.camera.exif.ExifInterface;
+import com.android.camera.gl.FrameDistributor.FrameConsumer;
+import com.android.camera.one.OneCamera;
+import com.android.camera.one.OneCamera.BurstParameters;
+import com.android.camera.one.OneCamera.BurstResultsCallback;
+import com.android.camera.session.CaptureSession;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper to manage burst, listen to burst results and saves media items.
+ * <p/>
+ * The UI feedback is rudimentary in form of a toast that is displayed on start of the
+ * burst and when artifacts are saved.
+ *
+ * TODO: Move functionality of saving burst items to a
+ * {@link com.android.camera.processing.ProcessingTask} and change API to use
+ * {@link com.android.camera.processing.ProcessingService}.
+ *
+ * TODO: Hook UI to the listener.
+ */
+class BurstFacadeImpl implements BurstFacade {
+    /**
+     * The state of the burst module.
+     */
+    private static enum BurstModuleState {
+        IDLE,
+        RUNNING,
+        STOPPING
+    }
+
+    private static final Tag TAG = new Tag("BurstFacadeImpl");
+
+    /**
+     * The format string of burst media item file name (without extension).
+     * <p/>
+     * An media item file name has the following format: "Burst_" + artifact
+     * type + "_" + index of artifact + "_" + timestamp
+     */
+    private static final String MEDIA_ITEM_FILENAME_FORMAT_STRING = "Burst_%s_%d_%d";
+    /**
+     * The title of Capture session for Burst.
+     * <p/>
+     * Title is of format: Burst_timestamp
+     */
+    private static final String BURST_TITLE_FORMAT_STRING = "Burst_%d";
+
+    private final AtomicReference<BurstModuleState> mBurstModuleState =
+            new AtomicReference<BurstModuleState>(BurstModuleState.IDLE);
+
+    /** Lock to protect starting and stopping of the burst. */
+    private final Object mStartStopBurstLock = new Object();
+
+    private final BurstController mBurstController;
+    private final AppController mAppController;
+    private final File mDebugDataDir;
+
+    private final MediaSaver.OnMediaSavedListener mOnMediaSavedListener =
+            new MediaSaver.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        mAppController.notifyNewMedia(uri);
+                    }
+                }
+            };
+
+    /**
+     * Results callback that is invoked by camera when results are available.
+     */
+    private final BurstResultsCallback
+            mBurstExtractsResultsCallback = new BurstResultsCallback() {
+                @Override
+                public void onBurstComplete(ResultsAccessor resultAccessor) {
+                    // Pass the results accessor to the controller.
+                    mBurstController.stopBurst(resultAccessor);
+                }
+            };
+
+    /**
+     * Listener for burst controller. Saves the results and interacts with the
+     * UI.
+     */
+    private final BurstResultsListener mBurstResultsListener =
+            new BurstResultsListener() {
+                @Override
+                public void onBurstStarted() {
+                }
+
+                @Override
+                public void onBurstError(Exception error) {
+                    Log.e(TAG, "Exception while running the burst" + error);
+                    mBurstModuleState.set(BurstModuleState.IDLE);
+                    // Re-enable the shutter button.
+                    mAppController.setShutterEnabled(true);
+                }
+
+                @Override
+                public void onBurstCompleted(BurstResult burstResult) {
+                    saveBurstResultAndEnableShutterButton(burstResult);
+                }
+
+                @Override
+                public void onArtifactCountAvailable(
+                        final Map<String, Integer> artifactTypeCount) {
+                    logArtifactCount(artifactTypeCount);
+                }
+            };
+
+    /** Camera instance for starting/stopping the burst. */
+    private OneCamera mCamera;
+
+    private final MediaSaver mMediaSaver;
+    private final LocationManager mLocationManager;
+    private final OrientationManager mOrientationManager;
+    private volatile ContentResolver mContentResolver;
+
+    /**
+     * Create a new BurstManagerImpl instance.
+     *
+     * @param appController the app level controller for controlling the shutter
+     *            button.
+     * @param mediaSaver the {@link MediaSaver} instance for saving results of
+     *            burst.
+     * @param locationManager for querying location of burst.
+     * @param orientationManager for querying orientation of burst.
+     * @param debugDataDir the debug directory to use for burst.
+     */
+    public BurstFacadeImpl(AppController appController,
+            MediaSaver mediaSaver,
+            LocationManager locationManager,
+            OrientationManager orientationManager,
+            File debugDataDir) {
+        mAppController = appController;
+        mMediaSaver = mediaSaver;
+        mLocationManager = locationManager;
+        mDebugDataDir = debugDataDir;
+        mOrientationManager = orientationManager;
+        mBurstController = new BurstControllerImpl(
+                mAppController.getAndroidContext(),
+                mBurstResultsListener);
+    }
+
+    /**
+     * Set the content resolver to be updated when saving burst results.
+     *
+     * @param contentResolver to be updated when burst results are saved.
+     */
+    @Override
+    public void setContentResolver(ContentResolver contentResolver) {
+        mContentResolver = contentResolver;
+    }
+
+    @Override
+    public void onCameraAttached(OneCamera camera) {
+        synchronized (mStartStopBurstLock) {
+            mCamera = camera;
+        }
+    }
+
+    @Override
+    public void onCameraDetached() {
+        synchronized (mStartStopBurstLock) {
+            mCamera = null;
+        }
+    }
+
+    @Override
+    public FrameConsumer getPreviewFrameConsumer() {
+        return mBurstController.getPreviewFrameConsumer();
+    }
+
+    @Override
+    public void startBurst() {
+        startBurstImpl();
+    }
+
+    @Override
+    public boolean isBurstRunning() {
+        return (mBurstModuleState.get() == BurstModuleState.RUNNING
+                || mBurstModuleState.get() == BurstModuleState.STOPPING);
+    }
+
+    private void startBurstImpl() {
+        synchronized (mStartStopBurstLock) {
+            if (mCamera != null &&
+                    mBurstModuleState.compareAndSet(BurstModuleState.IDLE,
+                            BurstModuleState.RUNNING)) {
+                // TODO: Use localized strings everywhere.
+                Log.d(TAG, "Starting burst.");
+                Location location = mLocationManager.getCurrentLocation();
+
+                // Set up the capture session.
+                long sessionTime = System.currentTimeMillis();
+                String title = String.format(BURST_TITLE_FORMAT_STRING, sessionTime);
+
+                // TODO: Fix the capture session and use it for saving
+                // intermediate results.
+                CaptureSession session = null;
+
+                BurstConfiguration burstConfig = mBurstController.startBurst();
+                BurstParameters params = new BurstParameters();
+                params.callback = mBurstExtractsResultsCallback;
+                params.burstConfiguration = burstConfig;
+                params.title = title;
+                params.orientation = mOrientationManager.getDeviceOrientation().getDegrees();
+                params.debugDataFolder = mDebugDataDir;
+                params.location = location;
+
+                // Disable the shutter button.
+                mAppController.setShutterEnabled(false);
+
+                // start burst.
+                mCamera.startBurst(params, session);
+            }
+        }
+    }
+
+    @Override
+    public void stopBurst() {
+        synchronized (mStartStopBurstLock) {
+            if (mBurstModuleState.compareAndSet(BurstModuleState.RUNNING,
+                    BurstModuleState.STOPPING)) {
+                if (mCamera != null) {
+                    mCamera.stopBurst();
+                }
+            }
+        }
+    }
+
+    /**
+     * Saves the burst result and on completion re-enables the shutter button.
+     *
+     * @param burstResult the result of the burst.
+     */
+    private void saveBurstResultAndEnableShutterButton(final BurstResult burstResult) {
+        Log.i(TAG, "Saving results of of the burst.");
+
+        AsyncTask<Void, String, Void> saveTask =
+                new AsyncTask<Void, String, Void>() {
+                    @Override
+                    protected Void doInBackground(Void... arg0) {
+                        for (String artifactType : burstResult.getTypes()) {
+                            publishProgress(artifactType);
+                            saveArtifacts(burstResult, artifactType);
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    protected void onPostExecute(Void result) {
+                        mBurstModuleState.set(BurstModuleState.IDLE);
+                        // Re-enable the shutter button.
+                        mAppController.setShutterEnabled(true);
+                    }
+
+                    @Override
+                    protected void onProgressUpdate(String... artifactTypes) {
+                        logProgressUpdate(artifactTypes, burstResult);
+                    }
+                };
+        saveTask.execute(null, null, null);
+    }
+
+    /**
+     * Save individual artifacts for bursts.
+     */
+    private void saveArtifacts(final BurstResult burstResult,
+            final String artifactType) {
+        int index = 0;
+        for (BurstArtifact artifact : burstResult.getArtifactsByType(artifactType)) {
+            for (BurstMediaItem mediaItem : artifact.getMediaItems()) {
+                saveBurstMediaItem(mediaItem,
+                        artifactType, ++index);
+            }
+        }
+    }
+
+    private void saveBurstMediaItem(BurstMediaItem mediaItem,
+            String artifactType,
+            int index) {
+        long timestamp = System.currentTimeMillis();
+        final String mimeType = mediaItem.getMimeType();
+        final String title = String.format(MEDIA_ITEM_FILENAME_FORMAT_STRING,
+                artifactType, index, timestamp);
+        byte[] data = mediaItem.getData();
+        ExifInterface exif = null;
+        if (LocalData.MIME_TYPE_JPEG.equals(mimeType)) {
+            exif = new ExifInterface();
+            exif.addDateTimeStampTag(
+                    ExifInterface.TAG_DATE_TIME,
+                    timestamp,
+                    TimeZone.getDefault());
+
+        }
+        mMediaSaver.addImage(data,
+                title,
+                timestamp,
+                mLocationManager.getCurrentLocation(),
+                mediaItem.getWidth(),
+                mediaItem.getHeight(),
+                mOrientationManager.getDeviceOrientation().getDegrees(),
+                exif, // exif,
+                mOnMediaSavedListener,
+                mContentResolver,
+                mimeType);
+    }
+
+    private void logArtifactCount(final Map<String, Integer> artifactTypeCount) {
+        final String prefix = "Finished burst. Creating ";
+        List<String> artifactDescription = new ArrayList<String>();
+        for (Map.Entry<String, Integer> entry :
+                artifactTypeCount.entrySet()) {
+            artifactDescription.add(entry.getValue() + " " + entry.getKey());
+        }
+
+        String message = prefix + TextUtils.join(" and ", artifactDescription) + ".";
+        Log.d(TAG, message);
+    }
+
+    private void logProgressUpdate(String[] artifactTypes, BurstResult burstResult) {
+        for (String artifactType : artifactTypes) {
+            List<BurstArtifact> artifacts =
+                    burstResult.getArtifactsByType(artifactType);
+            if (!artifacts.isEmpty()) {
+                Log.d(TAG, "Saving " + artifacts.size()
+                        + " " + artifactType + "s.");
+            }
+        }
+    }
+}
index d42bf97..8af9247 100644 (file)
@@ -45,6 +45,7 @@ public interface LocalData extends ImageData {
     static final Log.Tag TAG = new Log.Tag("LocalData");
 
     public static final String MIME_TYPE_JPEG = "image/jpeg";
+    public static final String MIME_TYPE_GIF = "image/gif";
 
     // Data actions.
     public static final int DATA_ACTION_NONE = 0;
index 9b473a7..6342fcf 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.camera.one;
 
+import com.android.camera.session.CaptureSession;
+
 import java.io.File;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -120,4 +122,14 @@ public abstract class AbstractOneCamera implements OneCamera {
     public void setZoom(float zoom) {
         // If not implemented, no-op.
     }
+
+    @Override
+    public void startBurst(BurstParameters params, CaptureSession session) {
+        throw new UnsupportedOperationException("Not implemented yet.");
+    }
+
+    @Override
+    public void stopBurst() {
+        throw new UnsupportedOperationException("Not implemented yet.");
+    }
 }
index 24e0a5c..0af93be 100644 (file)
@@ -21,6 +21,8 @@ import android.location.Location;
 import android.net.Uri;
 import android.view.Surface;
 
+import com.android.camera.burst.BurstConfiguration;
+import com.android.camera.burst.ResultsAccessor;
 import com.android.camera.session.CaptureSession;
 import com.android.camera.util.Size;
 
@@ -215,9 +217,37 @@ public interface OneCamera {
     }
 
     /**
+     * Parameters to be given to capture requests.
+     */
+    public static abstract class CaptureParameters {
+        /** The device orientation so we can compute the right JPEG rotation. */
+        public int orientation = Integer.MIN_VALUE;
+
+        /** The location of this capture. */
+        public Location location = null;
+
+        /** Set this to provide a debug folder for this capture. */
+        public File debugDataFolder;
+
+        protected static void checkRequired(int num) {
+            if (num == Integer.MIN_VALUE) {
+                throw new RuntimeException("Photo capture parameter missing.");
+            }
+        }
+
+        protected static void checkRequired(Object obj) {
+            if (obj == null) {
+                throw new RuntimeException("Photo capture parameter missing.");
+            }
+        }
+
+        public abstract void checkSanity();
+    }
+
+    /**
      * Parameters to be given to photo capture requests.
      */
-    public static final class PhotoCaptureParameters {
+    public static class PhotoCaptureParameters extends CaptureParameters {
         /**
          * Flash modes.
          * <p>
@@ -231,26 +261,20 @@ public interface OneCamera {
         public String title = null;
         /** Called when the capture is completed or failed. */
         public PictureCallback callback = null;
-        /** The device orientation so we can compute the right JPEG rotation. */
-        public int orientation = Integer.MIN_VALUE;
         /** The heading of the device at time of capture. In degrees. */
         public int heading = Integer.MIN_VALUE;
         /** Flash mode for this capture. */
         public Flash flashMode = Flash.AUTO;
-        /** The location of this capture. */
-        public Location location = null;
         /** Zoom value. */
         public float zoom = 1f;
         /** Timer duration in seconds or null for no timer. */
         public Float timerSeconds = null;
 
-        /** Set this to provide a debug folder for this capture. */
-        public File debugDataFolder;
-
         /**
          * Checks whether all required values are set. If one is missing, it
          * throws a {@link RuntimeException}.
          */
+        @Override
         public void checkSanity() {
             checkRequired(title);
             checkRequired(callback);
@@ -258,16 +282,34 @@ public interface OneCamera {
             checkRequired(heading);
         }
 
-        private void checkRequired(int num) {
-            if (num == Integer.MIN_VALUE) {
-                throw new RuntimeException("Photo capture parameter missing.");
-            }
-        }
+    }
+
+    /**
+     * The callback to be invoked when results are available.
+     */
+    public interface BurstResultsCallback {
+        void onBurstComplete(ResultsAccessor resultAccessor);
+    }
+
+    /**
+     * Parameters to be given to burst requests.
+     */
+    public static class BurstParameters extends CaptureParameters {
+        /** The title/filename (without suffix) for this capture. */
+        public String title = null;
+        public BurstConfiguration burstConfiguration;
+        public BurstResultsCallback callback;
+
+        /**
+         * Checks whether all required values are set. If one is missing, it
+         * throws a {@link RuntimeException}.
+         */
+        @Override
+        public void checkSanity() {
+            checkRequired(title);
+            checkRequired(callback);
+            checkRequired(burstConfiguration);
 
-        private void checkRequired(Object obj) {
-            if (obj == null) {
-                throw new RuntimeException("Photo capture parameter missing.");
-            }
         }
     }
 
@@ -291,6 +333,21 @@ public interface OneCamera {
     public void takePicture(PhotoCaptureParameters params, CaptureSession session);
 
     /**
+     * Call this to take a burst.
+     *
+     * @param params parameters for taking burst.
+     * @param session the capture session for this burst.
+     */
+
+    public void startBurst(BurstParameters params, CaptureSession session);
+
+    /**
+     * Call this to stop taking burst.
+     *
+     */
+    public void stopBurst();
+
+    /**
      * Sets or replaces a listener that is called whenever the camera encounters
      * an error.
      */
index 09c2bdb..4240a89 100644 (file)
@@ -29,6 +29,7 @@ import android.os.Handler;
 import android.os.SystemClock;
 import android.util.Pair;
 
+import com.android.camera.burst.BurstConfiguration.EvictionHandler;
 import com.android.camera.debug.Log;
 import com.android.camera.debug.Log.Tag;
 import com.android.camera.util.ConcurrentSharedRingBuffer;
@@ -44,13 +45,15 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Implements {@link android.media.ImageReader.OnImageAvailableListener} and
  * {@link android.hardware.camera2.CameraCaptureSession.CaptureListener} to
  * store the results of capture requests (both {@link Image}s and
- * {@link TotalCaptureResult}s in a ring-buffer from which they may be saved. 
+ * {@link TotalCaptureResult}s in a ring-buffer from which they may be saved.
  * <br>
  * This also manages the lifecycle of {@link Image}s within the application as
  * they are passed in from the lower-level camera2 API.
@@ -215,8 +218,50 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
         public TotalCaptureResult tryGetMetadata() {
             return mMetadata;
         }
+
+        /**
+         * Returs the timestamp of the image if present, -1 otherwise.
+         */
+        public long tryGetTimestamp() {
+            if (mImage != null) {
+                return mImage.getTimestamp();
+            }
+            if (mMetadata != null) {
+                return mMetadata.get(TotalCaptureResult.SENSOR_TIMESTAMP);
+            }
+            return -1;
+        }
     }
 
+    /**
+     * A stub implementation of eviction handler that returns -1 as timestamp of
+     * the frame to be dropped.
+     * <p/>
+     * This forces the ring buffer to use its default eviction strategy.
+     */
+    private static class DefaultEvictionHandler implements EvictionHandler {
+        @Override
+        public long selectFrameToDrop() {
+            return -1;
+        }
+
+        @Override
+        public void onFrameCaptureResultAvailable(long timestamp,
+                TotalCaptureResult captureResult) {
+        }
+
+        @Override
+        public void onFrameInserted(long timestamp) {
+        }
+
+        @Override
+        public void onFrameDropped(long timestamp) {
+        }
+    }
+
+    private static final EvictionHandler DEFAULT_EVICTION_HANDLER =
+            new DefaultEvictionHandler();
+
     private static final Tag TAG = new Tag("ZSLImageListener");
 
     /**
@@ -274,6 +319,10 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
      */
     private final Executor mImageCaptureListenerExecutor;
 
+    private final AtomicReference<EvictionHandler> mEvictionHandler =
+            new AtomicReference<EvictionHandler>(DEFAULT_EVICTION_HANDLER);
+    private final AtomicBoolean mIsCapturingBurst = new AtomicBoolean(false);
+
     /**
      * The set of constraints which must be satisfied for a newly acquired image
      * to be captured and sent to {@link #mPendingImageCaptureCallback}. null if
@@ -304,6 +353,12 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
             mMetadataChangeListeners = new ConcurrentHashMap<Key<?>, Set<MetadataChangeListener>>();
 
     /**
+     * The lock for guarding installation and uninstallation of burst eviction
+     * handler.
+     */
+    private final Object mBurstLock = new Object();
+
+    /**
      * @param maxImages the maximum number of images provided by the
      *            {@link ImageReader}. This must be greater than 2.
      * @param listenerHandler the handler on which to invoke listeners. Note
@@ -433,34 +488,93 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
         // Find the CapturedImage in the ring-buffer and attach the
         // TotalCaptureResult to it.
         // See documentation for swapLeast() for details.
-        boolean swapSuccess = mCapturedImageBuffer.swapLeast(timestamp,
+        boolean swapSuccess = doMetaDataSwap(result, timestamp);
+        if (!swapSuccess) {
+            // Do nothing on failure to swap in.
+            Log.v(TAG, "Unable to add new image metadata to ring-buffer.");
+        }
+
+        tryExecutePendingCaptureRequest(timestamp);
+    }
+
+    private boolean doMetaDataSwap(final TotalCaptureResult newMetadata, final long timestamp) {
+        mEvictionHandler.get().onFrameCaptureResultAvailable(timestamp, newMetadata);
+
+        if (mIsCapturingBurst.get()) {
+            // In case of burst we do not swap metadata in the ring buffer. This
+            // is to avoid the following scenario. If image for frame with
+            // timestamp A arrives first and the eviction handler decides to
+            // evict timestamp A. But when metadata for timestamp A arrives
+            // the eviction handler chooses to keep timestamp A. In this case
+            // the image for A will never be available.
+            return false;
+        }
+
+        return mCapturedImageBuffer.swapLeast(timestamp,
                 new SwapTask<CapturedImage>() {
                 @Override
                     public CapturedImage create() {
                         CapturedImage image = new CapturedImage();
-                        image.addMetadata(result);
+                        image.addMetadata(newMetadata);
                         return image;
                     }
 
                 @Override
                     public CapturedImage swap(CapturedImage oldElement) {
                         oldElement.reset();
-                        oldElement.addMetadata(result);
+                        oldElement.addMetadata(newMetadata);
                         return oldElement;
                     }
 
                 @Override
                     public void update(CapturedImage existingElement) {
-                        existingElement.addMetadata(result);
+                        existingElement.addMetadata(newMetadata);
+                    }
+
+                @Override
+                    public long getSwapKey() {
+                        return -1;
                     }
                 });
+    }
 
-        if (!swapSuccess) {
-            // Do nothing on failure to swap in.
-            Log.v(TAG, "Unable to add new image metadata to ring-buffer.");
-        }
+    private boolean doImageSwap(final Image newImage) {
+        return mCapturedImageBuffer.swapLeast(newImage.getTimestamp(),
+                new SwapTask<CapturedImage>() {
+                @Override
+                    public CapturedImage create() {
+                        mEvictionHandler.get().onFrameInserted(newImage.getTimestamp());
+                        CapturedImage image = new CapturedImage();
+                        image.addImage(newImage);
+                        return image;
+                    }
 
-        tryExecutePendingCaptureRequest(timestamp);
+                @Override
+                    public CapturedImage swap(CapturedImage oldElement) {
+                        mEvictionHandler.get().onFrameInserted(newImage.getTimestamp());
+                        long timestamp = oldElement.tryGetTimestamp();
+                        mEvictionHandler.get().onFrameDropped(timestamp);
+                        oldElement.reset();
+                        CapturedImage image = new CapturedImage();
+                        image.addImage(newImage);
+                        return image;
+                    }
+
+                @Override
+                    public void update(CapturedImage existingElement) {
+                        mEvictionHandler.get().onFrameInserted(newImage.getTimestamp());
+                        existingElement.addImage(newImage);
+                    }
+
+                @Override
+                    public long getSwapKey() {
+                    final long toDropTimestamp = mEvictionHandler.get().selectFrameToDrop();
+                        if (toDropTimestamp > 0) {
+                            mCapturedImageBuffer.releaseIfPinned(toDropTimestamp);
+                        }
+                        return toDropTimestamp;
+                    }
+                });
     }
 
     @Override
@@ -475,29 +589,9 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
                 Log.v(TAG, "Acquired an image. Number of open images = " + numOpenImages);
             }
 
+            long timestamp = img.getTimestamp();
             // Try to place the newly-acquired image into the ring buffer.
-            boolean swapSuccess = mCapturedImageBuffer.swapLeast(
-                    img.getTimestamp(), new SwapTask<CapturedImage>() {
-                            @Override
-                        public CapturedImage create() {
-                            CapturedImage image = new CapturedImage();
-                            image.addImage(img);
-                            return image;
-                        }
-
-                            @Override
-                        public CapturedImage swap(CapturedImage oldElement) {
-                            oldElement.reset();
-                            oldElement.addImage(img);
-                            return oldElement;
-                        }
-
-                            @Override
-                        public void update(CapturedImage existingElement) {
-                            existingElement.addImage(img);
-                        }
-                    });
-
+            boolean swapSuccess = doImageSwap(img);
             if (!swapSuccess) {
                 // If we were unable to save the image to the ring buffer, we
                 // must close it now.
@@ -507,9 +601,14 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
                 if (DEBUG_PRINT_OPEN_IMAGE_COUNT) {
                     Log.v(TAG, "Closed an image. Number of open images = " + numOpenImages);
                 }
+            } else {
+                if (mIsCapturingBurst.get()) {
+                    // In case of burst we pin every image.
+                    mCapturedImageBuffer.tryPin(timestamp);
+                }
             }
 
-            tryExecutePendingCaptureRequest(img.getTimestamp());
+            tryExecutePendingCaptureRequest(timestamp);
 
             long endTime = SystemClock.currentThreadTimeMillis();
             long totTime = endTime - startTime;
@@ -526,16 +625,7 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
      * s.
      */
     public void close() {
-        try {
-            mCapturedImageBuffer.close(new Task<CapturedImage>() {
-                    @Override
-                public void run(CapturedImage e) {
-                    e.reset();
-                }
-            });
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
+        closeBuffer();
     }
 
     /**
@@ -692,4 +782,107 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im
             return true;
         }
     }
+
+    /**
+     * Tries to capture a pinned image for the given key from the ring-buffer.
+     *
+     * @return the pair of (image, captureResult) if image is found, null
+     *         otherwise.
+     */
+    public Pair<Image, TotalCaptureResult>
+            tryCapturePinnedImage(long timestamp) {
+        final Pair<Long, CapturedImage> toCapture =
+                mCapturedImageBuffer.tryGetPinned(timestamp);
+        Image pinnedImage = null;
+        TotalCaptureResult imageCaptureResult = null;
+        // Return an Image
+        if (toCapture != null && toCapture.second != null) {
+            pinnedImage = toCapture.second.tryGetImage();
+            imageCaptureResult = toCapture.second.tryGetMetadata();
+        }
+        return Pair.create(pinnedImage, imageCaptureResult);
+    }
+
+    /**
+     * Sets a new burst eviction handler for the internal buffer.
+     * <p/>
+     * Also clears the buffer. If there was an old burst eviction handler
+     * already installed this method will throw an exception.
+     *
+     * @param evictionHandler the handler to install on the internal image
+     *            buffer.
+     */
+    public void setBurstEvictionHandler(EvictionHandler evictionHandler) {
+        if (evictionHandler == null) {
+            throw new IllegalArgumentException("setBurstEvictionHandler: evictionHandler is null.");
+        }
+        synchronized (mBurstLock) {
+            if (mIsCapturingBurst.compareAndSet(false, true)) {
+                if (!mEvictionHandler.compareAndSet(DEFAULT_EVICTION_HANDLER,
+                        evictionHandler)) {
+                    throw new IllegalStateException(
+                            "Trying to set eviction handler before restoring the original.");
+                } else {
+                    clearCapturedImageBuffer(0);
+                }
+            } else {
+                throw new IllegalStateException("Trying to start burst when it was already running.");
+            }
+        }
+    }
+
+    /**
+     * Removes the burst eviction handler from the buffer.
+     */
+    public void resetEvictionHandler() {
+        synchronized (mBurstLock) {
+            mEvictionHandler.set(DEFAULT_EVICTION_HANDLER);
+        }
+    }
+
+    /**
+     * Clears the underlying buffer and reset the eviction handler.
+     */
+    public void resetCaptureState() {
+        synchronized (mBurstLock) {
+            if (mIsCapturingBurst.compareAndSet(true, false)) {
+                // By default the image buffer has 1 slot that is reserved for
+                // unpinned elements.
+                clearCapturedImageBuffer(1);
+                mEvictionHandler.set(DEFAULT_EVICTION_HANDLER);
+            }
+        }
+    }
+
+    /**
+     * Clear the buffer and reserves <code>unpinnedReservedSlots</code> in the buffer.
+     *
+     * @param unpinnedReservedSlots the number of unpinned slots that are never
+     *            allowed to be pinned.
+     */
+    private void clearCapturedImageBuffer(int unpinnedReservedSlots) {
+        mCapturedImageBuffer.releaseAll();
+        closeBuffer();
+        try {
+            mCapturedImageBuffer.reopenBuffer(unpinnedReservedSlots);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Closes the buffer and frees up any images in the buffer.
+     */
+    private void closeBuffer() {
+        try {
+            mCapturedImageBuffer.close(new Task<CapturedImage>() {
+                @Override
+                public void run(CapturedImage e) {
+                    e.reset();
+                }
+            });
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
 }
index 107ea44..c9905fe 100644 (file)
@@ -39,10 +39,13 @@ import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.support.v4.util.Pools;
+import android.util.Pair;
 import android.view.Surface;
 
 import com.android.camera.CaptureModuleUtil;
 import com.android.camera.app.MediaSaver.OnMediaSavedListener;
+import com.android.camera.burst.BurstImage;
+import com.android.camera.burst.ResultsAccessor;
 import com.android.camera.debug.Log;
 import com.android.camera.debug.Log.Tag;
 import com.android.camera.exif.ExifInterface;
@@ -56,8 +59,8 @@ import com.android.camera.one.v2.ImageCaptureManager.ImageCaptureListener;
 import com.android.camera.one.v2.ImageCaptureManager.MetadataChangeListener;
 import com.android.camera.session.CaptureSession;
 import com.android.camera.util.CameraUtil;
-import com.android.camera.util.ListenerCombiner;
 import com.android.camera.util.JpegUtilNative;
+import com.android.camera.util.ListenerCombiner;
 import com.android.camera.util.Size;
 
 import java.nio.ByteBuffer;
@@ -67,9 +70,12 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * {@link OneCamera} implementation directly on top of the Camera2 API with zero
@@ -84,12 +90,12 @@ public class OneCameraZslImpl extends AbstractOneCamera {
     private static final int JPEG_QUALITY =
             CameraProfile.getJpegEncodingQualityParameter(CameraProfile.QUALITY_HIGH);
     /**
-     * The maximum number of images to store in the full-size ZSL ring buffer. 
+     * The maximum number of images to store in the full-size ZSL ring buffer.
      * <br>
      * TODO: Determine this number dynamically based on available memory and the
      * size of frames.
      */
-    private static final int MAX_CAPTURE_IMAGES = 10;
+    private static final int MAX_CAPTURE_IMAGES = 12;
     /**
      * True if zero-shutter-lag images should be captured. Some devices produce
      * lower-quality images for the high-frequency stream, so we may wish to
@@ -188,6 +194,9 @@ public class OneCameraZslImpl extends AbstractOneCamera {
 
     private MediaActionSound mMediaActionSound = new MediaActionSound();
 
+    private final AtomicReference<BurstParameters>
+            mBurstParams = new AtomicReference<BurstParameters>();
+
     /**
      * Ready state (typically displayed by the UI shutter-button) depends on two
      * things:<br>
@@ -1079,4 +1088,70 @@ public class OneCameraZslImpl extends AbstractOneCamera {
     private Rect cropRegionForZoom(float zoom) {
         return AutoFocusHelper.cropRegionForZoom(mCharacteristics, zoom);
     }
+
+    @Override
+    public void startBurst(BurstParameters params, CaptureSession session) {
+        params.checkSanity();
+        if (!mBurstParams.compareAndSet(null, params)) {
+            throw new IllegalStateException(
+                    "Attempting to start burst, when burst is already running.");
+        }
+        mCaptureManager.setBurstEvictionHandler(params.
+                burstConfiguration.getEvictionHandler());
+    }
+
+    private class ImageExtractor implements ResultsAccessor {
+        private final int mOrientation;
+
+        public ImageExtractor(int orientation) {
+            mOrientation = orientation;
+        }
+
+        @Override
+        public Future<BurstImage> extractImage(final long timestampToExtract) {
+            final Pair<Image, TotalCaptureResult> pinnedImageData =
+                    mCaptureManager.tryCapturePinnedImage(timestampToExtract);
+            return mImageSaverThreadPool.submit(new Callable<BurstImage>() {
+
+                @Override
+                public BurstImage call() throws Exception {
+                    BurstImage burstImage = null;
+                    Image image = pinnedImageData.first;
+                    if (image != null) {
+                        burstImage = new BurstImage();
+                        int degrees = CameraUtil.getJpegRotation(mOrientation, mCharacteristics);
+                        Size size = getImageSizeForOrientation(image.getWidth(),
+                                image.getHeight(),
+                                degrees);
+                        burstImage.width = size.getWidth();
+                        burstImage.height = size.getHeight();
+                        burstImage.data = acquireJpegBytes(image,
+                                degrees);
+                        burstImage.captureResult = pinnedImageData.second;
+                        burstImage.timestamp = timestampToExtract;
+                    } else {
+                        Log.e(TAG, "Failed to extract burst image for timestamp: "
+                                + timestampToExtract);
+                    }
+                    return burstImage;
+                }
+            });
+        }
+
+        @Override
+        public void close() {
+            mCaptureManager.resetCaptureState();
+        }
+    }
+
+    @Override
+    public void stopBurst() {
+        if (mBurstParams.get() == null) {
+            throw new IllegalStateException("Burst parameters should not be null.");
+        }
+        mCaptureManager.resetEvictionHandler();
+        mBurstParams.get().callback.onBurstComplete(
+                new ImageExtractor(mBurstParams.get().orientation));
+        mBurstParams.set(null);
+    }
 }
index 4e44972..249cc19 100644 (file)
@@ -70,6 +70,16 @@ public class ConcurrentSharedRingBuffer<E> {
          * @param existingElement the element to be updated.
          */
         public void update(E existingElement);
+
+        /**
+         * Returns the key of the element that the ring buffer should prefer
+         * when considering a swapping candidate. If the returned key is not an
+         * unpinned element then ring buffer will replace the element with least
+         * key.
+         *
+         * @return a key of an existing unpinned element or a negative value.
+         */
+        public long getSwapKey();
     }
 
     /**
@@ -119,6 +129,29 @@ public class ConcurrentSharedRingBuffer<E> {
         }
     }
 
+    /**
+     * A Semaphore that allows to reduce permits to negative values.
+     */
+    private static class NegativePermitsSemaphore extends Semaphore {
+        public NegativePermitsSemaphore(int permits) {
+            super(permits);
+        }
+
+        /**
+         * Reduces the number of permits by <code>permits</code>.
+         * <p/>
+         * This method can only be called when number of available permits is
+         * zero.
+         */
+        @Override
+        public void reducePermits(int permits) {
+            if (availablePermits() != 0) {
+                throw new IllegalStateException("Called without draining the semaphore.");
+            }
+            super.reducePermits(permits);
+        }
+    }
+
     /** Allow only one swapping operation at a time. */
     private final Object mSwapLock = new Object();
     /**
@@ -137,7 +170,7 @@ public class ConcurrentSharedRingBuffer<E> {
     /** Used to acquire space in mElements. */
     private final Semaphore mCapacitySemaphore;
     /** This must be acquired while an element is pinned. */
-    private final Semaphore mPinSemaphore;
+    private final NegativePermitsSemaphore mPinSemaphore;
     private boolean mClosed = false;
 
     private Handler mPinStateHandler = null;
@@ -159,7 +192,7 @@ public class ConcurrentSharedRingBuffer<E> {
         // Start with -1 permits to pin elements since we must always have at
         // least one unpinned
         // element available to swap out as the head of the buffer.
-        mPinSemaphore = new Semaphore(-1);
+        mPinSemaphore = new NegativePermitsSemaphore(-1);
     }
 
     /**
@@ -239,20 +272,36 @@ public class ConcurrentSharedRingBuffer<E> {
                     if (mClosed) {
                         return false;
                     }
+                    Pair<Long, Pinnable<E>> toSwapEntry = null;
+                    long swapKey = swapper.getSwapKey();
+                    // If swapKey is same as the inserted key return early.
+                    if (swapKey == newKey) {
+                        return false;
+                    }
 
-                    Map.Entry<Long, Pinnable<E>> toSwapEntry = mUnpinnedElements.pollFirstEntry();
+                    if (mUnpinnedElements.containsKey(swapKey)) {
+                        toSwapEntry = Pair.create(swapKey, mUnpinnedElements.remove(swapKey));
+                    } else {
+                        // The returned key from getSwapKey was not found in the
+                        // unpinned elements use the least entry from the
+                        // unpinned elements.
+                        Map.Entry<Long, Pinnable<E>> swapEntry = mUnpinnedElements.pollFirstEntry();
+                        if (swapEntry != null) {
+                            toSwapEntry = Pair.create(swapEntry.getKey(), swapEntry.getValue());
+                        }
+                    }
 
                     if (toSwapEntry == null) {
-                        // We should never get here.
-                        throw new RuntimeException("No unpinned element available.");
+                        // We can get here if no unpinned element was found.
+                        return false;
                     }
 
-                    toSwap = toSwapEntry.getValue();
+                    toSwap = toSwapEntry.second;
 
                     // We must remove the element from both mElements and
                     // mUnpinnedElements because it must be re-added after the
                     // swap to be placed in the correct order with newKey.
-                    mElements.remove(toSwapEntry.getKey());
+                    mElements.remove(toSwapEntry.first);
                 }
 
                 try {
@@ -335,7 +384,8 @@ public class ConcurrentSharedRingBuffer<E> {
             Pinnable<E> element = mElements.get(key);
 
             if (element == null) {
-                throw new InvalidParameterException("No entry found for the given key.");
+                throw new InvalidParameterException(
+                        "No entry found for the given key: " + key + ".");
             }
 
             if (!element.isPinned()) {
@@ -458,6 +508,8 @@ public class ConcurrentSharedRingBuffer<E> {
 
         for (Pinnable<E> element : mElements.values()) {
             task.run(element.mElement);
+            // Release the capacity permits.
+            mCapacitySemaphore.release();
         }
 
         mUnpinnedElements.clear();
@@ -465,6 +517,107 @@ public class ConcurrentSharedRingBuffer<E> {
         mElements.clear();
     }
 
+    /**
+     * Attempts to get a pinned element for the given key.
+     *
+     * @param key the key of the pinned element.
+     * @return (key, value) pair if found otherwise null.
+     */
+    public Pair<Long, E> tryGetPinned(long key) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return null;
+            }
+            for (java.util.Map.Entry<Long, Pinnable<E>> element : mElements.entrySet()) {
+                if (element.getKey() == key) {
+                    if (element.getValue().isPinned()) {
+                        return Pair.create(element.getKey(), element.getValue().getElement());
+                    } else {
+                        return null;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Reopens previously closed buffer.
+     * <p/>
+     * Buffer should be closed before calling this method. If called with an
+     * open buffer an {@link IllegalStateException} is thrown.
+     *
+     * @param unpinnedReservedSlotCount a non-negative integer for number of
+     *            slots to reserve for unpinned elements. These slots can never
+     *            be pinned and will always be available for swapping.
+     * @throws InterruptedException
+     */
+    public void reopenBuffer(int unpinnedReservedSlotCount)
+            throws InterruptedException {
+        if (unpinnedReservedSlotCount < 0
+                || unpinnedReservedSlotCount >= mCapacitySemaphore.availablePermits()) {
+            throw new IllegalArgumentException("Invalid unpinned reserved slot count: " +
+                    unpinnedReservedSlotCount);
+        }
+
+        // Ensure that any pending swap tasks complete before closing.
+        synchronized (mSwapLock) {
+            synchronized (mLock) {
+                if (!mClosed) {
+                    throw new IllegalStateException(
+                            "Attempt to reopen the buffer when it is not closed.");
+                }
+
+                mPinSemaphore.drainPermits();
+                mPinSemaphore.reducePermits(unpinnedReservedSlotCount);
+                mClosed = false;
+            }
+        }
+    }
+
+    /**
+     * Releases a pinned element for the given key.
+     * <p/>
+     * If element is unpinned, it is not released.
+     *
+     * @param key the key of the element, if the element is not present an
+     *            {@link IllegalArgumentException} is thrown.
+     */
+    public void releaseIfPinned(long key) {
+        synchronized (mLock) {
+            Pinnable<E> element = mElements.get(key);
+
+            if (element == null) {
+                throw new IllegalArgumentException("Invalid key." + key);
+            }
+
+            if (element.isPinned()) {
+                release(key);
+            }
+        }
+    }
+
+    /**
+     * Releases all pinned elements in the buffer.
+     * <p/>
+     * Note: it only calls {@link #release(long)} only once on a pinned element.
+     */
+    public void releaseAll() {
+        synchronized (mSwapLock) {
+            synchronized (mLock) {
+                if (mClosed || mElements.isEmpty()
+                        || mElements.size() == mUnpinnedElements.size()) {
+                    return;
+                }
+                for (java.util.Map.Entry<Long, Pinnable<E>> entry : mElements.entrySet()) {
+                    if (entry.getValue().isPinned()) {
+                        release(entry.getKey());
+                    }
+                }
+            }
+        }
+    }
+
     private void notifyPinStateChange(final boolean pinsAvailable) {
         synchronized (mLock) {
             // We must synchronize on mPinStateHandler and mPinStateListener.
index 4da24d5..a4a4434 100644 (file)
@@ -114,6 +114,11 @@ public class ModeOptionsOverlay extends FrameLayout
         // noop
     }
 
+    @Override
+    public void onShutterButtonLongPressed() {
+        // noop
+    }
+
     /**
      * Schedule (or re-schedule) the options menu to be closed after a number
      * of milliseconds.  If the options menu is already closed, nothing is
diff --git a/src_pd/com/android/camera/burst/BurstControllerImpl.java b/src_pd/com/android/camera/burst/BurstControllerImpl.java
new file mode 100644 (file)
index 0000000..dbf8d77
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 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.burst;
+
+import android.content.Context;
+
+import com.android.camera.gl.FrameDistributor.FrameConsumer;
+
+/**
+ * Stub implementation for burst controller.
+ */
+class BurstControllerImpl implements BurstController {
+    /**
+     * Create a new BurstController.
+     *
+     * @param context the context of the application.
+     * @param resultsListener listener for listening to burst events.
+     */
+    public BurstControllerImpl(Context context, BurstResultsListener resultsListener) {
+    }
+
+    /**
+     * Returns true if burst mode is supported by camera.
+     */
+    public static boolean isBurstModeSupported() {
+        return false;
+    }
+
+    @Override
+    public BurstConfiguration startBurst() {
+        return null;
+    }
+
+    @Override
+    public void stopBurst(ResultsAccessor resultsAccessor) {
+        // no op
+    }
+
+    @Override
+    public void onPreviewSizeChanged(int width, int height) {
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation, boolean isMirrored) {
+    }
+
+    @Override
+    public FrameConsumer getPreviewFrameConsumer() {
+        throw new IllegalStateException("Not implemented.");
+    }
+}