* @return An accessibility String to be announced during the peek animation.
*/
public abstract String getPeekAccessibilityString();
+
+ @Override
+ public void onShutterButtonLongPressed() {
+ // noop
+ }
}
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;
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;
/**
}
};
+
private static final Tag TAG = new Tag("CaptureModule");
private static final String PHOTO_MODULE_STRING_ID = "PhotoModule";
/** Enable additional debug output. */
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;
/** 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;
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
thread.start();
mCameraHandler = new Handler(thread.getLooper());
mCameraManager = mAppController.getCameraManager();
- mLocationManager = mAppController.getLocationManager();
mDisplayRotation = CameraUtil.getDisplayRotation(mContext);
mCameraFacing = getFacingFromCameraId(mSettingsManager.getInteger(
mAppController.getModuleScope(),
mLayoutListener);
mAppController.setPreviewStatusListener(mUI);
+ mBurstController.setContentResolver(activity.getContentResolver());
// Set the preview texture from UI for the SurfaceTextureConsumer.
mPreviewConsumer.setSurfaceTexture(
mAppController.getCameraAppUI().getSurfaceTexture(),
}
@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
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);
}
public void pause() {
mPaused = true;
getServices().getRemoteShutterListener().onModuleExit();
+ mBurstController.stopBurst();
cancelCountDown();
closeCamera();
resetTextureBufferSize();
@Override
public void onLayoutOrientationChanged(boolean isLandscape) {
Log.d(TAG, "onLayoutOrientationChanged");
+ mBurstController.stopBurst();
}
@Override
@Override
public void onReadyStateChanged(boolean readyForCapture) {
+ if (mBurstController.isBurstRunning()) {
+ return;
+ }
+
if (readyForCapture) {
mAppController.getCameraAppUI().enableModeOptions();
}
@Override
public void onCameraClosed() {
mCamera = null;
+ mBurstController.onCameraDetached();
mCameraOpenCloseLock.release();
}
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
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;
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()) {
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
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;
this.width = width;
this.height = height;
this.orientation = orientation;
+ this.mimeType = mimeType;
this.exif = exif;
this.resolver = resolver;
this.listener = listener;
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
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;
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.
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.
*/
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
public ShutterButton(Context context, AttributeSet attrs) {
super(context, attrs);
+ mGestureDetector = new GestureDetector(context, new LongPressGestureListener());
+ mGestureDetector.setIsLongpressEnabled(true);
}
/**
@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());
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());
* @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,
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);
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;
}
/**
// noop
}
+ @Override
+ public void onShutterButtonLongPressed() {
+ // noop
+ }
+
/**
* Set the mode options toggle clickable.
*/
/**
* 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.
/**
* 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.
*
* 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.
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.");
+ }
+ }
+ }
+}
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;
package com.android.camera.one;
+import com.android.camera.session.CaptureSession;
+
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
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.");
+ }
}
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;
}
/**
+ * 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>
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);
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.");
- }
}
}
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.
*/
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;
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.
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");
/**
*/
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
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
// 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
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.
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;
* s.
*/
public void close() {
- try {
- mCapturedImageBuffer.close(new Task<CapturedImage>() {
- @Override
- public void run(CapturedImage e) {
- e.reset();
- }
- });
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
+ closeBuffer();
}
/**
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();
+ }
+ }
}
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;
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;
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
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
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>
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);
+ }
}
* @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();
}
/**
}
}
+ /**
+ * 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();
/**
/** 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;
// 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);
}
/**
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 {
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()) {
for (Pinnable<E> element : mElements.values()) {
task.run(element.mElement);
+ // Release the capacity permits.
+ mCapacitySemaphore.release();
}
mUnpinnedElements.clear();
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.
// 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
--- /dev/null
+/*
+ * 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.");
+ }
+}