diff options
15 files changed, 522 insertions, 237 deletions
diff --git a/src/com/android/camera/CaptureModule.java b/src/com/android/camera/CaptureModule.java index c65d029ab..44428b1c6 100644 --- a/src/com/android/camera/CaptureModule.java +++ b/src/com/android/camera/CaptureModule.java @@ -59,12 +59,8 @@ import com.android.camera.one.OneCameraAccessException; import com.android.camera.one.OneCameraCharacteristics; import com.android.camera.one.OneCameraManager; import com.android.camera.one.v2.OneCameraManagerImpl; -import com.android.camera.one.v2.imagesaver.ImageSaver; -import com.android.camera.one.v2.imagesaver.YuvImageBackendImageSaver; import com.android.camera.one.v2.photo.ImageRotationCalculator; import com.android.camera.one.v2.photo.ImageRotationCalculatorImpl; -import com.android.camera.processing.ProcessingServiceManager; -import com.android.camera.processing.imagebackend.ImageBackend; import com.android.camera.remote.RemoteCameraModule; import com.android.camera.session.CaptureSession; import com.android.camera.settings.Keys; @@ -252,7 +248,8 @@ public class CaptureModule extends CameraModule implements mMainHandler.post(new Runnable() { @Override public void run() { - mAppController.getServices().getRemoteShutterListener().onPictureTaken(jpegImage); + mAppController.getServices().getRemoteShutterListener() + .onPictureTaken(jpegImage); } }); } @@ -1182,15 +1179,10 @@ public class CaptureModule extends CameraModule implements return; } - // Create the image saver. - // Used to rotate images the right way based on the sensor used - // for taking the image. + // Derive objects necessary for camera creation. MainThread mainThread = MainThread.create(); ImageRotationCalculator imageRotationCalculator = ImageRotationCalculatorImpl .from(cameraCharacteristics); - ImageBackend imageBackend = ProcessingServiceManager.getImageBackendInstance(); - ImageSaver.Builder imageSaverBuilder = new YuvImageBackendImageSaver( - mainThread, imageRotationCalculator, imageBackend); // Only enable HDR on the back camera boolean useHdr = mHdrEnabled && mCameraFacing == Facing.BACK; @@ -1203,96 +1195,96 @@ public class CaptureModule extends CameraModule implements return; } - mCameraManager.open(mCameraFacing, useHdr, mPictureSize, imageSaverBuilder, - new OpenCallback() { - @Override - public void onFailure() { - Log.e(TAG, "Could not open camera."); - mCamera = null; - mCameraOpenCloseLock.release(); - mAppController.showErrorAndFinish(R.string.cannot_connect_camera); - } - - @Override - public void onCameraClosed() { - mCamera = null; - mBurstController.onCameraDetached(); - mCameraOpenCloseLock.release(); - } - - @Override - 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 - // the last frame lingering. We need to hold off setting - // transform until preview is started. - updateFrameDistributorBufferSize(); - mState = ModuleState.WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED; - Log.d(TAG, "starting preview ..."); - - // TODO: make mFocusController final and remove null - // check. - if (mFocusController != null) { - camera.setFocusDistanceListener(mFocusController); - } - - // TODO: Consider rolling these two calls into one. - camera.startPreview(new Surface(getPreviewSurfaceTexture()), - new CaptureReadyCallback() { - @Override - public void onSetupFailed() { - // We must release this lock here, - // before posting to the main handler - // since we may be blocked in pause(), - // getting ready to close the camera. - mCameraOpenCloseLock.release(); - Log.e(TAG, "Could not set up preview."); - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCamera == null) { - Log.d(TAG, "Camera closed, aborting."); - return; + mCameraManager.open(mCameraFacing, useHdr, mPictureSize, + new OpenCallback() { + @Override + public void onFailure() { + Log.e(TAG, "Could not open camera."); + mCamera = null; + mCameraOpenCloseLock.release(); + mAppController.showErrorAndFinish(R.string.cannot_connect_camera); + } + + @Override + public void onCameraClosed() { + mCamera = null; + mBurstController.onCameraDetached(); + mCameraOpenCloseLock.release(); + } + + @Override + 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 + // the last frame lingering. We need to hold off setting + // transform until preview is started. + updateFrameDistributorBufferSize(); + mState = ModuleState.WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED; + Log.d(TAG, "starting preview ..."); + + // TODO: make mFocusController final and remove null + // check. + if (mFocusController != null) { + camera.setFocusDistanceListener(mFocusController); + } + + // TODO: Consider rolling these two calls into one. + camera.startPreview(new Surface(getPreviewSurfaceTexture()), + new CaptureReadyCallback() { + @Override + public void onSetupFailed() { + // We must release this lock here, + // before posting to the main handler + // since we may be blocked in pause(), + // getting ready to close the camera. + mCameraOpenCloseLock.release(); + Log.e(TAG, "Could not set up preview."); + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCamera == null) { + Log.d(TAG, "Camera closed, aborting."); + return; + } + mCamera.close(); + mCamera = null; + // TODO: Show an error message + // and exit. } - mCamera.close(); - mCamera = null; - // TODO: Show an error message - // and exit. - } - }); - } - - @Override - public void onReadyForCapture() { - // We must release this lock here, - // before posting to the main handler - // since we may be blocked in pause(), - // getting ready to close the camera. - mCameraOpenCloseLock.release(); - mMainHandler.post(new Runnable() { - @Override - public void run() { - Log.d(TAG, "Ready for capture."); - if (mCamera == null) { - Log.d(TAG, "Camera closed, aborting."); - return; + }); + } + + @Override + public void onReadyForCapture() { + // We must release this lock here, + // before posting to the main handler + // since we may be blocked in pause(), + // getting ready to close the camera. + mCameraOpenCloseLock.release(); + mMainHandler.post(new Runnable() { + @Override + public void run() { + Log.d(TAG, "Ready for capture."); + if (mCamera == null) { + Log.d(TAG, "Camera closed, aborting."); + return; + } + onPreviewStarted(); + // Enable zooming after preview + // has started. + mUI.initializeZoom(mCamera.getMaxZoom()); + mCamera.setFocusStateListener(CaptureModule.this); + mCamera.setReadyStateChangedListener(CaptureModule.this); } - onPreviewStarted(); - // Enable zooming after preview - // has started. - mUI.initializeZoom(mCamera.getMaxZoom()); - mCamera.setFocusStateListener(CaptureModule.this); - mCamera.setReadyStateChangedListener(CaptureModule.this); - } - }); - } - }); - } - }, mCameraHandler); + }); + } + }); + } + }, mCameraHandler, mainThread, imageRotationCalculator); } private void closeCamera() { diff --git a/src/com/android/camera/module/ModulesInfo.java b/src/com/android/camera/module/ModulesInfo.java index a8ecb68c9..327df75c8 100644 --- a/src/com/android/camera/module/ModulesInfo.java +++ b/src/com/android/camera/module/ModulesInfo.java @@ -81,6 +81,7 @@ public class ModulesInfo { @Override public ModuleController createModule(AppController app) { + Log.v(TAG, "EnableCaptureModule = " + ENABLE_CAPTURE_MODULE); return ENABLE_CAPTURE_MODULE ? new CaptureModule(app) : new PhotoModule(app); } }); diff --git a/src/com/android/camera/one/OneCameraManager.java b/src/com/android/camera/one/OneCameraManager.java index 9a4fe0c65..d23e5a37f 100644 --- a/src/com/android/camera/one/OneCameraManager.java +++ b/src/com/android/camera/one/OneCameraManager.java @@ -16,18 +16,19 @@ package com.android.camera.one; -import com.google.common.base.Optional; - import android.os.Handler; import android.util.DisplayMetrics; import com.android.camera.CameraActivity; +import com.android.camera.async.MainThread; import com.android.camera.debug.Log.Tag; import com.android.camera.one.OneCamera.Facing; import com.android.camera.one.OneCamera.OpenCallback; -import com.android.camera.one.v2.imagesaver.ImageSaver; +import com.android.camera.one.v2.photo.ImageRotationCalculator; import com.android.camera.util.Size; +import com.google.common.base.Optional; + /** * The camera manager is responsible for instantiating {@link OneCamera} * instances. @@ -38,7 +39,7 @@ public abstract class OneCameraManager { /** * Attempts to open the camera facing the given direction with the given * capture size. - * + * <p> * Exactly one call will always be made to a single method in the provided * {@link OpenCallback}. * @@ -47,13 +48,16 @@ public abstract class OneCameraManager { * @param enableHdr if an HDR feature exists, open a camera that supports it * @param captureSize the capture size. This must be one of the supported * sizes. - * @param imageSaverBuilder the builder that will be used to create {@link ImageSaver}. * @param callback this listener is called when the camera was opened or * when it failed to open. * @param handler the handler on which callback methods are invoked. + * @param mainThread Main thread executor + * @param imageRotationCalculator Image rotation calculator required for + * Camera Factory initialization */ public abstract void open(Facing facing, boolean enableHdr, Size captureSize, - ImageSaver.Builder imageSaverBuilder, OpenCallback callback, Handler handler); + OpenCallback callback, Handler handler, + MainThread mainThread, final ImageRotationCalculator imageRotationCalculator); // TODO: Move this to OneCameraCharacteristics class. /** @@ -62,13 +66,13 @@ public abstract class OneCameraManager { public abstract boolean hasCameraFacing(Facing facing); /** - * Retrieve the characteristics for the camera facing at the given direction. The first camera - * found in the given direction will be chosen. + * Retrieve the characteristics for the camera facing at the given + * direction. The first camera found in the given direction will be chosen. * * @param facing The facing direction of the camera. - * @return A #{link com.android.camera.one.OneCameraCharacteristics} object to provide camera - * characteristics information. Returns null if there is no camera facing the given - * direction. + * @return A #{link com.android.camera.one.OneCameraCharacteristics} object + * to provide camera characteristics information. Returns null if + * there is no camera facing the given direction. */ public abstract OneCameraCharacteristics getCameraCharacteristics(Facing facing) throws OneCameraAccessException; diff --git a/src/com/android/camera/one/v1/OneCameraManagerImpl.java b/src/com/android/camera/one/v1/OneCameraManagerImpl.java index 87b541905..4fa6be7e4 100644 --- a/src/com/android/camera/one/v1/OneCameraManagerImpl.java +++ b/src/com/android/camera/one/v1/OneCameraManagerImpl.java @@ -22,6 +22,7 @@ import android.hardware.Camera; import android.os.Handler; import com.android.camera.CameraActivity; +import com.android.camera.async.MainThread; import com.android.camera.debug.Log; import com.android.camera.one.OneCamera.Facing; import com.android.camera.one.OneCamera.OpenCallback; @@ -29,6 +30,7 @@ import com.android.camera.one.OneCameraAccessException; import com.android.camera.one.OneCameraCharacteristics; import com.android.camera.one.OneCameraManager; import com.android.camera.one.v2.imagesaver.ImageSaver; +import com.android.camera.one.v2.photo.ImageRotationCalculator; import com.android.camera.util.Size; /** @@ -93,7 +95,9 @@ public class OneCameraManagerImpl extends OneCameraManager { @Override public void open(Facing facing, boolean enableHdr, Size pictureSize, - ImageSaver.Builder imageSaverBuilder, OpenCallback callback, Handler handler) { + OpenCallback callback, Handler handler, + MainThread mainThread, + ImageRotationCalculator imageRotationCalculator) { throw new RuntimeException("Not implemented yet."); } diff --git a/src/com/android/camera/one/v2/OneCameraManagerImpl.java b/src/com/android/camera/one/v2/OneCameraManagerImpl.java index 340a6dfb3..6b3754b2b 100644 --- a/src/com/android/camera/one/v2/OneCameraManagerImpl.java +++ b/src/com/android/camera/one/v2/OneCameraManagerImpl.java @@ -28,6 +28,7 @@ import android.util.DisplayMetrics; import com.android.camera.CameraActivity; import com.android.camera.SoundPlayer; import com.android.camera.app.AppController; +import com.android.camera.async.MainThread; import com.android.camera.debug.Log; import com.android.camera.debug.Log.Tag; import com.android.camera.one.OneCamera; @@ -36,10 +37,11 @@ import com.android.camera.one.OneCamera.OpenCallback; import com.android.camera.one.OneCameraAccessException; import com.android.camera.one.OneCameraCharacteristics; import com.android.camera.one.OneCameraManager; -import com.android.camera.one.v2.imagesaver.ImageSaver; +import com.android.camera.one.v2.photo.ImageRotationCalculator; import com.android.camera.util.AndroidServices; import com.android.camera.util.ApiHelper; import com.android.camera.util.Size; + import com.google.common.base.Optional; /** @@ -55,7 +57,8 @@ public class OneCameraManagerImpl extends OneCameraManager { private final DisplayMetrics mDisplayMetrics; private final SoundPlayer mSoundPlayer; - public static Optional<OneCameraManager> create(CameraActivity activity, DisplayMetrics displayMetrics) { + public static Optional<OneCameraManager> create(CameraActivity activity, + DisplayMetrics displayMetrics) { if (!ApiHelper.HAS_CAMERA_2_API) { return Optional.absent(); } @@ -92,14 +95,16 @@ public class OneCameraManagerImpl extends OneCameraManager { @Override public void open(Facing facing, final boolean useHdr, final Size pictureSize, - final ImageSaver.Builder imageSaverBuilder, final OpenCallback openCallback, Handler handler) { + final OpenCallback openCallback, + Handler handler, final MainThread mainThread, + final ImageRotationCalculator imageRotationCalculator) { try { final String cameraId = getCameraId(facing); Log.i(TAG, "Opening Camera ID " + cameraId); mCameraManager.openCamera(cameraId, new CameraDevice.StateCallback() { // We may get multiple calls to StateCallback, but only the // first callback indicates the status of the camera-opening - // operation. For example, we may receive onOpened() and later + // operation. For example, we may receive onOpened() and later // onClosed(), but only the first should be relayed to // openCallback. private boolean isFirstCallback = true; @@ -139,10 +144,12 @@ public class OneCameraManagerImpl extends OneCameraManager { try { CameraCharacteristics characteristics = mCameraManager .getCameraCharacteristics(device.getId()); - // TODO: Set boolean based on whether HDR+ is enabled. + // TODO: Set boolean based on whether HDR+ is + // enabled. OneCamera oneCamera = OneCameraCreator.create(mAppController, useHdr, - device, characteristics, pictureSize, imageSaverBuilder, - mMaxMemoryMB, mDisplayMetrics, mSoundPlayer); + device, characteristics, pictureSize, + mMaxMemoryMB, mDisplayMetrics, mSoundPlayer, + mainThread, imageRotationCalculator); openCallback.onCameraOpened(oneCamera); } catch (CameraAccessException e) { Log.d(TAG, "Could not get camera characteristics"); @@ -181,7 +188,8 @@ public class OneCameraManagerImpl extends OneCameraManager { throws OneCameraAccessException { String cameraId = getCameraId(facing); try { - CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId); + CameraCharacteristics characteristics = mCameraManager + .getCameraCharacteristics(cameraId); return new OneCameraCharacteristicsImpl(characteristics); } catch (CameraAccessException ex) { throw new OneCameraAccessException("Unable to get camera characteristics", ex); diff --git a/src/com/android/camera/one/v2/imagesaver/JpegImageBackendImageSaver.java b/src/com/android/camera/one/v2/imagesaver/JpegImageBackendImageSaver.java new file mode 100644 index 000000000..ad2b8ebef --- /dev/null +++ b/src/com/android/camera/one/v2/imagesaver/JpegImageBackendImageSaver.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2015 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.one.v2.imagesaver; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; + +import com.android.camera.app.OrientationManager; +import com.android.camera.async.MainThread; +import com.android.camera.debug.Log; +import com.android.camera.one.OneCamera; +import com.android.camera.one.v2.camera2proxy.ImageProxy; +import com.android.camera.one.v2.photo.ImageRotationCalculator; +import com.android.camera.processing.imagebackend.ImageBackend; +import com.android.camera.processing.imagebackend.ImageConsumer; +import com.android.camera.processing.imagebackend.ImageProcessorListener; +import com.android.camera.processing.imagebackend.ImageProcessorProxyListener; +import com.android.camera.processing.imagebackend.ImageToProcess; +import com.android.camera.processing.imagebackend.TaskImageContainer; +import com.android.camera.session.CaptureSession; + +import com.google.common.base.Optional; + +import java.util.HashSet; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +/** + * Wires up the ImageBackend task submission process to save JPEG images. Camera + * delivers a JPEG-compressed full-size image. This class does very little work + * and just routes this image artifact as the thumbnail and to remote devices. + */ +public class JpegImageBackendImageSaver implements ImageSaver.Builder { + private static Log.Tag TAG = new Log.Tag("JpegImgBESaver"); + + @ParametersAreNonnullByDefault + private static class ImageSaverImpl implements SingleImageSaver { + private final MainThread mExecutor; + private final CaptureSession mSession; + private final OrientationManager.DeviceOrientation mImageRotation; + private final ImageBackend mImageBackend; + private final ImageProcessorListener mPreviewListener; + + public ImageSaverImpl(MainThread executor, + CaptureSession session, OrientationManager.DeviceOrientation imageRotation, + ImageBackend imageBackend, ImageProcessorListener previewListener) { + mExecutor = executor; + mSession = session; + mImageRotation = imageRotation; + mImageBackend = imageBackend; + mPreviewListener = previewListener; + } + + @Override + public void saveAndCloseImage(ImageProxy image, Optional<ImageProxy> thumbnail) { + // TODO: Use thumbnail to speed up RGB thumbnail creation whenever + // possible. For now, just close it. + if (thumbnail.isPresent()) { + thumbnail.get().close(); + } + final ImageProcessorProxyListener listenerProxy = mImageBackend.getProxyListener(); + + listenerProxy.registerListener(mPreviewListener, image); + + Set<ImageConsumer.ImageTaskFlags> taskFlagsSet = new HashSet<>(); + taskFlagsSet.add(ImageConsumer.ImageTaskFlags.COMPRESS_TO_JPEG_AND_WRITE_TO_DISK); + taskFlagsSet.add(ImageConsumer.ImageTaskFlags.CLOSE_ON_ALL_TASKS_RELEASE); + + try { + mImageBackend.receiveImage(new ImageToProcess(image, mImageRotation), + mExecutor, taskFlagsSet, mSession); + } catch (InterruptedException e) { + // Impossible exception because receiveImage is nonblocking + throw new RuntimeException(e); + } + } + } + + private static class PreviewListener implements ImageProcessorListener { + private final MainThread mExecutor; + private final ImageProcessorProxyListener mListenerProxy; + private final CaptureSession mSession; + private final OrientationManager.DeviceOrientation mImageRotation; + private final OneCamera.PictureSaverCallback mPictureSaverCallback; + + private PreviewListener(MainThread executor, + ImageProcessorProxyListener listenerProxy, CaptureSession session, + OrientationManager.DeviceOrientation imageRotation, + OneCamera.PictureSaverCallback pictureSaverCallback) { + mExecutor = executor; + mListenerProxy = listenerProxy; + mSession = session; + mImageRotation = imageRotation; + mPictureSaverCallback = pictureSaverCallback; + } + + @Override + public void onStart(TaskImageContainer.TaskInfo task) { + if (task.destination == TaskImageContainer.TaskInfo.Destination.FINAL_IMAGE) { + mSession.startEmpty(); + } + } + + @Override + public void onResultCompressed(TaskImageContainer.TaskInfo task, + TaskImageContainer.CompressedPayload payload) { + if (task.destination == TaskImageContainer.TaskInfo.Destination.FINAL_IMAGE) { + // Just start the thumbnail now, since there's no earlier event. + mPictureSaverCallback.onThumbnailProcessingBegun(); + final Bitmap bitmap = BitmapFactory.decodeByteArray(payload.data, 0, + payload.data.length); + // Send image to disk saver. + mPictureSaverCallback.onThumbnailAvailable(bitmap, mImageRotation.getDegrees()); + // Send image to remote devices + mPictureSaverCallback.onRemoteThumbnailAvailable(payload.data); + } + + } + + @Override + public void onResultUncompressed(TaskImageContainer.TaskInfo task, + TaskImageContainer.UncompressedPayload payload) { + // Do Nothing + } + + @Override + public void onResultUri(TaskImageContainer.TaskInfo task, Uri uri) { + // Remove yourself from the listener after JPEG save. + // TODO: This should really be done by the ImageBackend to guarantee + // ordering, since technically this could happen out of order. + mListenerProxy.unregisterListener(this); + } + } + + private final MainThread mExecutor; + private final ImageRotationCalculator mImageRotationCalculator; + private final ImageBackend mImageBackend; + + /** + * Constructor + * + * @param executor Executor to run listener events on the ImageBackend + * @param imageRotationCalculator the image rotation calculator to determine + */ + public JpegImageBackendImageSaver(MainThread executor, + ImageRotationCalculator imageRotationCalculator, + ImageBackend imageBackend) { + mExecutor = executor; + mImageRotationCalculator = imageRotationCalculator; + mImageBackend = imageBackend; + } + + /** + * Builder for the Zsl/ImageBackend Interface + * + * @return Instantiated interface object + */ + @Override + public ImageSaver build( + @Nonnull OneCamera.PictureSaverCallback pictureSaverCallback, + @Nonnull OrientationManager.DeviceOrientation orientation, + @Nonnull CaptureSession session) { + final OrientationManager.DeviceOrientation imageRotation = mImageRotationCalculator + .toImageRotation(orientation); + + ImageProcessorProxyListener proxyListener = mImageBackend.getProxyListener(); + + PreviewListener previewListener = new PreviewListener(mExecutor, + proxyListener, session, imageRotation, pictureSaverCallback); + return new MostRecentImageSaver(new ImageSaverImpl(mExecutor, session, + imageRotation, mImageBackend, previewListener)); + } +} diff --git a/src/com/android/camera/one/v2/imagesaver/YuvImageBackendImageSaver.java b/src/com/android/camera/one/v2/imagesaver/YuvImageBackendImageSaver.java index 1f1dd95b3..e7a10fb3b 100644 --- a/src/com/android/camera/one/v2/imagesaver/YuvImageBackendImageSaver.java +++ b/src/com/android/camera/one/v2/imagesaver/YuvImageBackendImageSaver.java @@ -74,9 +74,10 @@ public class YuvImageBackendImageSaver implements ImageSaver.Builder { listenerProxy.registerListener(mPreviewListener, image); Set<ImageConsumer.ImageTaskFlags> taskFlagsSet = new HashSet<>(); - taskFlagsSet.add(ImageConsumer.ImageTaskFlags.CONVERT_IMAGE_TO_RGB_PREVIEW); - taskFlagsSet.add(ImageConsumer.ImageTaskFlags.COMPRESS_IMAGE_TO_JPEG); - taskFlagsSet.add(ImageConsumer.ImageTaskFlags.CLOSE_IMAGE_ON_RELEASE); + taskFlagsSet.add(ImageConsumer.ImageTaskFlags.CREATE_EARLY_FILMSTRIP_PREVIEW); + taskFlagsSet.add(ImageConsumer.ImageTaskFlags.CONVERT_TO_RGB_PREVIEW); + taskFlagsSet.add(ImageConsumer.ImageTaskFlags.COMPRESS_TO_JPEG_AND_WRITE_TO_DISK); + taskFlagsSet.add(ImageConsumer.ImageTaskFlags.CLOSE_ON_ALL_TASKS_RELEASE); try { mImageBackend.receiveImage(new ImageToProcess(image, mImageRotation), diff --git a/src/com/android/camera/processing/imagebackend/ImageBackend.java b/src/com/android/camera/processing/imagebackend/ImageBackend.java index 3630e90e9..7ba442cec 100644 --- a/src/com/android/camera/processing/imagebackend/ImageBackend.java +++ b/src/com/android/camera/processing/imagebackend/ImageBackend.java @@ -16,13 +16,10 @@ package com.android.camera.processing.imagebackend; -import com.android.camera.app.CameraAppUI; import com.android.camera.debug.Log; -import com.android.camera.one.v2.camera2proxy.ImageProxy; import com.android.camera.session.CaptureSession; import com.android.camera.util.Size; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -92,7 +89,7 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager { * Approximate viewable size (in pixels) for the fast thumbnail in the * current UX definition of the product. Note that these values will be the * minimum size of FAST_THUMBNAIL target for the - * CONVERT_IMAGE_TO_RGB_PREVIEW task. + * CONVERT_TO_RGB_PREVIEW task. */ private final static Size FAST_THUMBNAIL_TARGET_SIZE = new Size(160, 100); @@ -100,7 +97,7 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager { * A standard viewable size (in pixels) for the filmstrip thumbnail in the * current UX definition of the product. Note that this size is the minimum * size for the Preview on the filmstrip associated with - * COMPRESS_IMAGE_TO_JPEG task. + * COMPRESS_TO_JPEG_AND_WRITE_TO_DISK task. */ private final static Size FILMSTRIP_THUMBNAIL_TARGET_SIZE = new Size(512, 384); @@ -369,31 +366,29 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager { // Now add the pre-mixed versions of the tasks. - if (processingFlags.contains(ImageTaskFlags.COMPRESS_IMAGE_TO_JPEG) - || processingFlags.contains(ImageTaskFlags.WRITE_IMAGE_TO_DISK)) { - // Add this type of task to the appropriate queue. - // tasksToExecute.add(new TaskCompressImageToJpeg(img, executor, this, session)); - tasksToExecute.add(new TaskPreviewChainedJpeg(img, executor, this, session, - FILMSTRIP_THUMBNAIL_TARGET_SIZE)); + if (processingFlags.contains(ImageTaskFlags.COMPRESS_TO_JPEG_AND_WRITE_TO_DISK)) { + if (processingFlags.contains(ImageTaskFlags.CREATE_EARLY_FILMSTRIP_PREVIEW)) { + // Request job that creates both filmstrip thumbnail from YUV, + // JPEG compression of the YUV Image, and writes the result to disk + tasksToExecute.add(new TaskPreviewChainedJpeg(img, executor, this, session, + FILMSTRIP_THUMBNAIL_TARGET_SIZE)); + } else { + // Request job that only does JPEG compression and writes the result to disk + tasksToExecute.add(new TaskCompressImageToJpeg(img, executor, this, session)); + } } - if (processingFlags.contains(ImageTaskFlags.CONVERT_IMAGE_TO_RGB_PREVIEW)) { - // Add this type of task to the appropriate queue. + if (processingFlags.contains(ImageTaskFlags.CONVERT_TO_RGB_PREVIEW)) { + // Add an additional type of task to the appropriate queue. tasksToExecute.add(new TaskConvertImageToRGBPreview(img, executor, this, TaskImageContainer.ProcessingPriority.FAST, session, FAST_THUMBNAIL_TARGET_SIZE, TaskConvertImageToRGBPreview.ThumbnailShape.SQUARE_ASPECT_CIRCULAR_INSET)); } - if (processingFlags.contains(ImageTaskFlags.WRITE_IMAGE_TO_DISK)) { - // Add this type of task to the appropriate queue. - // Has a dependency as well on the result JPEG_COMPRESSION - // TODO: Put disk writing implementation within the framework. - } - receiveImage(img, tasksToExecute, - processingFlags.contains(ImageTaskFlags.BLOCK_UNTIL_IMAGE_RELEASE), - processingFlags.contains(ImageTaskFlags.CLOSE_IMAGE_ON_RELEASE)); + processingFlags.contains(ImageTaskFlags.BLOCK_UNTIL_ALL_TASKS_RELEASE), + processingFlags.contains(ImageTaskFlags.CLOSE_ON_ALL_TASKS_RELEASE)); return true; } diff --git a/src/com/android/camera/processing/imagebackend/ImageConsumer.java b/src/com/android/camera/processing/imagebackend/ImageConsumer.java index 16e523536..43a5369e0 100644 --- a/src/com/android/camera/processing/imagebackend/ImageConsumer.java +++ b/src/com/android/camera/processing/imagebackend/ImageConsumer.java @@ -26,20 +26,45 @@ import java.util.concurrent.Executor; * object that merely delivers images to the backend. After the tasks and * Android image is submitted to the ImageConsumer, the responsibility to close * on the Android image object as early as possible is transferred to the - * implementation. Whether an image can be submitted again for process is up to - * the implementation of the consumer. For integration of Camera Application, - * we now pass in the CaptureSession in order to properly update filmstrip and UI. + * implementation. Whether an image can be submitted again for process is up to + * the implementation of the consumer. For integration of Camera Application, we + * now pass in the CaptureSession in order to properly update filmstrip and UI. */ public interface ImageConsumer { /** + * ImageTaskFlags specifies the current tasks that will be run with an + * image. + * <ol> + * <li>CREATE_EARLY_FILMSTRIP_PREVIEW: Subsamples a YUV Image and converts + * it to an ARGB Image with nearly similar aspect ratio. ONLY Valid when + * specified with COMPRESS_TO_JPEG_AND_WRITE_TO_DISK. Otherwise, ignored.</li> + * <li>COMPRESS_TO_JPEG_AND_WRITE_TO_DISK: Compresses an YUV/JPEG Image to + * JPEG (when necessary), delivers the compressed artifact via the listener, + * and writes it to disk.</li> + * <li>CONVERT_TO_RGB_PREVIEW: Subsamples a YUV Image and converts the + * uncompressed output to ARGB image inset within a circle</li> + * <li>BLOCK_UNTIL_ALL_TASKS_RELEASE: Block on ReceiveImage call until image + * is released.</li> + * <li>CLOSE_ON_ALL_TASKS_RELEASE: Close the ImageProxy on ReceiveImage Call + * when all tasks release their image</li> + * </ol> + */ + public enum ImageTaskFlags { + CREATE_EARLY_FILMSTRIP_PREVIEW, + COMPRESS_TO_JPEG_AND_WRITE_TO_DISK, + CONVERT_TO_RGB_PREVIEW, + BLOCK_UNTIL_ALL_TASKS_RELEASE, + CLOSE_ON_ALL_TASKS_RELEASE + } + + /** * Provides the basic functionality of camera processing via an easy-to-use * method call. * * @param img The Image to be Processed * @param executor The executor on which to execute events and image close - * @param processingFlags Bit vector comprised of logically ORed TASK_FLAG* - * constants + * @param processingFlags {@see ImageTaskFlags} */ public boolean receiveImage(ImageToProcess img, Executor executor, Set<ImageTaskFlags> processingFlags, CaptureSession captureSession) @@ -102,12 +127,4 @@ public interface ImageConsumer { */ public ImageProcessorProxyListener getProxyListener(); - // Current jobs that should be able to be tagged to an image. - public enum ImageTaskFlags { - COMPRESS_IMAGE_TO_JPEG, - CONVERT_IMAGE_TO_RGB_PREVIEW, - WRITE_IMAGE_TO_DISK, - BLOCK_UNTIL_IMAGE_RELEASE, - CLOSE_IMAGE_ON_RELEASE - } } diff --git a/src/com/android/camera/processing/imagebackend/TaskChainedCompressImageToJpeg.java b/src/com/android/camera/processing/imagebackend/TaskChainedCompressImageToJpeg.java index 6e4cf4fd0..61b2c878d 100644 --- a/src/com/android/camera/processing/imagebackend/TaskChainedCompressImageToJpeg.java +++ b/src/com/android/camera/processing/imagebackend/TaskChainedCompressImageToJpeg.java @@ -52,23 +52,26 @@ class TaskChainedCompressImageToJpeg extends TaskJpegEncode { img.proxy.getHeight(), img.proxy.getFormat()); final TaskImage resultImage = new TaskImage(mImage.rotation, img.proxy.getWidth(), img.proxy.getHeight(), ImageFormat.JPEG); - - onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE); - + byte[] dataCopy; int[] strides = new int[3]; - // Do the byte copy - strides[0] = planeList.get(0).getRowStride() - / planeList.get(0).getPixelStride(); - strides[1] = planeList.get(1).getRowStride() - / planeList.get(1).getPixelStride(); - strides[2] = 2 * planeList.get(2).getRowStride() - / planeList.get(2).getPixelStride(); - - // TODO: For performance, use a cache subsystem for buffer reuse. - byte[] dataCopy = convertYUV420ImageToPackedNV21(img.proxy); - - // Release the image now that you have a usable copy - mImageTaskManager.releaseSemaphoreReference(img, mExecutor); + + try { + onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE); + + // Do the byte copy + strides[0] = planeList.get(0).getRowStride() + / planeList.get(0).getPixelStride(); + strides[1] = planeList.get(1).getRowStride() + / planeList.get(1).getPixelStride(); + strides[2] = 2 * planeList.get(2).getRowStride() + / planeList.get(2).getPixelStride(); + + // TODO: For performance, use a cache subsystem for buffer reuse. + dataCopy = convertYUV420ImageToPackedNV21(img.proxy); + } finally { + // Release the image now that you have a usable copy + mImageTaskManager.releaseSemaphoreReference(img, mExecutor); + } final byte[] chainedDataCopy = dataCopy; final int[] chainedStrides = strides; diff --git a/src/com/android/camera/processing/imagebackend/TaskCompressImageToJpeg.java b/src/com/android/camera/processing/imagebackend/TaskCompressImageToJpeg.java index ea69440cf..a13e596af 100644 --- a/src/com/android/camera/processing/imagebackend/TaskCompressImageToJpeg.java +++ b/src/com/android/camera/processing/imagebackend/TaskCompressImageToJpeg.java @@ -22,6 +22,7 @@ import android.net.Uri; import com.android.camera.app.MediaSaver; import com.android.camera.app.OrientationManager.DeviceOrientation; import com.android.camera.exif.ExifInterface; +import com.android.camera.one.v2.camera2proxy.ImageProxy; import com.android.camera.session.CaptureSession; import com.android.camera.util.JpegUtilNative; import com.android.camera.util.Size; @@ -31,12 +32,12 @@ import java.util.concurrent.Executor; /** * Implements the conversion of a YUV_420_888 image to compressed JPEG byte - * array, using the native implementation of the Camera Application. - * <p> - * TODO: Instead of setting the orientation in EXIF, actually rotate the image - * here and set a rotation of 0. + * array, using the native implementation of the Camera Application. If the + * image is already JPEG, then it passes it through properly with the assumption + * that the JPEG is already encoded in the proper orientation. */ public class TaskCompressImageToJpeg extends TaskJpegEncode { + private static final int DEFAULT_JPEG_COMPRESSION_QUALITY = 90; /** @@ -48,56 +49,100 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode { * @param captureSession Handler for UI/Disk events */ TaskCompressImageToJpeg(ImageToProcess image, Executor executor, - ImageTaskManager imageTaskManager, CaptureSession captureSession) { + ImageTaskManager imageTaskManager, + CaptureSession captureSession) { super(image, executor, imageTaskManager, ProcessingPriority.SLOW, captureSession); } - private void logWrapper(String message) { - // Do nothing. + /** + * Wraps the static call to JpegUtilNative for testability. {@see + * JpegUtilNative#compressJpegFromYUV420Image} + */ + public int compressJpegFromYUV420Image(ImageProxy img, ByteBuffer outBuf, int quality, + int degrees) { + return JpegUtilNative.compressJpegFromYUV420Image(img, outBuf, quality, degrees); } @Override public void run() { ImageToProcess img = mImage; + if (img.rotation != DeviceOrientation.CLOCKWISE_0 + && img.proxy.getFormat() == ImageFormat.JPEG) { + // TODO: Ensure the capture for SimpleOneCameraFactory is always + // at zero rotation + // To avoid suboptimal rotation implementation, the JPEG should + // come with zero orientation. Any post-rotation of JPEG would be + // REALLY sub-optimal. + + // Release image references on error + mImageTaskManager.releaseSemaphoreReference(img, mExecutor); + + throw new IllegalStateException( + "Image Rotation for JPEG should be zero, but is actually " + + img.rotation.getDegrees()); + } final TaskImage inputImage = new TaskImage( - img.rotation, img.proxy.getWidth(), img.proxy.getHeight(), img.proxy.getFormat()); - - // Resulting image will be rotated so that viewers won't have to rotate. - // That's why the resulting image will have 0 rotation. - Size resultSize = getImageSizeForOrientation(img.proxy.getWidth(), img.proxy.getHeight(), + img.rotation, img.proxy.getWidth(), img.proxy.getHeight(), + img.proxy.getFormat()); + Size resultSize = getImageSizeForOrientation(img.proxy.getWidth(), + img.proxy.getHeight(), img.rotation); final TaskImage resultImage = new TaskImage( DeviceOrientation.CLOCKWISE_0, resultSize.getWidth(), resultSize.getHeight(), ImageFormat.JPEG); - - onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE); - logWrapper("TIMER_END Full-size YUV buffer available, w=" + img.proxy.getWidth() + " h=" - + img.proxy.getHeight() + " of format " + img.proxy.getFormat() - + " (35==YUV_420_888)"); - - ByteBuffer compressedData = ByteBuffer.allocateDirect(3 * resultImage.width - * resultImage.height); - - // If Orientation is UNKNOWN, treat input image orientation as - // CLOCKWISE_0. - int numBytes = JpegUtilNative.compressJpegFromYUV420Image( - img.proxy, compressedData, DEFAULT_JPEG_COMPRESSION_QUALITY, - (inputImage.orientation == DeviceOrientation.UNKNOWN) ? 0 : inputImage.orientation - .getDegrees()); - - if (numBytes < 0) { - throw new RuntimeException("Error compressing jpeg."); + byte[] writeOut; + int numBytes; + + try { + // Resulting image will be rotated so that viewers won't have to + // rotate. That's why the resulting image will have 0 rotation. + + onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE); + logWrapper("TIMER_END Full-size YUV buffer available, w=" + img.proxy.getWidth() + + " h=" + + img.proxy.getHeight() + " of format " + img.proxy.getFormat() + + " (35==YUV_420_888)"); + + ByteBuffer compressedData; + switch (inputImage.format) { + case ImageFormat.JPEG: + compressedData = img.proxy.getPlanes().get(0).getBuffer(); + numBytes = compressedData.capacity(); + break; + case ImageFormat.YUV_420_888: + compressedData = ByteBuffer.allocateDirect(3 * resultImage.width + * resultImage.height); + + // If Orientation is UNKNOWN, treat input image orientation + // as CLOCKWISE_0. + numBytes = compressJpegFromYUV420Image( + img.proxy, compressedData, DEFAULT_JPEG_COMPRESSION_QUALITY, + (inputImage.orientation == DeviceOrientation.UNKNOWN) ? 0 + : inputImage.orientation + .getDegrees()); + + if (numBytes < 0) { + throw new RuntimeException("Error compressing jpeg."); + } + compressedData.limit(numBytes); + break; + default: + throw new IllegalArgumentException( + "Unsupported input image format for TaskCompressImageToJpeg"); + } + + writeOut = new byte[numBytes]; + compressedData.get(writeOut); + compressedData.rewind(); + } finally { + // Release the image now that you have a usable copy in local memory + // Or you failed to process + mImageTaskManager.releaseSemaphoreReference(img, mExecutor); } - compressedData.limit(numBytes); - byte[] writeOut = new byte[numBytes]; - compressedData.get(writeOut); - compressedData.rewind(); - - // Release the image now that you have a usable copy - mImageTaskManager.releaseSemaphoreReference(img, mExecutor); - onJpegEncodeDone(mId, inputImage, resultImage, writeOut, TaskInfo.Destination.FINAL_IMAGE); + onJpegEncodeDone(mId, inputImage, resultImage, writeOut, + TaskInfo.Destination.FINAL_IMAGE); // TODO: the app actually crashes here on a race condition: // TaskCompressImageToJpeg might complete before @@ -113,7 +158,23 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode { }); } - private static ExifInterface createExif(TaskImage image) { + /** + * Wraps a possible log message to be overridden for testability purposes. + * + * @param message + */ + protected void logWrapper(String message) { + // Do nothing. + } + + /** + * Wraps EXIF Interface for JPEG Metadata creation. Can be overridden for + * testing + * + * @param image Metadata for a jpeg image to create EXIF Interface + * @return the created Exif Interface + */ + protected ExifInterface createExif(TaskImage image) { ExifInterface exif = new ExifInterface(); exif.setTag(exif.buildTag(ExifInterface.TAG_PIXEL_X_DIMENSION, image.width)); exif.setTag(exif.buildTag(ExifInterface.TAG_PIXEL_Y_DIMENSION, image.height)); diff --git a/src/com/android/camera/processing/imagebackend/TaskConvertImageToRGBPreview.java b/src/com/android/camera/processing/imagebackend/TaskConvertImageToRGBPreview.java index b355fad70..e4d50a0af 100644 --- a/src/com/android/camera/processing/imagebackend/TaskConvertImageToRGBPreview.java +++ b/src/com/android/camera/processing/imagebackend/TaskConvertImageToRGBPreview.java @@ -740,16 +740,20 @@ public class TaskConvertImageToRGBPreview extends TaskImageContainer { new Size(inputImage.width, inputImage.height), mTargetSize); final TaskImage resultImage = calculateResultImage(img, subsample); + final int[] convertedImage; - onStart(mId, inputImage, resultImage, TaskInfo.Destination.FAST_THUMBNAIL); + try { + onStart(mId, inputImage, resultImage, TaskInfo.Destination.FAST_THUMBNAIL); - logWrapper("TIMER_END Rendering preview YUV buffer available, w=" + img.proxy.getWidth() - / subsample + " h=" + img.proxy.getHeight() / subsample + " of subsample " - + subsample); + logWrapper("TIMER_END Rendering preview YUV buffer available, w=" + img.proxy.getWidth() + / subsample + " h=" + img.proxy.getHeight() / subsample + " of subsample " + + subsample); - final int[] convertedImage = runSelectedConversion(img.proxy, subsample); - // Signal backend that reference has been released - mImageTaskManager.releaseSemaphoreReference(img, mExecutor); + convertedImage = runSelectedConversion(img.proxy, subsample); + } finally { + // Signal backend that reference has been released + mImageTaskManager.releaseSemaphoreReference(img, mExecutor); + } onPreviewDone(resultImage, inputImage, convertedImage, TaskInfo.Destination.FAST_THUMBNAIL); } diff --git a/src/com/android/camera/processing/imagebackend/TaskPreviewChainedJpeg.java b/src/com/android/camera/processing/imagebackend/TaskPreviewChainedJpeg.java index 25a1f27a7..7d41d243c 100644 --- a/src/com/android/camera/processing/imagebackend/TaskPreviewChainedJpeg.java +++ b/src/com/android/camera/processing/imagebackend/TaskPreviewChainedJpeg.java @@ -27,8 +27,6 @@ import java.util.concurrent.Executor; * inscribed in a circle. */ public class TaskPreviewChainedJpeg extends TaskConvertImageToRGBPreview { - protected final static Log.Tag TAG = new Log.Tag("TaskPreviewChainedJpeg"); - /** * Constructor * @@ -46,7 +44,8 @@ public class TaskPreviewChainedJpeg extends TaskConvertImageToRGBPreview { } public void logWrapper(String message) { - Log.v(TAG, message); + // final Log.Tag TAG = new Log.Tag("TaskPreviewChainedJpeg"); + // Log.v(TAG, message); } @Override @@ -58,22 +57,26 @@ public class TaskPreviewChainedJpeg extends TaskConvertImageToRGBPreview { new Size(inputImage.width, inputImage.height), mTargetSize); final TaskImage resultImage = calculateResultImage(img, subsample); + final int[] convertedImage; - onStart(mId, inputImage, resultImage, TaskInfo.Destination.INTERMEDIATE_THUMBNAIL); + try { + onStart(mId, inputImage, resultImage, TaskInfo.Destination.INTERMEDIATE_THUMBNAIL); - logWrapper("TIMER_END Rendering preview YUV buffer available, w=" + img.proxy.getWidth() - / subsample + " h=" + img.proxy.getHeight() / subsample + " of subsample " - + subsample); + logWrapper("TIMER_END Rendering preview YUV buffer available, w=" + img.proxy.getWidth() + / subsample + " h=" + img.proxy.getHeight() / subsample + " of subsample " + + subsample); - final int[] convertedImage = runSelectedConversion(img.proxy,subsample); + convertedImage = runSelectedConversion(img.proxy, subsample); - // Chain JPEG task - TaskImageContainer jpegTask = new TaskCompressImageToJpeg(img, mExecutor, - mImageTaskManager, mSession); - mImageTaskManager.appendTasks(img, jpegTask); + // Chain JPEG task + TaskImageContainer jpegTask = new TaskCompressImageToJpeg(img, mExecutor, + mImageTaskManager, mSession); + mImageTaskManager.appendTasks(img, jpegTask); + } finally { + // Signal backend that reference has been released + mImageTaskManager.releaseSemaphoreReference(img, mExecutor); + } - // Signal backend that reference has been released - mImageTaskManager.releaseSemaphoreReference(img, mExecutor); onPreviewDone(resultImage, inputImage, convertedImage, TaskInfo.Destination.INTERMEDIATE_THUMBNAIL); } diff --git a/src/com/android/camera/util/ApiHelper.java b/src/com/android/camera/util/ApiHelper.java index 67a67a8c7..37209a636 100644 --- a/src/com/android/camera/util/ApiHelper.java +++ b/src/com/android/camera/util/ApiHelper.java @@ -97,6 +97,6 @@ public class ApiHelper { public static boolean isLOrHigher() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - || "L".equals(Build.VERSION.CODENAME); + || "L".equals(Build.VERSION.CODENAME) || "LOLLIPOP".equals(Build.VERSION.CODENAME); } } diff --git a/src_pd/com/android/camera/one/v2/OneCameraCreator.java b/src_pd/com/android/camera/one/v2/OneCameraCreator.java index bf6b419dd..a1736ec31 100644 --- a/src_pd/com/android/camera/one/v2/OneCameraCreator.java +++ b/src_pd/com/android/camera/one/v2/OneCameraCreator.java @@ -20,17 +20,19 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.util.DisplayMetrics; -import com.android.camera.app.AppController; import com.android.camera.SoundPlayer; +import com.android.camera.app.AppController; +import com.android.camera.async.MainThread; import com.android.camera.one.OneCamera; -import com.android.camera.one.v2.imagesaver.ImageSaver; +import com.android.camera.one.v2.photo.ImageRotationCalculator; import com.android.camera.util.Size; public class OneCameraCreator { public static OneCamera create(AppController context, boolean useHdr, CameraDevice device, CameraCharacteristics characteristics, Size pictureSize, - ImageSaver.Builder imageSaverBuilder, int maxMemoryMB, - DisplayMetrics displayMetrics, SoundPlayer soundPlayer) { + int maxMemoryMB, + DisplayMetrics displayMetrics, SoundPlayer soundPlayer, + MainThread mainThread, ImageRotationCalculator imageRotationCalculator) { // TODO: Might want to switch current camera to vendor HDR. return new OneCameraImpl(device, characteristics, pictureSize); } |