summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorByunghun Jeon <bjeon@codeaurora.org>2015-08-03 17:42:49 -0700
committerGerrit - the friendly Code Review server <code-review@localhost>2015-08-12 18:03:03 -0700
commite713237ec94737904e339ce05a6d09fed5d9a72d (patch)
tree1ba6ce4732747a79e382125298a78ec40c376419 /src
parent4cc34823df8e420c366d4bf4497d7f906676a022 (diff)
downloadandroid_packages_apps_Snap-e713237ec94737904e339ce05a6d09fed5d9a72d.tar.gz
android_packages_apps_Snap-e713237ec94737904e339ce05a6d09fed5d9a72d.tar.bz2
android_packages_apps_Snap-e713237ec94737904e339ce05a6d09fed5d9a72d.zip
Revert "SnapdragonCamera: Improved panorama"
This reverts commit "SnapdragonCamera: Improved panorama" Change-Id: I7aef9427c218d06b78cbc097dd32a1629b5ab9d1
Diffstat (limited to 'src')
-rw-r--r--src/com/android/camera/Mosaic.java206
-rw-r--r--src/com/android/camera/MosaicFrameProcessor.java268
-rw-r--r--src/com/android/camera/MosaicPreviewRenderer.java207
-rw-r--r--src/com/android/camera/MosaicRenderer.java101
-rw-r--r--src/com/android/camera/WideAnglePanoramaModule.java786
-rw-r--r--src/com/android/camera/WideAnglePanoramaUI.java397
-rw-r--r--src/com/android/camera/ui/FilmStripView.java1
7 files changed, 1389 insertions, 577 deletions
diff --git a/src/com/android/camera/Mosaic.java b/src/com/android/camera/Mosaic.java
new file mode 100644
index 000000000..a9bc32e12
--- /dev/null
+++ b/src/com/android/camera/Mosaic.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+/**
+ * The Java interface to JNI calls regarding mosaic stitching.
+ *
+ * A high-level usage is:
+ *
+ * Mosaic mosaic = new Mosaic();
+ * mosaic.setSourceImageDimensions(width, height);
+ * mosaic.reset(blendType);
+ *
+ * while ((pixels = hasNextImage()) != null) {
+ * mosaic.setSourceImage(pixels);
+ * }
+ *
+ * mosaic.createMosaic(highRes);
+ * byte[] result = mosaic.getFinalMosaic();
+ *
+ */
+public class Mosaic {
+ /**
+ * In this mode, the images are stitched together in the same spatial arrangement as acquired
+ * i.e. if the user follows a curvy trajectory, the image boundary of the resulting mosaic will
+ * be curved in the same manner. This mode is useful if the user wants to capture a mosaic as
+ * if "painting" the scene using the smart-phone device and does not want any corrective warps
+ * to distort the captured images.
+ */
+ public static final int BLENDTYPE_FULL = 0;
+
+ /**
+ * This mode is the same as BLENDTYPE_FULL except that the resulting mosaic is rotated
+ * to balance the first and last images to be approximately at the same vertical offset in the
+ * output mosaic. This is useful when acquiring a mosaic by a typical panning-like motion to
+ * remove a one-sided curve in the mosaic (typically due to the camera not staying horizontal
+ * during the video capture) and convert it to a more symmetrical "smiley-face" like output.
+ */
+ public static final int BLENDTYPE_PAN = 1;
+
+ /**
+ * This mode compensates for typical "smiley-face" like output in longer mosaics and creates
+ * a rectangular mosaic with minimal black borders (by unwrapping the mosaic onto an imaginary
+ * cylinder). If the user follows a curved trajectory (instead of a perfect panning trajectory),
+ * the resulting mosaic here may suffer from some image distortions in trying to map the
+ * trajectory to a cylinder.
+ */
+ public static final int BLENDTYPE_CYLINDERPAN = 2;
+
+ /**
+ * This mode is basically BLENDTYPE_CYLINDERPAN plus doing a rectangle cropping before returning
+ * the mosaic. The mode is useful for making the resulting mosaic have a rectangle shape.
+ */
+ public static final int BLENDTYPE_HORIZONTAL =3;
+
+ /**
+ * This strip type will use the default thin strips where the strips are
+ * spaced according to the image capture rate.
+ */
+ public static final int STRIPTYPE_THIN = 0;
+
+ /**
+ * This strip type will use wider strips for blending. The strip separation
+ * is controlled by a threshold on the native side. Since the strips are
+ * wider, there is an additional cross-fade blending step to make the seam
+ * boundaries smoother. Since this mode uses lesser image frames, it is
+ * computationally more efficient than the thin strip mode.
+ */
+ public static final int STRIPTYPE_WIDE = 1;
+
+ /**
+ * Return flags returned by createMosaic() are one of the following.
+ */
+ public static final int MOSAIC_RET_OK = 1;
+ public static final int MOSAIC_RET_ERROR = -1;
+ public static final int MOSAIC_RET_CANCELLED = -2;
+ public static final int MOSAIC_RET_LOW_TEXTURE = -3;
+ public static final int MOSAIC_RET_FEW_INLIERS = 2;
+
+
+ static {
+ System.loadLibrary("jni_snapcammosaic");
+ }
+
+ /**
+ * Allocate memory for the image frames at the given resolution.
+ *
+ * @param width width of the input frames in pixels
+ * @param height height of the input frames in pixels
+ */
+ public native void allocateMosaicMemory(int width, int height);
+
+ /**
+ * Free memory allocated by allocateMosaicMemory.
+ *
+ */
+ public native void freeMosaicMemory();
+
+ /**
+ * Pass the input image frame to the native layer. Each time the a new
+ * source image t is set, the transformation matrix from the first source
+ * image to t is computed and returned.
+ *
+ * @param pixels source image of NV21 format.
+ * @return Float array of length 11; first 9 entries correspond to the 3x3
+ * transformation matrix between the first frame and the passed frame;
+ * the 10th entry is the number of the passed frame, where the counting
+ * starts from 1; and the 11th entry is the returning code, whose value
+ * is one of those MOSAIC_RET_* returning flags defined above.
+ */
+ public native float[] setSourceImage(byte[] pixels);
+
+ /**
+ * This is an alternative to the setSourceImage function above. This should
+ * be called when the image data is already on the native side in a fixed
+ * byte array. In implementation, this array is filled by the GL thread
+ * using glReadPixels directly from GPU memory (where it is accessed by
+ * an associated SurfaceTexture).
+ *
+ * @return Float array of length 11; first 9 entries correspond to the 3x3
+ * transformation matrix between the first frame and the passed frame;
+ * the 10th entry is the number of the passed frame, where the counting
+ * starts from 1; and the 11th entry is the returning code, whose value
+ * is one of those MOSAIC_RET_* returning flags defined above.
+ */
+ public native float[] setSourceImageFromGPU();
+
+ /**
+ * Set the type of blending.
+ *
+ * @param type the blending type defined in the class. {BLENDTYPE_FULL,
+ * BLENDTYPE_PAN, BLENDTYPE_CYLINDERPAN, BLENDTYPE_HORIZONTAL}
+ */
+ public native void setBlendingType(int type);
+
+ /**
+ * Set the type of strips to use for blending.
+ * @param type the blending strip type to use {STRIPTYPE_THIN,
+ * STRIPTYPE_WIDE}.
+ */
+ public native void setStripType(int type);
+
+ /**
+ * Tell the native layer to create the final mosaic after all the input frame
+ * data have been collected.
+ * The case of generating high-resolution mosaic may take dozens of seconds to finish.
+ *
+ * @param value True means generating a high-resolution mosaic -
+ * which is based on the original images set in setSourceImage().
+ * False means generating a low-resolution version -
+ * which is based on 1/4 downscaled images from the original images.
+ * @return Returns a status code suggesting if the mosaic building was
+ * successful, in error, or was cancelled by the user.
+ */
+ public native int createMosaic(boolean value);
+
+ /**
+ * Get the data for the created mosaic.
+ *
+ * @return Returns an integer array which contains the final mosaic in the ARGB_8888 format.
+ * The first MosaicWidth*MosaicHeight values contain the image data, followed by 2
+ * integers corresponding to the values MosaicWidth and MosaicHeight respectively.
+ */
+ public native int[] getFinalMosaic();
+
+ /**
+ * Get the data for the created mosaic.
+ *
+ * @return Returns a byte array which contains the final mosaic in the NV21 format.
+ * The first MosaicWidth*MosaicHeight*1.5 values contain the image data, followed by
+ * 8 bytes which pack the MosaicWidth and MosaicHeight integers into 4 bytes each
+ * respectively.
+ */
+ public native byte[] getFinalMosaicNV21();
+
+ /**
+ * Reset the state of the frame arrays which maintain the captured frame data.
+ * Also re-initializes the native mosaic object to make it ready for capturing a new mosaic.
+ */
+ public native void reset();
+
+ /**
+ * Get the progress status of the mosaic computation process.
+ * @param hires Boolean flag to select whether to report progress of the
+ * low-res or high-res mosaicer.
+ * @param cancelComputation Boolean flag to allow cancelling the
+ * mosaic computation when needed from the GUI end.
+ * @return Returns a number from 0-100 where 50 denotes that the mosaic
+ * computation is 50% done.
+ */
+ public native int reportProgress(boolean hires, boolean cancelComputation);
+}
diff --git a/src/com/android/camera/MosaicFrameProcessor.java b/src/com/android/camera/MosaicFrameProcessor.java
index de92f347f..cb305344d 100644
--- a/src/com/android/camera/MosaicFrameProcessor.java
+++ b/src/com/android/camera/MosaicFrameProcessor.java
@@ -16,48 +16,55 @@
package com.android.camera;
-import android.content.Context;
-import android.os.Handler;
-import android.os.Message;
import android.util.Log;
-import com.android.camera.WideAnglePanoramaModule.INotifier;
-
/**
* A singleton to handle the processing of each frame by {@link Mosaic}.
*/
public class MosaicFrameProcessor {
private static final String TAG = "MosaicFrameProcessor";
-
- public static final int MAX_HORIZONAL_ANGLE = 180;
- public static final int MAX_VERTICAL_ANGLE = 180;
- public static final int MSG_PANORAMA_TIP = 0x0001;
- public static final int MSG_CAPTURE_SUCCESS = 0x0002;
- public static final int MSG_CAPTURE_FAILED = 0x0003;
- public static final int MSG_UPDATE_UI = 0x0004;
- /** Panorama direction unknown. */
- public final static int DIRECTION_UNKNOW = 0x00000000;
- /** Panorama direction left to right. */
- public final static int DIRECTION_LEFTTORIGHT = 0x00000001;
- /** Panorama direction right to left. */
- public final static int DIRECTION_RIGHTTOLEFT = 0x00000002;
- /** Panorama direction up to down. */
- public final static int DIRECTION_UPTODOWN = 0x00000004;
- /** Panorama direction down to up. */
- public final static int DIRECTION_DOWMTOUP = 0x00000008;
+ private static final int NUM_FRAMES_IN_BUFFER = 2;
+ private static final int MAX_NUMBER_OF_FRAMES = 100;
+ private static final int MOSAIC_RET_CODE_INDEX = 10;
+ private static final int FRAME_COUNT_INDEX = 9;
+ private static final int X_COORD_INDEX = 2;
+ private static final int Y_COORD_INDEX = 5;
+ private static final int HR_TO_LR_DOWNSAMPLE_FACTOR = 4;
+ private static final int WINDOW_SIZE = 3;
+
+ private Mosaic mMosaicer;
+ private boolean mIsMosaicMemoryAllocated = false;
+ private float mTranslationLastX;
+ private float mTranslationLastY;
+
+ private int mFillIn = 0;
+ private int mTotalFrameCount = 0;
+ private int mLastProcessFrameIdx = -1;
+ private int mCurrProcessFrameIdx = -1;
+ private boolean mFirstRun;
+
+ // Panning rate is in unit of percentage of image content translation per
+ // frame. Use moving average to calculate the panning rate.
+ private float mPanningRateX;
+ private float mPanningRateY;
+
+ private float[] mDeltaX = new float[WINDOW_SIZE];
+ private float[] mDeltaY = new float[WINDOW_SIZE];
+ private int mOldestIdx = 0;
+ private float mTotalTranslationX = 0f;
+ private float mTotalTranslationY = 0f;
+
+ private ProgressListener mProgressListener;
private int mPreviewWidth;
private int mPreviewHeight;
+ private int mPreviewBufferSize;
- private Handler mThreadHandler = null;
- private long mHandler = 0;
- private boolean mIsActive = false;
-
- private INotifier mPanoNotifier;
private static MosaicFrameProcessor sMosaicFrameProcessor; // singleton
- static {
- System.loadLibrary("jni_snapcammosaic");
+ public interface ProgressListener {
+ public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
+ float progressX, float progressY);
}
public static MosaicFrameProcessor getInstance() {
@@ -67,87 +74,164 @@ public class MosaicFrameProcessor {
return sMosaicFrameProcessor;
}
- public synchronized boolean IsInited() {
- return (mHandler != 0 && mIsActive);
+ private MosaicFrameProcessor() {
+ mMosaicer = new Mosaic();
}
- public synchronized int Init(Context context, int maxFrameCount, int width, int height,
- INotifier notifier) {
- Log.v(TAG, "Init <----");
- Uninit();
- mPanoNotifier = notifier;
- mThreadHandler = new Handler(context.getMainLooper(), new Handler.Callback() {
-
- @Override
- public boolean handleMessage(Message msg) {
- if (mPanoNotifier != null) {
- mPanoNotifier.onNotify(msg.what, msg.obj);
- }
- return true;
- }
+ public void setProgressListener(ProgressListener listener) {
+ mProgressListener = listener;
+ }
- });
- mPreviewWidth = width;
- mPreviewHeight = height;
- mHandler = _InitMosaic(context, this, maxFrameCount, width, height);
- mIsActive = (mHandler != 0);
- Log.v(TAG, "Init mHandler: " + mHandler + " ---->");
- return mIsActive ? 0 : -1;
+ public int reportProgress(boolean hires, boolean cancel) {
+ return mMosaicer.reportProgress(hires, cancel);
}
- public synchronized void Uninit() {
- if (mHandler != 0) {
- Log.v(TAG, "Unint <----");
- mIsActive = false;
- mThreadHandler = null;
- _UninitMosaic(mHandler);
- mHandler = 0;
- Log.v(TAG, "Unint ---->");
- }
+ public void initialize(int previewWidth, int previewHeight, int bufSize) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ mPreviewBufferSize = bufSize;
+ setupMosaicer(mPreviewWidth, mPreviewHeight, mPreviewBufferSize);
+ setStripType(Mosaic.STRIPTYPE_WIDE);
+ // no need to call reset() here. reset() should be called by the client
+ // after this initialization before calling other methods of this object.
}
- public synchronized int Process(byte[] data, int width, int height) {
- int res = -1;
- if (IsInited() && data != null && mPreviewWidth == width && mPreviewHeight == height) {
- Log.v(TAG, "Process <----");
- res = _ProcessMosaic(mHandler, data, width, height);
- Log.v(TAG, "Process res " + res + " ---->");
+ public void clear() {
+ if (mIsMosaicMemoryAllocated) {
+ mMosaicer.freeMosaicMemory();
+ mIsMosaicMemoryAllocated = false;
}
- else {
- Log.v(TAG, "Process Error " + " mWidth " + mPreviewWidth + " mHeight " + mPreviewHeight
- + " width " + width + " height " + height + " data " + (data != null));
+ synchronized (this) {
+ notify();
}
- return res;
}
- public synchronized int StopProcessing() {
- int res = -1;
- if (IsInited()) {
- Log.v(TAG, "StopProcessing <----");
- mIsActive = false;
- res = _StopProcessMosaic(mHandler);
- Log.v(TAG, "StopProcessing res " + res + " ---->");
- }
- return res;
+ public boolean isMosaicMemoryAllocated() {
+ return mIsMosaicMemoryAllocated;
+ }
+
+ public void setStripType(int type) {
+ mMosaicer.setStripType(type);
+ }
+
+ private void setupMosaicer(int previewWidth, int previewHeight, int bufSize) {
+ Log.v(TAG, "setupMosaicer w, h=" + previewWidth + ',' + previewHeight + ',' + bufSize);
+
+ if (mIsMosaicMemoryAllocated) throw new RuntimeException("MosaicFrameProcessor in use!");
+ mIsMosaicMemoryAllocated = true;
+ mMosaicer.allocateMosaicMemory(previewWidth, previewHeight);
}
- public int onNotify(int key, Object obj) {
- if (mThreadHandler != null && mHandler != 0) {
- Message msg = new Message();
- msg.what = key;
- msg.obj = obj;
- mThreadHandler.sendMessage(msg);
+ public void reset() {
+ // reset() can be called even if MosaicFrameProcessor is not initialized.
+ // Only counters will be changed.
+ mFirstRun = true;
+ mTotalFrameCount = 0;
+ mFillIn = 0;
+ mTotalTranslationX = 0;
+ mTranslationLastX = 0;
+ mTotalTranslationY = 0;
+ mTranslationLastY = 0;
+ mPanningRateX = 0;
+ mPanningRateY = 0;
+ mLastProcessFrameIdx = -1;
+ mCurrProcessFrameIdx = -1;
+ for (int i = 0; i < WINDOW_SIZE; ++i) {
+ mDeltaX[i] = 0f;
+ mDeltaY[i] = 0f;
}
- return 0;
+ mMosaicer.reset();
+ }
+
+ public int createMosaic(boolean highRes) {
+ return mMosaicer.createMosaic(highRes);
}
- public native long _InitMosaic(Context context, Object thiz, int maxFrameCount, int width,
- int height);
+ public byte[] getFinalMosaicNV21() {
+ return mMosaicer.getFinalMosaicNV21();
+ }
- public native int _UninitMosaic(long handler);
+ // Processes the last filled image frame through the mosaicer and
+ // updates the UI to show progress.
+ // When done, processes and displays the final mosaic.
+ public void processFrame() {
+ if (!mIsMosaicMemoryAllocated) {
+ // clear() is called and buffers are cleared, stop computation.
+ // This can happen when the onPause() is called in the activity, but still some frames
+ // are not processed yet and thus the callback may be invoked.
+ return;
+ }
- public native int _StopProcessMosaic(long handler);
+ mCurrProcessFrameIdx = mFillIn;
+ mFillIn = ((mFillIn + 1) % NUM_FRAMES_IN_BUFFER);
+
+ // Check that we are trying to process a frame different from the
+ // last one processed (useful if this class was running asynchronously)
+ if (mCurrProcessFrameIdx != mLastProcessFrameIdx) {
+ mLastProcessFrameIdx = mCurrProcessFrameIdx;
+
+ // TODO: make the termination condition regarding reaching
+ // MAX_NUMBER_OF_FRAMES solely determined in the library.
+ if (mTotalFrameCount < MAX_NUMBER_OF_FRAMES) {
+ // If we are still collecting new frames for the current mosaic,
+ // process the new frame.
+ calculateTranslationRate();
+
+ // Publish progress of the ongoing processing
+ if (mProgressListener != null) {
+ mProgressListener.onProgress(false, mPanningRateX, mPanningRateY,
+ mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
+ mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
+ }
+ } else {
+ if (mProgressListener != null) {
+ mProgressListener.onProgress(true, mPanningRateX, mPanningRateY,
+ mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
+ mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
+ }
+ }
+ }
+ }
- public native int _ProcessMosaic(long handler, byte[] data, int width, int height);
+ public void calculateTranslationRate() {
+ float[] frameData = mMosaicer.setSourceImageFromGPU();
+ int ret_code = (int) frameData[MOSAIC_RET_CODE_INDEX];
+ mTotalFrameCount = (int) frameData[FRAME_COUNT_INDEX];
+ float translationCurrX = frameData[X_COORD_INDEX];
+ float translationCurrY = frameData[Y_COORD_INDEX];
+
+ if (mFirstRun) {
+ // First time: no need to update delta values.
+ mTranslationLastX = translationCurrX;
+ mTranslationLastY = translationCurrY;
+ mFirstRun = false;
+ return;
+ }
+ // Moving average: remove the oldest translation/deltaTime and
+ // add the newest translation/deltaTime in
+ int idx = mOldestIdx;
+ mTotalTranslationX -= mDeltaX[idx];
+ mTotalTranslationY -= mDeltaY[idx];
+ mDeltaX[idx] = Math.abs(translationCurrX - mTranslationLastX);
+ mDeltaY[idx] = Math.abs(translationCurrY - mTranslationLastY);
+ mTotalTranslationX += mDeltaX[idx];
+ mTotalTranslationY += mDeltaY[idx];
+
+ // The panning rate is measured as the rate of the translation percentage in
+ // image width/height. Take the horizontal panning rate for example, the image width
+ // used in finding the translation is (PreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR).
+ // To get the horizontal translation percentage, the horizontal translation,
+ // (translationCurrX - mTranslationLastX), is divided by the
+ // image width. We then get the rate by dividing the translation percentage with the
+ // number of frames.
+ mPanningRateX = mTotalTranslationX /
+ (mPreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
+ mPanningRateY = mTotalTranslationY /
+ (mPreviewHeight / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
+
+ mTranslationLastX = translationCurrX;
+ mTranslationLastY = translationCurrY;
+ mOldestIdx = (mOldestIdx + 1) % WINDOW_SIZE;
+ }
}
diff --git a/src/com/android/camera/MosaicPreviewRenderer.java b/src/com/android/camera/MosaicPreviewRenderer.java
new file mode 100644
index 000000000..ba35b6f5a
--- /dev/null
+++ b/src/com/android/camera/MosaicPreviewRenderer.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.graphics.SurfaceTexture;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import javax.microedition.khronos.opengles.GL10;
+
+public class MosaicPreviewRenderer {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "CAM_MosaicPreviewRenderer";
+
+ private int mWidth; // width of the view in UI
+ private int mHeight; // height of the view in UI
+
+ private boolean mIsLandscape = true;
+ private int mOrientation = 0;
+ private final float[] mTransformMatrix = new float[16];
+
+ private ConditionVariable mEglThreadBlockVar = new ConditionVariable();
+ private HandlerThread mEglThread;
+ private MyHandler mHandler;
+ private SurfaceTextureRenderer mSTRenderer;
+
+ private SurfaceTexture mInputSurfaceTexture;
+
+ private boolean mEnableWarpedPanoPreview = false;
+
+ private class MyHandler extends Handler {
+ public static final int MSG_INIT_SYNC = 0;
+ public static final int MSG_SHOW_PREVIEW_FRAME_SYNC = 1;
+ public static final int MSG_SHOW_PREVIEW_FRAME = 2;
+ public static final int MSG_ALIGN_FRAME_SYNC = 3;
+ public static final int MSG_RELEASE = 4;
+ public static final int MSG_DO_PREVIEW_RESET = 5;
+ public MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INIT_SYNC:
+ doInit();
+ mEglThreadBlockVar.open();
+ break;
+ case MSG_SHOW_PREVIEW_FRAME_SYNC:
+ doShowPreviewFrame();
+ mEglThreadBlockVar.open();
+ break;
+ case MSG_DO_PREVIEW_RESET:
+ doPreviewReset();
+ break;
+ case MSG_SHOW_PREVIEW_FRAME:
+ doShowPreviewFrame();
+ break;
+ case MSG_ALIGN_FRAME_SYNC:
+ doAlignFrame();
+ mEglThreadBlockVar.open();
+ break;
+ case MSG_RELEASE:
+ doRelease();
+ mEglThreadBlockVar.open();
+ break;
+ }
+ }
+
+ private void doAlignFrame() {
+ mInputSurfaceTexture.updateTexImage();
+ mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
+
+ // Call setPreviewBackground to render high-res RGB textures to full screen.
+ MosaicRenderer.setPreviewBackground(true);
+ MosaicRenderer.preprocess(mTransformMatrix);
+ MosaicRenderer.step();
+ MosaicRenderer.setPreviewBackground(false);
+
+ MosaicRenderer.setWarping(true);
+ MosaicRenderer.transferGPUtoCPU();
+ MosaicRenderer.updateMatrix();
+ MosaicRenderer.step();
+ }
+
+ private void doShowPreviewFrame() {
+ mInputSurfaceTexture.updateTexImage();
+ mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
+
+ MosaicRenderer.setWarping(false);
+ // Call preprocess to render it to low-res and high-res RGB textures.
+ MosaicRenderer.preprocess(mTransformMatrix);
+ MosaicRenderer.updateMatrix();
+ MosaicRenderer.step();
+ }
+
+ private void doInit() {
+ mInputSurfaceTexture = new SurfaceTexture(MosaicRenderer.init(mEnableWarpedPanoPreview));
+ MosaicRenderer.reset(mWidth, mHeight, mIsLandscape, mOrientation);
+ }
+
+ private void doPreviewReset() {
+ MosaicRenderer.reset(mWidth, mHeight, mIsLandscape, mOrientation);
+ }
+
+ private void doRelease() {
+ releaseSurfaceTexture(mInputSurfaceTexture);
+ mEglThread.quit();
+ }
+
+ private void releaseSurfaceTexture(SurfaceTexture st) {
+ st.release();
+ }
+
+ // Should be called from other thread.
+ public void sendMessageSync(int msg) {
+ mEglThreadBlockVar.close();
+ sendEmptyMessage(msg);
+ mEglThreadBlockVar.block();
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param tex The {@link SurfaceTexture} for the final UI output.
+ * @param w The width of the UI view.
+ * @param h The height of the UI view.
+ * @param isLandscape The UI orientation. {@code true} if in landscape,
+ * false if in portrait.
+ */
+ public MosaicPreviewRenderer(SurfaceTexture tex, int w, int h, boolean isLandscape,
+ int orientation, boolean enableWarpedPanoPreview) {
+ mIsLandscape = isLandscape;
+ mOrientation = orientation;
+ mEnableWarpedPanoPreview = enableWarpedPanoPreview;
+ mEglThread = new HandlerThread("PanoramaRealtimeRenderer");
+ mEglThread.start();
+ mHandler = new MyHandler(mEglThread.getLooper());
+ mWidth = w;
+ mHeight = h;
+
+ SurfaceTextureRenderer.FrameDrawer dummy = new SurfaceTextureRenderer.FrameDrawer() {
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ // nothing, we have our draw functions.
+ }
+ };
+ mSTRenderer = new SurfaceTextureRenderer(tex, mHandler, dummy);
+
+ // We need to sync this because the generation of surface texture for input is
+ // done here and the client will continue with the assumption that the
+ // generation is completed.
+ mHandler.sendMessageSync(MyHandler.MSG_INIT_SYNC);
+ }
+
+ public void previewReset(int w, int h, boolean isLandscape, int orientation) {
+ mWidth = w;
+ mHeight = h;
+ mIsLandscape = isLandscape;
+ mOrientation = orientation;
+ mHandler.sendEmptyMessage(MyHandler.MSG_DO_PREVIEW_RESET);
+ mSTRenderer.draw(false);
+ }
+
+ public void release() {
+ mSTRenderer.release();
+ mHandler.sendMessageSync(MyHandler.MSG_RELEASE);
+ }
+
+ public void showPreviewFrameSync() {
+ mHandler.sendMessageSync(MyHandler.MSG_SHOW_PREVIEW_FRAME_SYNC);
+ mSTRenderer.draw(true);
+ }
+
+ public void showPreviewFrame() {
+ mHandler.sendEmptyMessage(MyHandler.MSG_SHOW_PREVIEW_FRAME);
+ mSTRenderer.draw(false);
+ }
+
+ public void alignFrameSync() {
+ mHandler.sendMessageSync(MyHandler.MSG_ALIGN_FRAME_SYNC);
+ mSTRenderer.draw(true);
+ }
+
+ public SurfaceTexture getInputSurfaceTexture() {
+ return mInputSurfaceTexture;
+ }
+}
diff --git a/src/com/android/camera/MosaicRenderer.java b/src/com/android/camera/MosaicRenderer.java
new file mode 100644
index 000000000..5006b364d
--- /dev/null
+++ b/src/com/android/camera/MosaicRenderer.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * The Java interface to JNI calls regarding mosaic preview rendering.
+ *
+ */
+public class MosaicRenderer
+{
+ static
+ {
+ System.loadLibrary("jni_snapcammosaic");
+ }
+
+ /**
+ * Function to be called in onSurfaceCreated() to initialize
+ * the GL context, load and link the shaders and create the
+ * program. Returns a texture ID to be used for SurfaceTexture.
+ *
+ * @return textureID the texture ID of the newly generated texture to
+ * be assigned to the SurfaceTexture object.
+ */
+ public static native int init(boolean mEnableWarpedPanoPreview);
+
+ /**
+ * Pass the drawing surface's width and height to initialize the
+ * renderer viewports and FBO dimensions.
+ *
+ * @param width width of the drawing surface in pixels.
+ * @param height height of the drawing surface in pixels.
+ * @param isLandscapeOrientation is the orientation of the activity layout in landscape.
+ * @param oientation is the orientation value in integer.
+ */
+ public static native void reset(int width, int height,
+ boolean isLandscapeOrientation, int orientation);
+
+ /**
+ * Calling this function will render the SurfaceTexture to a new 2D texture
+ * using the provided STMatrix.
+ *
+ * @param stMatrix texture coordinate transform matrix obtained from the
+ * Surface texture
+ */
+ public static native void preprocess(float[] stMatrix);
+
+ /**
+ * This function calls glReadPixels to transfer both the low-res and high-res
+ * data from the GPU memory to the CPU memory for further processing by the
+ * mosaicing library.
+ */
+ public static native void transferGPUtoCPU();
+
+ /**
+ * Function to be called in onDrawFrame() to update the screen with
+ * the new frame data.
+ */
+ public static native void step();
+
+ /**
+ * Call this function when a new low-res frame has been processed by
+ * the mosaicing library. This will tell the renderer library to
+ * update its texture and warping transformation. Any calls to step()
+ * after this call will use the new image frame and transformation data.
+ */
+ public static native void updateMatrix();
+
+ /**
+ * This function allows toggling between showing the input image data
+ * (without applying any warp) and the warped image data. For running
+ * the renderer as a viewfinder, we set the flag to false. To see the
+ * preview mosaic, we set the flag to true.
+ *
+ * @param flag boolean flag to set the warping to true or false.
+ */
+ public static native void setWarping(boolean flag);
+ /**
+ * This function allows toggling between drawing the background full
+ * screen preview image data to screen and drawing the warped smaller
+ * preview on top of it. To render the full screen background preview,
+ * we set the falsg to true and to render the warped image on top of this
+ * we set the flag to false and flag in setWarping to true.
+ *
+ * @param flag boolean flag to set the warping to true or false.
+ */
+ public static native void setPreviewBackground(boolean flag);
+}
diff --git a/src/com/android/camera/WideAnglePanoramaModule.java b/src/com/android/camera/WideAnglePanoramaModule.java
index 0f7596af3..cd8f6fe65 100644
--- a/src/com/android/camera/WideAnglePanoramaModule.java
+++ b/src/com/android/camera/WideAnglePanoramaModule.java
@@ -25,28 +25,28 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
-import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
import android.graphics.YuvImage;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.location.Location;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
+import android.os.PowerManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.OrientationEventListener;
-import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import com.android.camera.PhotoModule;
import com.android.camera.CameraManager.CameraProxy;
-import com.android.camera.CameraManager.CameraPreviewDataCallback;
import com.android.camera.app.OrientationManager;
import com.android.camera.data.LocalData;
import com.android.camera.exif.ExifInterface;
@@ -64,9 +64,12 @@ import java.util.TimeZone;
/**
* Activity to handle panorama capturing.
*/
-public class WideAnglePanoramaModule implements
- CameraModule, WideAnglePanoramaController,
- CameraPreviewDataCallback {
+public class WideAnglePanoramaModule
+ implements CameraModule, WideAnglePanoramaController,
+ SurfaceTexture.OnFrameAvailableListener {
+
+ public static final int DEFAULT_SWEEP_ANGLE = 160;
+ public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
@@ -84,23 +87,43 @@ public class WideAnglePanoramaModule implements
public static final int CAPTURE_STATE_VIEWFINDER = 0;
public static final int CAPTURE_STATE_MOSAIC = 1;
+ // The unit of speed is degrees per frame.
+ private static final float PANNING_SPEED_THRESHOLD = 2.5f;
private static final boolean DEBUG = false;
private ContentResolver mContentResolver;
private WideAnglePanoramaUI mUI;
+ private MosaicPreviewRenderer mMosaicPreviewRenderer;
+ private Object mRendererLock = new Object();
+ private Object mWaitObject = new Object();
private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
+ private String mPreparePreviewString;
+ private String mDialogTitle;
+ private String mDialogOkString;
+ private String mDialogPanoramaFailedString;
+ private String mDialogWaitingPreviousString;
+
+ private int mPreviewUIWidth;
+ private int mPreviewUIHeight;
private boolean mUsingFrontCamera;
private int mCameraPreviewWidth;
private int mCameraPreviewHeight;
private int mCameraState;
private int mCaptureState;
+ private PowerManager.WakeLock mPartialWakeLock;
private MosaicFrameProcessor mMosaicFrameProcessor;
+ private boolean mMosaicFrameProcessorInitialized;
+ private AsyncTask <Void, Void, Void> mWaitProcessorTask;
private long mTimeTaken;
private Handler mMainHandler;
- private SurfaceHolder mCameraSurfaceHolder;
- private boolean isCapturing = false;
+ private SurfaceTexture mCameraTexture;
+ private boolean mThreadRunning;
+ private boolean mCancelComputation;
+ private float mHorizontalViewAngle;
+ private float mVerticalViewAngle;
+
// Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
// getting a better image quality by the former.
private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
@@ -116,6 +139,8 @@ public class WideAnglePanoramaModule implements
private SoundClips.Player mSoundPlayer;
+ private Runnable mOnFrameAvailableRunnable;
+
private CameraActivity mActivity;
private View mRootView;
private CameraProxy mCameraDevice;
@@ -128,25 +153,11 @@ public class WideAnglePanoramaModule implements
private boolean mPreviewFocused = true;
private boolean mPreviewLayoutChanged = false;
- // The display rotation in degrees. This is only valid when mCameraState is
- // not PREVIEW_STOPPED.
- private int mDisplayRotation;
- // The value for android.hardware.Camera.setDisplayOrientation.
- private int mCameraDisplayOrientation;
- // The value for UI components like indicators.
- private int mDisplayOrientation;
-
- private int mCaptureOrientation = MosaicFrameProcessor.DIRECTION_UNKNOW;
- private int mPanoState = STATUS_PREVIEW;
- public static final int STATUS_PREVIEW = 0x0001;
- public static final int STATUS_PREPARE = 0x0002;
- public static final int STATUS_CAPTURING = 0x0003;
- public static final int STATUS_SUCCESS = 0x0004;
- public static final int STATUS_FAILED = 0x0006;
+ private boolean mDirectionChanged = false;
@Override
public void onPreviewUIReady() {
- resetToPreviewIfPossible();
+ configMosaicPreview();
}
@Override
@@ -212,14 +223,72 @@ public class WideAnglePanoramaModule implements
mOrientationManager = new OrientationManager(activity);
mCaptureState = CAPTURE_STATE_VIEWFINDER;
mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView);
+ mUI.setCaptureProgressOnDirectionChangeListener(
+ new PanoProgressBar.OnDirectionChangeListener() {
+ @Override
+ public void onDirectionChange(int direction) {
+ if (mDirectionChanged) {
+ stopCapture(false);
+ return;
+ }
+ if (mCaptureState == CAPTURE_STATE_MOSAIC) {
+ mUI.showDirectionIndicators(direction);
+ }
+ if (direction != PanoProgressBar.DIRECTION_NONE) {
+ mDirectionChanged = true;
+ }
+ }
+ });
mContentResolver = mActivity.getContentResolver();
+ // This runs in UI thread.
+ mOnFrameAvailableRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // Frames might still be available after the activity is paused.
+ // If we call onFrameAvailable after pausing, the GL thread will crash.
+ if (mPaused) return;
+
+ MosaicPreviewRenderer renderer = null;
+ synchronized (mRendererLock) {
+ if (mMosaicPreviewRenderer == null) {
+ return;
+ }
+ renderer = mMosaicPreviewRenderer;
+ }
+ if (mRootView.getVisibility() != View.VISIBLE) {
+ renderer.showPreviewFrameSync();
+ mRootView.setVisibility(View.VISIBLE);
+ } else {
+ if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
+ if (mPreviewLayoutChanged) {
+ boolean isLandscape = (mDeviceOrientation / 90) % 2 == 1;
+ renderer.previewReset(mPreviewUIWidth, mPreviewUIHeight,
+ isLandscape, mDeviceOrientation);
+ mPreviewLayoutChanged = false;
+ }
+ renderer.showPreviewFrame();
+ } else {
+ renderer.alignFrameSync();
+ mMosaicFrameProcessor.processFrame();
+ }
+ }
+ }
+ };
+
+ PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
+ mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
mOrientationEventListener = new PanoOrientationEventListener(mActivity);
mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
Resources appRes = mActivity.getResources();
+ mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
+ mDialogTitle = appRes.getString(R.string.pano_dialog_title);
+ mDialogOkString = appRes.getString(R.string.dialog_ok);
+ mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
+ mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
mPreferences = new ComboPreferences(mActivity);
CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal(), activity);
@@ -229,8 +298,31 @@ public class WideAnglePanoramaModule implements
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
+ case MSG_LOW_RES_FINAL_MOSAIC_READY:
+ onBackgroundThreadFinished();
+ showFinalMosaic((Bitmap) msg.obj);
+ saveHighResMosaic();
+ break;
+ case MSG_GENERATE_FINAL_MOSAIC_ERROR:
+ onBackgroundThreadFinished();
+ if (mPaused) {
+ resetToPreviewIfPossible();
+ } else {
+ mUI.showAlertDialog(
+ mDialogTitle, mDialogPanoramaFailedString,
+ mDialogOkString, new Runnable() {
+ @Override
+ public void run() {
+ resetToPreviewIfPossible();
+ }
+ });
+ }
+ clearMosaicFrameProcessorIfNeeded();
+ break;
case MSG_END_DIALOG_RESET_TO_PREVIEW:
+ onBackgroundThreadFinished();
resetToPreviewIfPossible();
+ clearMosaicFrameProcessorIfNeeded();
break;
case MSG_CLEAR_SCREEN_DELAY:
mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
@@ -358,6 +450,9 @@ public class WideAnglePanoramaModule implements
}
parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE);
+
+ mHorizontalViewAngle = parameters.getHorizontalViewAngle();
+ mVerticalViewAngle = parameters.getVerticalViewAngle();
}
public int getPreviewBufSize() {
@@ -372,6 +467,44 @@ public class WideAnglePanoramaModule implements
}
/**
+ * Configures the preview renderer according to the dimension defined by
+ * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}.
+ * Will stop the camera preview first.
+ */
+ private void configMosaicPreview() {
+ if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0
+ || mUI.getSurfaceTexture() == null) {
+ return;
+ }
+
+ stopCameraPreview();
+ synchronized (mRendererLock) {
+ if (mMosaicPreviewRenderer != null) {
+ mMosaicPreviewRenderer.release();
+ }
+ mMosaicPreviewRenderer = null;
+ }
+ final boolean isLandscape = (mDeviceOrientation / 90) % 2 == 1;
+ final boolean enableWarpedPanoPreview =
+ mActivity.getResources().getBoolean(R.bool.enable_warped_pano_preview);
+ mUI.flipPreviewIfNeeded();
+ MosaicPreviewRenderer renderer = new MosaicPreviewRenderer(
+ mUI.getSurfaceTexture(), mPreviewUIWidth, mPreviewUIHeight,
+ isLandscape, mDeviceOrientation, enableWarpedPanoPreview);
+ synchronized (mRendererLock) {
+ mMosaicPreviewRenderer = renderer;
+ mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
+
+ if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
+ mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
+ }
+ mRendererLock.notifyAll();
+ }
+ mMosaicPreviewConfigured = true;
+ resetToPreviewIfPossible();
+ }
+
+ /**
* Receives the layout change event from the preview area. So we can
* initialize the mosaic preview renderer.
*/
@@ -382,6 +515,9 @@ public class WideAnglePanoramaModule implements
if (mCaptureState == CAPTURE_STATE_MOSAIC){
capturePending = true;
}
+ mPreviewUIWidth = r - l;
+ mPreviewUIHeight = b - t;
+ configMosaicPreview();
if (capturePending == true){
mMainHandler.post(new Runnable() {
@Override
@@ -395,34 +531,68 @@ public class WideAnglePanoramaModule implements
}
}
+ @Override
+ public void onFrameAvailable(SurfaceTexture surface) {
+ /* This function may be called by some random thread,
+ * so let's be safe and jump back to ui thread.
+ * No OpenGL calls can be done here. */
+ mActivity.runOnUiThread(mOnFrameAvailableRunnable);
+ }
+
public void startCapture() {
// Reset values so we can do this again.
+ mCancelComputation = false;
mTimeTaken = System.currentTimeMillis();
mActivity.setSwipingEnabled(false);
mCaptureState = CAPTURE_STATE_MOSAIC;
mUI.onStartCapture();
- mCaptureOrientation = MosaicFrameProcessor.DIRECTION_UNKNOW;
Parameters parameters = mCameraDevice.getParameters();
parameters.setAutoExposureLock(true);
parameters.setAutoWhiteBalanceLock(true);
configureCamera(parameters);
- updateState(STATUS_PREPARE);
- createPanoramaEngine();
+ mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
+ @Override
+ public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
+ float progressX, float progressY) {
+ float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
+ float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
+ boolean isRotated = !(mDeviceOrientationAtCapture == mDeviceOrientation);
+ if (isFinished
+ || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
+ || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)
+ || isRotated) {
+ stopCapture(false);
+ } else {
+ float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
+ float panningRateYInDegree = panningRateY * mVerticalViewAngle;
+ if (mDeviceOrientation == 180 || mDeviceOrientation == 90) {
+ accumulatedHorizontalAngle = -accumulatedHorizontalAngle;
+ accumulatedVerticalAngle = -accumulatedVerticalAngle;
+ }
+ mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree,
+ accumulatedHorizontalAngle, accumulatedVerticalAngle,
+ PANNING_SPEED_THRESHOLD);
+ }
+ }
+ });
+
+ mUI.resetCaptureProgress();
+ // TODO: calculate the indicator width according to different devices to reflect the actual
+ // angle of view of the camera device.
+ mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE);
+ mUI.showCaptureProgress();
mDeviceOrientationAtCapture = mDeviceOrientation;
keepScreenOn();
mOrientationLocked = true;
- }
-
- private void createPanoramaEngine() {
- if (mMosaicFrameProcessor == null) {
- mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
- }
- mMosaicFrameProcessor.Init(mActivity, 30, mCameraPreviewWidth, mCameraPreviewHeight,
- mPanoNotifier);
+ int degrees = CameraUtil.getDisplayRotation(mActivity);
+ int cameraId = CameraHolder.instance().getBackCameraId();
+ int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId);
+ mUI.setProgressOrientation(orientation);
}
private void stopCapture(boolean aborted) {
+ mDirectionChanged = false;
mCaptureState = CAPTURE_STATE_VIEWFINDER;
mUI.onStopCapture();
Parameters parameters = mCameraDevice.getParameters();
@@ -430,20 +600,41 @@ public class WideAnglePanoramaModule implements
parameters.setAutoWhiteBalanceLock(false);
configureCamera(parameters);
- if (mMosaicFrameProcessor != null) {
- mMosaicFrameProcessor.StopProcessing();
- }
-
+ mMosaicFrameProcessor.setProgressListener(null);
stopCameraPreview();
- mMainHandler.sendMessage(mMainHandler.obtainMessage(
- MSG_END_DIALOG_RESET_TO_PREVIEW));
+ mCameraTexture.setOnFrameAvailableListener(null);
+
+ if (!aborted && !mThreadRunning) {
+ mUI.showWaitingDialog(mPreparePreviewString);
+ // Hide shutter button, shutter icon, etc when waiting for
+ // panorama to stitch
+ mUI.hideUI();
+ runBackgroundThread(new Thread() {
+ @Override
+ public void run() {
+ MosaicJpeg jpeg = generateFinalMosaic(false);
+
+ if (jpeg != null && jpeg.isValid) {
+ Bitmap bitmap = null;
+ bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
+ } else {
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_END_DIALOG_RESET_TO_PREVIEW));
+ }
+ }
+ });
+ }
keepScreenOnAwhile();
}
@Override
public void onShutterButtonClick() {
- if (mPaused || mCameraSurfaceHolder == null) {
+ // If mCameraTexture == null then GL setup is not finished yet.
+ // No buttons can be pressed.
+ if (mPaused || mThreadRunning || mCameraTexture == null) {
return;
}
// Since this button will stay on the screen when capturing, we need to check the state
@@ -468,6 +659,35 @@ public class WideAnglePanoramaModule implements
}
}
+ public void reportProgress() {
+ mUI.resetSavingProgress();
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ while (mThreadRunning) {
+ final int progress = mMosaicFrameProcessor.reportProgress(
+ true, mCancelComputation);
+
+ try {
+ synchronized (mWaitObject) {
+ mWaitObject.wait(50);
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Panorama reportProgress failed", e);
+ }
+ // Update the progress bar
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mUI.updateSavingProgress(progress);
+ }
+ });
+ }
+ }
+ };
+ t.start();
+ }
+
private int getCaptureOrientation() {
// The panorama image returned from the library is oriented based on the
// natural orientation of a camera. We need to set an orientation for the image
@@ -493,9 +713,62 @@ public class WideAnglePanoramaModule implements
return mCameraOrientation;
}
+ public void saveHighResMosaic() {
+ runBackgroundThread(new Thread() {
+ @Override
+ public void run() {
+ mPartialWakeLock.acquire();
+ MosaicJpeg jpeg;
+ try {
+ jpeg = generateFinalMosaic(true);
+ } finally {
+ mPartialWakeLock.release();
+ }
+
+ if (jpeg == null) { // Cancelled by user.
+ mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW);
+ } else if (!jpeg.isValid) { // Error when generating mosaic.
+ mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
+ } else {
+ int orientation = getCaptureOrientation();
+ final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
+ if (uri != null) {
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.notifyNewMedia(uri);
+ }
+ });
+ }
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW));
+ }
+ }
+ });
+ reportProgress();
+ }
+
+ private void runBackgroundThread(Thread thread) {
+ mThreadRunning = true;
+ thread.start();
+ }
+
+ private void onBackgroundThreadFinished() {
+ mThreadRunning = false;
+ mUI.dismissAllDialogs();
+ }
+
+ private void cancelHighResComputation() {
+ mCancelComputation = true;
+ synchronized (mWaitObject) {
+ mWaitObject.notify();
+ }
+ }
+
// This function will be called upon the first camera frame is available.
private void reset() {
mCaptureState = CAPTURE_STATE_VIEWFINDER;
+ mDirectionChanged = false;
mOrientationLocked = false;
mUI.setOrientation(mDeviceOrientation, true);
@@ -506,11 +779,14 @@ public class WideAnglePanoramaModule implements
if (mPreviewFocused) {
mUI.showPreviewUI();
}
+ mMosaicFrameProcessor.reset();
}
private void resetToPreviewIfPossible() {
reset();
- if (mUI.getSurfaceHolder() == null) {
+ if (!mMosaicFrameProcessorInitialized
+ || mUI.getSurfaceTexture() == null
+ || !mMosaicPreviewConfigured) {
return;
}
if (!mPaused) {
@@ -563,6 +839,38 @@ public class WideAnglePanoramaModule implements
exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()));
}
+ private void clearMosaicFrameProcessorIfNeeded() {
+ if (!mPaused || mThreadRunning) return;
+ // Only clear the processor if it is initialized by this activity
+ // instance. Other activity instances may be using it.
+ if (mMosaicFrameProcessorInitialized) {
+ mMosaicFrameProcessor.clear();
+ mMosaicFrameProcessorInitialized = false;
+ }
+ }
+
+ private void initMosaicFrameProcessorIfNeeded() {
+ if (mPaused || mThreadRunning) {
+ return;
+ }
+
+ int perct = 100;
+ final ActivityManager am = (ActivityManager)
+ mActivity.getSystemService(Context.ACTIVITY_SERVICE);
+ if (am.isLowRamDevice()) {
+ perct = mActivity.getResources().getInteger(R.integer.panorama_frame_size_reduction);
+ }
+
+ int width = (mCameraPreviewWidth * perct) / 100;
+ int height = (mCameraPreviewHeight * perct) / 100;
+ if ((0 < width) && (0 < height)) {
+ mMosaicFrameProcessor.initialize(width, height, getPreviewBufSize());
+ mMosaicFrameProcessorInitialized = true;
+ } else {
+ throw new RuntimeException("Invalid preview dimension");
+ }
+ }
+
@Override
public void onPauseBeforeSuper() {
mPaused = true;
@@ -584,8 +892,22 @@ public class WideAnglePanoramaModule implements
}
mUI.showPreviewCover();
releaseCamera();
- mCameraSurfaceHolder = null;
+ synchronized (mRendererLock) {
+ mCameraTexture = null;
+
+ // The preview renderer might not have a chance to be initialized
+ // before onPause().
+ if (mMosaicPreviewRenderer != null) {
+ mMosaicPreviewRenderer.release();
+ mMosaicPreviewRenderer = null;
+ }
+ }
+ clearMosaicFrameProcessorIfNeeded();
+ if (mWaitProcessorTask != null) {
+ mWaitProcessorTask.cancel(true);
+ mWaitProcessorTask = null;
+ }
resetScreenOn();
mUI.removeDisplayChangeListener();
if (mSoundPlayer != null) {
@@ -597,7 +919,7 @@ public class WideAnglePanoramaModule implements
@Override
public void onConfigurationChanged(Configuration newConfig) {
- mUI.onConfigurationChanged(newConfig, false);
+ mUI.onConfigurationChanged(newConfig, mThreadRunning);
}
@Override
@@ -631,20 +953,33 @@ public class WideAnglePanoramaModule implements
Log.e(TAG, "Failed to open camera, aborting");
return;
}
- resetToPreviewIfPossible();
// Set up sound playback for shutter button
mSoundPlayer = SoundClips.getPlayer(mActivity);
// Check if another panorama instance is using the mosaic frame processor.
mUI.dismissAllDialogs();
-
- mMainHandler.post(new Runnable(){
- @Override
- public void run(){
- mActivity.updateStorageSpaceAndHint();
- }
- });
+ if (mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
+ mUI.showWaitingDialog(mDialogWaitingPreviousString);
+ // If stitching is still going on, make sure switcher and shutter button
+ // are not showing
+ mUI.hideUI();
+ mWaitProcessorTask = new WaitProcessorTask().execute();
+ } else {
+ // Camera must be initialized before MosaicFrameProcessor is
+ // initialized. The preview size has to be decided by camera device.
+ initMosaicFrameProcessorIfNeeded();
+ Point size = mUI.getPreviewAreaSize();
+ mPreviewUIWidth = size.x;
+ mPreviewUIHeight = size.y;
+ configMosaicPreview();
+ mMainHandler.post(new Runnable(){
+ @Override
+ public void run(){
+ mActivity.updateStorageSpaceAndHint();
+ }
+ });
+ }
keepScreenOnAwhile();
mOrientationManager.resume();
@@ -655,19 +990,54 @@ public class WideAnglePanoramaModule implements
mUI.initDisplayChangeListener();
UsageStatistics.onContentViewChanged(
UsageStatistics.COMPONENT_CAMERA, "PanoramaModule");
- mUI.hidePreviewCover();
}
- private void setDisplayOrientation() {
- mDisplayRotation = CameraUtil.getDisplayRotation(mActivity);
- mDisplayOrientation = CameraUtil.getDisplayOrientation(mDisplayRotation, CameraHolder
- .instance().getBackCameraId());
- mCameraDisplayOrientation = mDisplayOrientation;
+ /**
+ * Generate the final mosaic image.
+ *
+ * @param highRes flag to indicate whether we want to get a high-res version.
+ * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
+ * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
+ * is an error in generating the final mosaic.
+ */
+ public MosaicJpeg generateFinalMosaic(boolean highRes) {
+ int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
+ if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
+ return null;
+ } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
+ return new MosaicJpeg();
+ }
- // Change the camera display orientation
- if (mCameraDevice != null) {
- mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+ byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
+ if (imageData == null) {
+ Log.e(TAG, "getFinalMosaicNV21() returned null.");
+ return new MosaicJpeg();
+ }
+
+ int len = imageData.length - 8;
+ int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
+ + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
+ int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
+ + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
+ Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
+
+ if (width <= 0 || height <= 0) {
+ // TODO: pop up an error message indicating that the final result is not generated.
+ Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
+ height);
+ return new MosaicJpeg();
}
+
+ YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
+ try {
+ out.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in storing final mosaic", e);
+ return new MosaicJpeg();
+ }
+ return new MosaicJpeg(out.toByteArray(), width, height);
}
private void startCameraPreview() {
@@ -676,25 +1046,38 @@ public class WideAnglePanoramaModule implements
return;
}
- if (mUI.getSurfaceHolder() == null) {
+ if (mUI.getSurfaceTexture() == null) {
// UI is not ready.
return;
}
mErrorCallback.setActivity(mActivity);
mCameraDevice.setErrorCallback(mErrorCallback);
- if (mUI.getSurfaceHolder() == null) return;
- mCameraSurfaceHolder = mUI.getSurfaceHolder();
-
- // If we're previewing already, stop the preview first (this will
- // blank the screen).
- if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
-
- mCameraDevice.setPreviewDataCallback(mMainHandler, this);
- mCameraDevice.setPreviewDisplay(mCameraSurfaceHolder);
-
+ // This works around a driver issue. startPreview may fail if
+ // stopPreview/setPreviewTexture/startPreview are called several times
+ // in a row. mCameraTexture can be null after pressing home during
+ // mosaic generation and coming back. Preview will be started later in
+ // onLayoutChange->configMosaicPreview. This also reduces the latency.
+ synchronized (mRendererLock) {
+ if (mCameraTexture == null) return;
+
+ // If we're previewing already, stop the preview first (this will
+ // blank the screen).
+ if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
+
+ // Set the display orientation to 0, so that the underlying mosaic
+ // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight
+ // image data from SurfaceTexture.
+ // as Panoroma will add 90 degree rotation compensation during
+ // postprocessing, we need to consider both camera mount angle and
+ // this compensation angle
+ mCameraDevice.setDisplayOrientation(0);
+
+ if (mCameraTexture != null)
+ mCameraTexture.setOnFrameAvailableListener(this);
+ mCameraDevice.setPreviewTexture(mCameraTexture);
+ }
mCameraDevice.startPreview();
- setDisplayOrientation();
mCameraState = PREVIEW_ACTIVE;
}
@@ -714,6 +1097,8 @@ public class WideAnglePanoramaModule implements
public boolean onBackPressed() {
// If panorama is generating low res or high res mosaic, ignore back
// key. So the activity will not be destroyed.
+ if (mThreadRunning) return true;
+
if (mUI.hideSwitcherPopup())
return true;
@@ -736,8 +1121,41 @@ public class WideAnglePanoramaModule implements
mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
+ private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (mMosaicFrameProcessor) {
+ while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
+ try {
+ mMosaicFrameProcessor.wait();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+ mActivity.updateStorageSpace();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ mWaitProcessorTask = null;
+ mUI.dismissAllDialogs();
+ // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE);
+ initMosaicFrameProcessorIfNeeded();
+ Point size = mUI.getPreviewAreaSize();
+ mPreviewUIWidth = size.x;
+ mPreviewUIHeight = size.y;
+ configMosaicPreview();
+ resetToPreviewIfPossible();
+ mActivity.updateStorageHint(mActivity.getStorageSpaceBytes());
+ }
+ }
+
@Override
public void cancelHighResStitching() {
+ if (mPaused || mCameraTexture == null) return;
+ cancelHighResComputation();
}
@Override
@@ -752,226 +1170,6 @@ public class WideAnglePanoramaModule implements
public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
- @Override
- public void onPreviewFrame(byte[] data, CameraProxy camera) {
- if (mMosaicFrameProcessor != null && mMosaicFrameProcessor.IsInited()) {
- mMosaicFrameProcessor.Process(data, mCameraPreviewWidth, mCameraPreviewHeight);
- }
- }
-
- /**
- * This class defines panorama stitching information.
- **/
- static class PanoramaInfo {
- /** The angle for the panorama information. */
- public int angle;
- /** The offset of the next preview frame to be acquired. */
- public Point offset;
- /** The direction of capturing. */
- public int direction;
- /**
- * Indicate whether current frame is properly positioned to be added for
- * stitching: 0 for not properly positioned, 1 for ready to stitch.
- */
- public int selected;
-
- /**
- * This is the default constructor of the panorama information object.
- */
- public PanoramaInfo() {
- offset = new Point(0, 0);
- }
-
- public PanoramaInfo(int ang, Point ptOffset, int direct, int sel) {
- angle = ang;
- offset = new Point(ptOffset.x, ptOffset.y);
- direction = direct;
- selected = sel;
- }
-
- public PanoramaInfo(int ang, int offsetX, int offsetY, int direct, int sel) {
- angle = ang;
- offset = new Point(offsetX, offsetY);
- direction = direct;
- selected = sel;
- }
- }
-
- public interface INotifier {
- /**
- * Messages are notified to the application through this function.
- *
- * @param key The message key.
- * @param obj The message object with this message key.
- * @return The user defined result of processing message.
- */
- int onNotify(int key, Object obj);
- }
-
- private final INotifier mPanoNotifier = new INotifier() {
- @Override
- public int onNotify(int key, Object obj) {
- byte[] imageData;
- int len;
- int width;
- int height;
- YuvImage yuvimage;
- ByteArrayOutputStream out;
- switch (key) {
- case MosaicFrameProcessor.MSG_PANORAMA_TIP:
- if (mCaptureState == CAPTURE_STATE_MOSAIC) {
- PanoramaInfo panoInfo = (PanoramaInfo) obj;
- if (null == panoInfo)
- break;
- if (!isCapturing) {
- isCapturing = true;
-
- /**
- * Device Orientation and Capture Orientation Note
- * that: Pano's left is always device's top; Pano's
- * right is always device's bottom Pano's top is
- * always device's right; Pano's bottom is always
- * device's left
- */
- mCaptureOrientation = panoInfo.direction;
- updateState(STATUS_CAPTURING);
- }
-
- boolean bIsHorizonal = MosaicFrameProcessor.DIRECTION_LEFTTORIGHT == panoInfo.direction
- || MosaicFrameProcessor.DIRECTION_RIGHTTOLEFT == panoInfo.direction;
- int iAbsAngle = Math.abs(panoInfo.angle);
- if ((bIsHorizonal && iAbsAngle >= MosaicFrameProcessor.MAX_HORIZONAL_ANGLE)
- || (!bIsHorizonal && iAbsAngle >= MosaicFrameProcessor.MAX_VERTICAL_ANGLE)) {
- Log.v(TAG, TAG + "capture end !");
- break;
- }
- }
- break;
-
- case MosaicFrameProcessor.MSG_CAPTURE_FAILED:
- if (mCaptureState == CAPTURE_STATE_MOSAIC) {
- mMosaicFrameProcessor.Uninit();
- }
- break;
-
- case MosaicFrameProcessor.MSG_CAPTURE_SUCCESS:
- imageData = (byte[]) obj;
-
- len = imageData.length - 8;
- width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
- + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
- height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
- + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
-
- if (width == 0 || height == 0) {
- onNotify(MosaicFrameProcessor.MSG_CAPTURE_FAILED, null);
- return 0;
- }
-
- yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
- out = new ByteArrayOutputStream();
- yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
- final MosaicJpeg jpeg = new MosaicJpeg(out.toByteArray(), width, height);
- try {
- out.close();
- } catch (Exception e) {
- mMosaicFrameProcessor.Uninit();
- Log.e(TAG, "Exception in storing final mosaic", e);
- return 0;
- }
-
- mMainHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- saveFinalMosaic(jpeg);
- }
- }, 200);
- break;
- case MosaicFrameProcessor.MSG_UPDATE_UI:
- if (mCaptureState == CAPTURE_STATE_MOSAIC) {
- if (mCaptureOrientation == 0)
- break;
- imageData = (byte[]) obj;
- len = imageData.length - 8;
- width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
- + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
- height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
- + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
-
- yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
- out = new ByteArrayOutputStream();
- yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
- byte[] jpeg2 = out.toByteArray();
- Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg2, 0, jpeg2.length);
- Matrix rotateMatrix = new Matrix();
- rotateMatrix.setRotate(getCaptureOrientation());
- bitmap = Bitmap.createBitmap(
- bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
- rotateMatrix, false);
- try {
- out.close();
- } catch (Exception e) {
- }
-
- boolean horiz = true;
- switch (mDeviceOrientationAtCapture) {
- case 0:
- case 180:
- horiz = (mCaptureOrientation == 4 || mCaptureOrientation == 8);
- break;
- case 90:
- case 270:
- horiz = (mCaptureOrientation == 1 || mCaptureOrientation == 2);
- break;
-
- }
- mUI.drawCapturePreview(bitmap, mDeviceOrientationAtCapture, horiz);
- }
- break;
- default:
- Log.v(TAG, "on Notify unknown MSG: " + key);
- break;
- }
- return 0;
- }
- };
-
- void saveFinalMosaic(MosaicJpeg jpeg) {
- mMosaicFrameProcessor.Uninit();
- if (mCaptureState == CAPTURE_STATE_MOSAIC)
- stopCapture(false);
- Bitmap bitmap = null;
- bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
- int orientation = getCaptureOrientation();
- showFinalMosaic(bitmap);
-
- final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
- if (uri != null) {
- mActivity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.notifyNewMedia(uri);
- }
- });
- }
- updateState(STATUS_SUCCESS);
- }
-
- public void updateState(int status) {
- mPanoState = status;
- switch (status) {
- case STATUS_PREVIEW:
- case STATUS_SUCCESS:
- case STATUS_PREPARE:
- case STATUS_FAILED:
- isCapturing = false;
- break;
-
- case STATUS_CAPTURING:
- isCapturing = true;
- break;
- }
- }
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
diff --git a/src/com/android/camera/WideAnglePanoramaUI.java b/src/com/android/camera/WideAnglePanoramaUI.java
index ba42fc969..862e4d2ba 100644
--- a/src/com/android/camera/WideAnglePanoramaUI.java
+++ b/src/com/android/camera/WideAnglePanoramaUI.java
@@ -29,26 +29,21 @@ import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
-import android.util.TypedValue;
import android.view.Gravity;
import android.view.Display;
import android.view.LayoutInflater;
-import android.view.SurfaceView;
-import android.view.SurfaceHolder;
-import android.view.SurfaceHolder.Callback;
+import android.view.TextureView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
-import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageView;
-import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -64,7 +59,7 @@ import org.codeaurora.snapcam.R;
* The UI of {@link WideAnglePanoramaModule}.
*/
public class WideAnglePanoramaUI implements
- SurfaceHolder.Callback,
+ TextureView.SurfaceTextureListener,
ShutterButton.OnShutterButtonListener,
CameraRootView.MyDisplayListener,
View.OnLayoutChangeListener {
@@ -81,23 +76,29 @@ public class WideAnglePanoramaUI implements
private View mReviewLayout;
private ImageView mReview;
private View mPreviewBorder;
+ private View mLeftIndicator;
+ private View mRightIndicator;
private View mCaptureIndicator;
+ private PanoProgressBar mCaptureProgressBar;
private PanoProgressBar mSavingProgressBar;
private TextView mTooFastPrompt;
private View mPreviewLayout;
private ViewGroup mReviewControl;
- private SurfaceView mSurfaceView;
+ private TextureView mTextureView;
private ShutterButton mShutterButton;
private CameraControls mCameraControls;
private ImageView mThumbnail;
- private ImageView mCapturePreview;
- private ViewGroup mCapturePreviewLayout;
+
+ private Matrix mProgressDirectionMatrix = new Matrix();
+ private float[] mProgressAngle = new float[2];
private DialogHelper mDialogHelper;
// Color definitions.
+ private int mIndicatorColor;
+ private int mIndicatorColorFast;
private int mReviewBackground;
- private SurfaceHolder mSurfaceHolder;
+ private SurfaceTexture mSurfaceTexture;
private View mPreviewCover;
private int mOrientation;
@@ -106,34 +107,6 @@ public class WideAnglePanoramaUI implements
private RotateLayout mPanoFailedDialog;
private Button mPanoFailedButton;
- private int mPreviewWidth = 0;
- private int mPreviewHeight = 0;
- private int mOriginalPreviewWidth = 0;
- private int mOriginalPreviewHeight = 0;
-
- private boolean mOrientationResize;
- private boolean mPrevOrientationResize;
-
- private Matrix mMatrix = null;
-
- private int mScreenRatio = CameraUtil.RATIO_UNKNOWN;
- private int mTopMargin = 0;
- private int mBottomMargin = 0;
- private boolean mCapturePreviewSet = false;
-
- private final int capturePreviewW = 80;
- private final int capturePreviewH = 80;
- private final int sidePadding = 10;
- private final int botPadding = 130;
-
- private void setTransformMatrix(int width, int height) {
- mMatrix = mSurfaceView.getMatrix();
-
- // Calculate the new preview rectangle.
- RectF previewRect = new RectF(0, 0, width, height);
- mMatrix.mapRect(previewRect);
- }
-
/** Constructor. */
public WideAnglePanoramaUI(
CameraActivity activity,
@@ -163,27 +136,13 @@ public class WideAnglePanoramaUI implements
mSwitcher.setOrientation(mOrientation, false);
}
});
-
- mOrientationResize = false;
- mPrevOrientationResize = false;
-
- Point size = new Point();
- mActivity.getWindowManager().getDefaultDisplay().getSize(size);
- mScreenRatio = CameraUtil.determineRatio(size.x, size.y);
- if (mScreenRatio == CameraUtil.RATIO_16_9) {
- int l = size.x > size.y ? size.x : size.y;
- int tm = mActivity.getResources().getDimensionPixelSize(R.dimen.preview_top_margin);
- int bm = mActivity.getResources().getDimensionPixelSize(R.dimen.preview_bottom_margin);
- mTopMargin = l / 4 * tm / (tm + bm);
- mBottomMargin = l / 4 - mTopMargin;
- }
- mCameraControls.setMargins(mTopMargin, mBottomMargin);
}
public void onStartCapture() {
hideSwitcher();
mShutterButton.setImageResource(R.drawable.shutter_button_stop);
mCaptureIndicator.setVisibility(View.VISIBLE);
+ showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
}
public void showPreviewUI() {
@@ -193,8 +152,8 @@ public class WideAnglePanoramaUI implements
public void onStopCapture() {
mCaptureIndicator.setVisibility(View.INVISIBLE);
- mCapturePreview.setImageBitmap(null);
- mCapturePreviewSet = false;
+ hideTooFastIndication();
+ hideDirectionIndicators();
}
public void hideSwitcher() {
@@ -228,56 +187,116 @@ public class WideAnglePanoramaUI implements
mSwitcher.setVisibility(View.VISIBLE);
}
- public void drawCapturePreview(Bitmap bitmap, int mDeviceOrientationAtCapture, boolean horiz) {
- if (!mCapturePreviewSet) {
- int w = horiz ? LayoutParams.MATCH_PARENT : convertDpToPix(capturePreviewW);
- int h = horiz ? convertDpToPix(capturePreviewH) : LayoutParams.MATCH_PARENT;
- FrameLayout.LayoutParams param = (FrameLayout.LayoutParams) mCapturePreview
- .getLayoutParams();
- param.height = h;
- param.width = w;
- param.gravity = Gravity.CENTER;
- mCapturePreview.setLayoutParams(param);
+ public void setCaptureProgressOnDirectionChangeListener(
+ PanoProgressBar.OnDirectionChangeListener listener) {
+ mCaptureProgressBar.setOnDirectionChangeListener(listener);
+ }
+
+ public void resetCaptureProgress() {
+ mCaptureProgressBar.reset();
+ }
+
+ public void setMaxCaptureProgress(int max) {
+ mCaptureProgressBar.setMaxProgress(max);
+ }
+
+ public void showCaptureProgress() {
+ mCaptureProgressBar.setVisibility(View.VISIBLE);
+ }
- mCapturePreviewSet = true;
- setPreviewOrientation(mDeviceOrientationAtCapture, horiz);
+ public void updateCaptureProgress(
+ float panningRateXInDegree, float panningRateYInDegree,
+ float progressHorizontalAngle, float progressVerticalAngle,
+ float maxPanningSpeed) {
+ if ((Math.abs(panningRateXInDegree) > maxPanningSpeed)
+ || (Math.abs(panningRateYInDegree) > maxPanningSpeed)) {
+ showTooFastIndication();
+ } else {
+ hideTooFastIndication();
}
- mCapturePreview.setImageBitmap(bitmap);
+
+ // progressHorizontalAngle and progressVerticalAngle are relative to the
+ // camera. Convert them to UI direction.
+ mProgressAngle[0] = progressHorizontalAngle;
+ mProgressAngle[1] = progressVerticalAngle;
+ mProgressDirectionMatrix.mapPoints(mProgressAngle);
+
+ int angleInMajorDirection =
+ (Math.abs(mProgressAngle[0]) > Math.abs(mProgressAngle[1]))
+ ? (int) mProgressAngle[0]
+ : (int) mProgressAngle[1];
+ mCaptureProgressBar.setProgress((angleInMajorDirection));
}
- public SurfaceHolder getSurfaceHolder() {
- return mSurfaceHolder;
+ public void setProgressOrientation(int orientation) {
+ mProgressDirectionMatrix.reset();
+ mProgressDirectionMatrix.postRotate(orientation);
}
- // SurfaceHolder callbacks
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- hidePreviewCover();
+ public void showDirectionIndicators(int direction) {
+ switch (direction) {
+ case PanoProgressBar.DIRECTION_NONE:
+ mLeftIndicator.setVisibility(View.VISIBLE);
+ mRightIndicator.setVisibility(View.VISIBLE);
+ break;
+ case PanoProgressBar.DIRECTION_LEFT:
+ mLeftIndicator.setVisibility(View.VISIBLE);
+ mRightIndicator.setVisibility(View.INVISIBLE);
+ break;
+ case PanoProgressBar.DIRECTION_RIGHT:
+ mLeftIndicator.setVisibility(View.INVISIBLE);
+ mRightIndicator.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+
+ public SurfaceTexture getSurfaceTexture() {
+ return mSurfaceTexture;
}
@Override
- public void surfaceCreated(SurfaceHolder holder) {
- mSurfaceHolder = holder;
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {
+ mSurfaceTexture = surfaceTexture;
mController.onPreviewUIReady();
+ mActivity.updateThumbnail(mThumbnail);
+ }
- if (mPreviewWidth != 0 && mPreviewHeight != 0) {
- // Re-apply transform matrix for new surface texture
- setTransformMatrix(mPreviewWidth, mPreviewHeight);
- }
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {
- mActivity.updateThumbnail(mThumbnail);
}
@Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- mSurfaceHolder = null;
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
mController.onPreviewUIDestroyed();
+ mSurfaceTexture = null;
+ Log.d(TAG, "surfaceTexture is destroyed");
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+ // Make sure preview cover is hidden if preview data is available.
+ if (mPreviewCover.getVisibility() != View.GONE) {
+ mPreviewCover.setVisibility(View.GONE);
+ }
+ }
+
+ private void hideDirectionIndicators() {
+ mLeftIndicator.setVisibility(View.INVISIBLE);
+ mRightIndicator.setVisibility(View.INVISIBLE);
+ }
+
+ public Point getPreviewAreaSize() {
+ return new Point(
+ mTextureView.getWidth(), mTextureView.getHeight());
}
public void reset() {
mShutterButton.setImageResource(R.drawable.btn_new_shutter_panorama);
mReviewLayout.setVisibility(View.GONE);
+ mCaptureProgressBar.setVisibility(View.INVISIBLE);
}
public void showFinalMosaic(Bitmap bitmap, int orientation) {
@@ -289,6 +308,14 @@ public class WideAnglePanoramaUI implements
rotateMatrix, false);
}
+ mReview.setImageBitmap(bitmap);
+ mCaptureLayout.setVisibility(View.GONE);
+ mReviewLayout.setVisibility(View.VISIBLE);
+ // If capture is stopped by device rotation, the rendering progress bar
+ // is sometimes not shown due to wrong layout result. It's likely to be
+ // a framework bug. Call requestLayout() as a workaround.
+ mSavingProgressBar.requestLayout();
+
mActivity.updateThumbnail(bitmap);
}
@@ -306,6 +333,7 @@ public class WideAnglePanoramaUI implements
inflater.inflate(R.layout.pano_review_control, mReviewControl, true);
mRootView.bringChildToFront(mCameraControls);
+ setViews(mActivity.getResources());
if (threadRunning) {
mReview.setImageDrawable(lowResReview);
mCaptureLayout.setVisibility(View.GONE);
@@ -330,18 +358,31 @@ public class WideAnglePanoramaUI implements
yOffset = (height - h) / 2;
FrameLayout.LayoutParams param = new FrameLayout.LayoutParams(w, h);
-
- mSurfaceView.setLayoutParams(param);
- mSurfaceView.setX(xOffset);
- mSurfaceView.setY(yOffset);
+ mTextureView.setLayoutParams(param);
+ mTextureView.setX(xOffset);
+ mTextureView.setY(yOffset);
mPreviewBorder.setLayoutParams(param);
mPreviewBorder.setX(xOffset);
mPreviewBorder.setY(yOffset);
mPreviewYOffset = yOffset;
+ int t = mPreviewYOffset;
+ int b1 = mTextureView.getBottom() - mPreviewYOffset;
+ int r = mTextureView.getRight();
+ int b2 = mTextureView.getBottom();
+
mCameraControls.setPreviewRatio(1.0f, true);
}
+ public void resetSavingProgress() {
+ mSavingProgressBar.reset();
+ mSavingProgressBar.setRightIncreasing(true);
+ }
+
+ public void updateSavingProgress(int progress) {
+ mSavingProgressBar.setProgress(progress);
+ }
+
@Override
public void onShutterButtonFocus(boolean pressed) {
// Do nothing.
@@ -381,18 +422,29 @@ public class WideAnglePanoramaUI implements
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflator.inflate(R.layout.panorama_module, mRootView, true);
+ Resources appRes = mActivity.getResources();
+ mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
+ mReviewBackground = appRes.getColor(R.color.review_background);
+ mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
+
mPreviewCover = mRootView.findViewById(R.id.preview_cover);
mPreviewLayout = mRootView.findViewById(R.id.pano_preview_layout);
mReviewControl = (ViewGroup) mRootView.findViewById(R.id.pano_review_control);
mReviewLayout = mRootView.findViewById(R.id.pano_review_layout);
mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea);
mCaptureLayout = (FrameLayout) mRootView.findViewById(R.id.panorama_capture_layout);
- mCapturePreviewLayout = (FrameLayout) mRootView
- .findViewById(R.id.pano_capture_preview_layout);
- mCapturePreview = (ImageView) mRootView.findViewById(R.id.pano_capture_preview);
- mCapturePreview.setScaleType(ScaleType.FIT_CENTER);
+ mCaptureProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar);
+ mCaptureProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
+ mCaptureProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
+ mCaptureProgressBar.setIndicatorColor(mIndicatorColor);
+ mCaptureProgressBar.setIndicatorWidth(20);
+
mPreviewBorder = mCaptureLayout.findViewById(R.id.pano_preview_area_border);
+ mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator);
+ mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator);
+ mLeftIndicator.setEnabled(false);
+ mRightIndicator.setEnabled(false);
mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview);
mCaptureIndicator = mRootView.findViewById(R.id.pano_capture_indicator);
@@ -406,11 +458,9 @@ public class WideAnglePanoramaUI implements
// TODO: set display change listener properly.
((CameraRootView) mRootView).setDisplayChangeListener(null);
- mSurfaceView = (SurfaceView) mRootView.findViewById(R.id.pano_preview_surfaceview);
- mSurfaceHolder = mSurfaceView.getHolder();
- mSurfaceHolder.addCallback(this);
- mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
- mSurfaceView.addOnLayoutChangeListener(this);
+ mTextureView = (TextureView) mRootView.findViewById(R.id.pano_preview_textureview);
+ mTextureView.setSurfaceTextureListener(this);
+ mTextureView.addOnLayoutChangeListener(this);
mCameraControls = (CameraControls) mRootView.findViewById(R.id.camera_controls);
setPanoramaPreviewView();
@@ -418,11 +468,64 @@ public class WideAnglePanoramaUI implements
mPanoFailedDialog = (RotateLayout) mRootView.findViewById(R.id.pano_dialog_layout);
mPanoFailedButton = (Button) mRootView.findViewById(R.id.pano_dialog_button1);
mDialogHelper = new DialogHelper();
+ setViews(appRes);
+ }
+
+ private void setViews(Resources appRes) {
+ int weight = appRes.getInteger(R.integer.SRI_pano_layout_weight);
+
+ mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar);
+ mSavingProgressBar.setIndicatorWidth(0);
+ mSavingProgressBar.setMaxProgress(100);
+ mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
+ mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
+
+ View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button);
+ cancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ mController.cancelHighResStitching();
+ }
+ });
+
+
+ }
+
+ private void showTooFastIndication() {
+ mTooFastPrompt.setVisibility(View.VISIBLE);
+ // The PreviewArea also contains the border for "too fast" indication.
+ mPreviewBorder.setVisibility(View.VISIBLE);
+ mCaptureProgressBar.setIndicatorColor(mIndicatorColorFast);
+ mLeftIndicator.setEnabled(true);
+ mRightIndicator.setEnabled(true);
+ }
+
+ private void hideTooFastIndication() {
+ mTooFastPrompt.setVisibility(View.GONE);
+ mPreviewBorder.setVisibility(View.INVISIBLE);
+ mCaptureProgressBar.setIndicatorColor(mIndicatorColor);
+ mLeftIndicator.setEnabled(false);
+ mRightIndicator.setEnabled(false);
+ }
+
+ public void flipPreviewIfNeeded() {
+ // Rotation needed to display image correctly clockwise
+ int cameraOrientation = mController.getCameraOrientation();
+ // Display rotated counter-clockwise
+ int displayRotation = CameraUtil.getDisplayRotation(mActivity);
+ // Rotation needed to display image correctly on current display
+ int rotation = (cameraOrientation - displayRotation + 360) % 360;
+ if (rotation >= 180) {
+ mTextureView.setRotation(180);
+ } else {
+ mTextureView.setRotation(0);
+ }
}
@Override
public void onDisplayChanged() {
mCameraControls.checkLayoutFlip();
+ flipPreviewIfNeeded();
}
public void initDisplayChangeListener() {
@@ -437,12 +540,6 @@ public class WideAnglePanoramaUI implements
mPreviewCover.setVisibility(View.VISIBLE);
}
- public void hidePreviewCover() {
- if (mPreviewCover.getVisibility() != View.GONE) {
- mPreviewCover.setVisibility(View.GONE);
- }
- }
-
private class DialogHelper {
DialogHelper() {
@@ -501,91 +598,7 @@ public class WideAnglePanoramaUI implements
return true;
}
return false;
- }
-
- private int convertDpToPix(int dp) {
- Resources r = mActivity.getResources();
- return ((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
- r.getDisplayMetrics()));
- }
-
-
- public void setPreviewOrientation(int orientation, boolean horiz) {
-
- Point size = new Point();
- mActivity.getWindowManager().getDefaultDisplay().getSize(size);
- int sW = size.x;
- int sH = size.y;
- ViewGroup v = mCapturePreviewLayout;
- int w = v.getWidth();
- int h = v.getHeight();
- int idx1;
- int idx2;
- int botPadding_pix = convertDpToPix(botPadding);
- int sidePadding_pix = convertDpToPix(sidePadding);
-
- final View dummy = mRootView.findViewById(R.id.pano_dummy_layout);
- int t = dummy.getTop();
- int b1 = dummy.getBottom();
- int r = dummy.getRight();
- int b2 = dummy.getBottom();
- int yc = (t + b1) / 2;
- int xc = r / 2;
- int x = xc;
- int y = yc;
- int vH = convertDpToPix(capturePreviewH);
- int vW = convertDpToPix(capturePreviewW);
- if (horiz) {
- v.setPivotX(w / 2);
- v.setPivotY(h / 2);
- switch (orientation) {
- case 90:
- x = sW - vH / 2 - sidePadding_pix;
- y = yc;
- break;
- case 180:
- x = xc;
- y = t / 2;
- break;
- case 270:
- x = vH / 2 + sidePadding_pix;
- y = yc;
- break;
- default:
- x = xc;
- y = sH - botPadding_pix;
- break;
- }
- v.setTranslationX(x - xc);
- v.setTranslationY(y - yc);
- v.setRotation(-orientation);
- } else {
- v.setPivotX(w / 2);
- v.setPivotY(h / 2);
- switch (orientation) {
- case 90:
- x = xc;
- y = sH - botPadding_pix;
- break;
- case 180:
- x = sW - vW / 2 - sidePadding_pix;
- y = yc;
- break;
- case 270:
- x = xc;
- y = t / 2;
-
- break;
- default:
- x = vW / 2 + sidePadding_pix;
- y = yc;
- break;
- }
- v.setTranslationX(x - xc);
- v.setTranslationY(y - yc);
- v.setRotation(-orientation);
- }
- }
+ }
public void setOrientation(int orientation, boolean animation) {
mOrientation = orientation;
@@ -606,7 +619,9 @@ public class WideAnglePanoramaUI implements
int b2 = dummy.getBottom();
final FrameLayout progressLayout = (FrameLayout)
mRootView.findViewById(R.id.pano_progress_layout);
- int pivotY = ((ViewGroup) progressLayout).getPaddingTop();
+ int pivotY = ((ViewGroup) progressLayout).getPaddingTop()
+ + progressLayout.getChildAt(0).getHeight() / 2;
+
int[] x = { r / 2, r / 10, r * 9 / 10, r / 2 };
int[] y = { t / 2, (t + b1) / 2, (t + b1) / 2, b1 + pivotY };
@@ -655,7 +670,7 @@ public class WideAnglePanoramaUI implements
v.setRotation(-orientation);
}
- final View[] views2 = { progressLayout, mReviewControl};
+ final View[] views2 = { progressLayout, mReviewControl };
for (final View v : views2) {
v.setPivotX(r / 2);
v.setPivotY(pivotY);
diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java
index b1b3133db..01469baeb 100644
--- a/src/com/android/camera/ui/FilmStripView.java
+++ b/src/com/android/camera/ui/FilmStripView.java
@@ -1819,6 +1819,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener {
@Override
public boolean onTouchEvent(MotionEvent ev) {
+ mGestureRecognizer.onTouchEvent(ev);
return true;
}