diff options
author | Byunghun Jeon <bjeon@codeaurora.org> | 2015-08-03 17:42:49 -0700 |
---|---|---|
committer | Gerrit - the friendly Code Review server <code-review@localhost> | 2015-08-12 18:03:03 -0700 |
commit | e713237ec94737904e339ce05a6d09fed5d9a72d (patch) | |
tree | 1ba6ce4732747a79e382125298a78ec40c376419 /src | |
parent | 4cc34823df8e420c366d4bf4497d7f906676a022 (diff) | |
download | android_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.java | 206 | ||||
-rw-r--r-- | src/com/android/camera/MosaicFrameProcessor.java | 268 | ||||
-rw-r--r-- | src/com/android/camera/MosaicPreviewRenderer.java | 207 | ||||
-rw-r--r-- | src/com/android/camera/MosaicRenderer.java | 101 | ||||
-rw-r--r-- | src/com/android/camera/WideAnglePanoramaModule.java | 786 | ||||
-rw-r--r-- | src/com/android/camera/WideAnglePanoramaUI.java | 397 | ||||
-rw-r--r-- | src/com/android/camera/ui/FilmStripView.java | 1 |
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; } |