diff options
Diffstat (limited to 'src/com/android/camera/PanoramaModule.java')
-rw-r--r-- | src/com/android/camera/PanoramaModule.java | 1312 |
1 files changed, 1312 insertions, 0 deletions
diff --git a/src/com/android/camera/PanoramaModule.java b/src/com/android/camera/PanoramaModule.java new file mode 100644 index 000000000..18290872e --- /dev/null +++ b/src/com/android/camera/PanoramaModule.java @@ -0,0 +1,1312 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.graphics.YuvImage; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +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.LayoutInflater; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.ui.LayoutChangeNotifier; +import com.android.camera.ui.LayoutNotifyView; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.Rotatable; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifData; +import com.android.gallery3d.exif.ExifInvalidFormatException; +import com.android.gallery3d.exif.ExifOutputStream; +import com.android.gallery3d.exif.ExifReader; +import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.ui.GLRootView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.TimeZone; + +/** + * Activity to handle panorama capturing. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture +public class PanoramaModule implements CameraModule, + SurfaceTexture.OnFrameAvailableListener, + ShutterButton.OnShutterButtonListener, + LayoutChangeNotifier.Listener { + + 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; + private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2; + private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3; + private static final int MSG_CLEAR_SCREEN_DELAY = 4; + private static final int MSG_CONFIG_MOSAIC_PREVIEW = 5; + private static final int MSG_RESET_TO_PREVIEW = 6; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + private static final String TAG = "CAM PanoModule"; + private static final int PREVIEW_STOPPED = 0; + private static final int PREVIEW_ACTIVE = 1; + private static final int CAPTURE_STATE_VIEWFINDER = 0; + private 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 ContentResolver mContentResolver; + + private GLRootView mGLRootView; + private ViewGroup mPanoLayout; + private LinearLayout mCaptureLayout; + private View mReviewLayout; + private ImageView mReview; + private View mCaptureIndicator; + private PanoProgressBar mPanoProgressBar; + private PanoProgressBar mSavingProgressBar; + private Matrix mProgressDirectionMatrix = new Matrix(); + private float[] mProgressAngle = new float[2]; + private LayoutNotifyView mPreviewArea; + private View mLeftIndicator; + private View mRightIndicator; + private MosaicPreviewRenderer mMosaicPreviewRenderer; + private Object mRendererLock = new Object(); + private TextView mTooFastPrompt; + private ShutterButton mShutterButton; + private Object mWaitObject = new Object(); + + private String mPreparePreviewString; + private String mDialogTitle; + private String mDialogOkString; + private String mDialogPanoramaFailedString; + private String mDialogWaitingPreviousString; + + private int mIndicatorColor; + private int mIndicatorColorFast; + private int mReviewBackground; + + private boolean mUsingFrontCamera; + private int mPreviewWidth; + private int mPreviewHeight; + 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 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; + + private PanoOrientationEventListener mOrientationEventListener; + // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise + // respectively. + private int mDeviceOrientation; + private int mDeviceOrientationAtCapture; + private int mCameraOrientation; + private int mOrientationCompensation; + + private RotateDialogController mRotateDialog; + + private SoundClips.Player mSoundPlayer; + + private Runnable mOnFrameAvailableRunnable; + + private CameraActivity mActivity; + private View mRootView; + private CameraProxy mCameraDevice; + private boolean mPaused; + private boolean mIsCreatingRenderer; + private boolean mIsConfigPending; + + private class MosaicJpeg { + public MosaicJpeg(byte[] data, int width, int height) { + this.data = data; + this.width = width; + this.height = height; + this.isValid = true; + } + + public MosaicJpeg() { + this.data = null; + this.width = 0; + this.height = 0; + this.isValid = false; + } + + public final byte[] data; + public final int width; + public final int height; + public final boolean isValid; + } + + private class PanoOrientationEventListener extends OrientationEventListener { + public PanoOrientationEventListener(Context context) { + super(context); + } + + @Override + public void onOrientationChanged(int orientation) { + // We keep the last known orientation. So if the user first orient + // the camera then point the camera to floor or sky, we still have + // the correct orientation. + if (orientation == ORIENTATION_UNKNOWN) return; + mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation); + // When the screen is unlocked, display rotation may change. Always + // calculate the up-to-date orientationCompensation. + int orientationCompensation = mDeviceOrientation + + Util.getDisplayRotation(mActivity) % 360; + if (mOrientationCompensation != orientationCompensation) { + mOrientationCompensation = orientationCompensation; + mActivity.getGLRoot().requestLayoutContentPane(); + } + } + } + + @Override + public void init(CameraActivity activity, View parent, boolean reuseScreenNail) { + mActivity = activity; + mRootView = parent; + + createContentView(); + + mContentResolver = mActivity.getContentResolver(); + if (reuseScreenNail) { + mActivity.reuseCameraScreenNail(true); + } else { + mActivity.createCameraScreenNail(true); + } + + // 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) { + try { + while (mMosaicPreviewRenderer == null) { + mRendererLock.wait(); + } + renderer = mMosaicPreviewRenderer; + } catch (InterruptedException e) { + Log.e(TAG, "Unexpected interruption", e); + } + } + if (mGLRootView.getVisibility() != View.VISIBLE) { + renderer.showPreviewFrameSync(); + mGLRootView.setVisibility(View.VISIBLE); + } else { + if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { + 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); + + mGLRootView = (GLRootView) mActivity.getGLRoot(); + + mMainHandler = new Handler() { + @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) { + resetToPreview(); + } else { + mRotateDialog.showAlertDialog( + mDialogTitle, mDialogPanoramaFailedString, + mDialogOkString, new Runnable() { + @Override + public void run() { + resetToPreview(); + }}, + null, null); + } + clearMosaicFrameProcessorIfNeeded(); + break; + case MSG_END_DIALOG_RESET_TO_PREVIEW: + onBackgroundThreadFinished(); + resetToPreview(); + clearMosaicFrameProcessorIfNeeded(); + break; + case MSG_CLEAR_SCREEN_DELAY: + mActivity.getWindow().clearFlags(WindowManager.LayoutParams. + FLAG_KEEP_SCREEN_ON); + break; + case MSG_CONFIG_MOSAIC_PREVIEW: + configMosaicPreview(msg.arg1, msg.arg2); + break; + case MSG_RESET_TO_PREVIEW: + resetToPreview(); + break; + } + } + }; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + return mActivity.superDispatchTouchEvent(m); + } + + private void setupCamera() throws CameraHardwareException, CameraDisabledException { + openCamera(); + Parameters parameters = mCameraDevice.getParameters(); + setupCaptureParams(parameters); + configureCamera(parameters); + } + + private void releaseCamera() { + if (mCameraDevice != null) { + mCameraDevice.setPreviewCallbackWithBuffer(null); + CameraHolder.instance().release(); + mCameraDevice = null; + mCameraState = PREVIEW_STOPPED; + } + } + + private void openCamera() throws CameraHardwareException, CameraDisabledException { + int cameraId = CameraHolder.instance().getBackCameraId(); + // If there is no back camera, use the first camera. Camera id starts + // from 0. Currently if a camera is not back facing, it is front facing. + // This is also forward compatible if we have a new facing other than + // back or front in the future. + if (cameraId == -1) cameraId = 0; + mCameraDevice = Util.openCamera(mActivity, cameraId); + mCameraOrientation = Util.getCameraOrientation(cameraId); + if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true; + } + + private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, + boolean needSmaller) { + int pixelsDiff = DEFAULT_CAPTURE_PIXELS; + boolean hasFound = false; + for (Size size : supportedSizes) { + int h = size.height; + int w = size.width; + // we only want 4:3 format. + int d = DEFAULT_CAPTURE_PIXELS - h * w; + if (needSmaller && d < 0) { // no bigger preview than 960x720. + continue; + } + if (need4To3 && (h * 4 != w * 3)) { + continue; + } + d = Math.abs(d); + if (d < pixelsDiff) { + mPreviewWidth = w; + mPreviewHeight = h; + pixelsDiff = d; + hasFound = true; + } + } + return hasFound; + } + + private void setupCaptureParams(Parameters parameters) { + List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); + if (!findBestPreviewSize(supportedSizes, true, true)) { + Log.w(TAG, "No 4:3 ratio preview size supported."); + if (!findBestPreviewSize(supportedSizes, false, true)) { + Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); + findBestPreviewSize(supportedSizes, false, false); + } + } + Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); + parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); + + List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); + int last = frameRates.size() - 1; + int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; + int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; + parameters.setPreviewFpsRange(minFps, maxFps); + Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); + + List<String> supportedFocusModes = parameters.getSupportedFocusModes(); + if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) { + parameters.setFocusMode(mTargetFocusMode); + } else { + // Use the default focus mode and log a message + Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode + + " becuase the mode is not supported."); + } + + parameters.set(Util.RECORDING_HINT, Util.FALSE); + + mHorizontalViewAngle = parameters.getHorizontalViewAngle(); + mVerticalViewAngle = parameters.getVerticalViewAngle(); + } + + public int getPreviewBufSize() { + PixelFormat pixelInfo = new PixelFormat(); + PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); + // TODO: remove this extra 32 byte after the driver bug is fixed. + return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; + } + + private void configureCamera(Parameters parameters) { + mCameraDevice.setParameters(parameters); + } + + private void configMosaicPreview(final int w, final int h) { + synchronized (mRendererLock) { + if (mIsCreatingRenderer) { + mMainHandler.removeMessages(MSG_CONFIG_MOSAIC_PREVIEW); + mMainHandler.obtainMessage(MSG_CONFIG_MOSAIC_PREVIEW, w, h).sendToTarget(); + mIsConfigPending = true; + return; + } + mIsCreatingRenderer = true; + mIsConfigPending = false; + } + stopCameraPreview(); + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + screenNail.setSize(w, h); + synchronized (mRendererLock) { + if (mMosaicPreviewRenderer != null) { + mMosaicPreviewRenderer.release(); + } + mMosaicPreviewRenderer = null; + screenNail.releaseSurfaceTexture(); + screenNail.acquireSurfaceTexture(); + } + mActivity.notifyScreenNailChanged(); + final boolean isLandscape = (mActivity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); + new Thread(new Runnable() { + @Override + public void run() { + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + SurfaceTexture surfaceTexture = screenNail.getSurfaceTexture(); + if (surfaceTexture == null) { + synchronized (mRendererLock) { + mIsConfigPending = true; // try config again later. + mIsCreatingRenderer = false; + mRendererLock.notifyAll(); + return; + } + } + MosaicPreviewRenderer renderer = new MosaicPreviewRenderer( + screenNail.getSurfaceTexture(), w, h, isLandscape); + synchronized (mRendererLock) { + mMosaicPreviewRenderer = renderer; + mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture(); + + if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) { + mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); + } + mIsCreatingRenderer = false; + mRendererLock.notifyAll(); + } + } + }).start(); + } + + // Receives the layout change event from the preview area. So we can set + // the camera preview screennail to the same size and initialize the mosaic + // preview renderer. + @Override + public void onLayoutChange(View v, int l, int t, int r, int b) { + Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t)); + mActivity.onLayoutChange(v, l, t, r, b); + configMosaicPreview(r - l, b - t); + } + + @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); + } + + private void hideDirectionIndicators() { + mLeftIndicator.setVisibility(View.GONE); + mRightIndicator.setVisibility(View.GONE); + } + + private 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.GONE); + break; + case PanoProgressBar.DIRECTION_RIGHT: + mLeftIndicator.setVisibility(View.GONE); + mRightIndicator.setVisibility(View.VISIBLE); + break; + } + } + + public void startCapture() { + // Reset values so we can do this again. + mCancelComputation = false; + mTimeTaken = System.currentTimeMillis(); + mActivity.setSwipingEnabled(false); + mActivity.hideSwitcher(); + mShutterButton.setImageResource(R.drawable.btn_shutter_recording); + mCaptureState = CAPTURE_STATE_MOSAIC; + mCaptureIndicator.setVisibility(View.VISIBLE); + showDirectionIndicators(PanoProgressBar.DIRECTION_NONE); + + 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; + if (isFinished + || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE) + || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) { + stopCapture(false); + } else { + float panningRateXInDegree = panningRateX * mHorizontalViewAngle; + float panningRateYInDegree = panningRateY * mVerticalViewAngle; + updateProgress(panningRateXInDegree, panningRateYInDegree, + accumulatedHorizontalAngle, accumulatedVerticalAngle); + } + } + }); + + mPanoProgressBar.reset(); + // TODO: calculate the indicator width according to different devices to reflect the actual + // angle of view of the camera device. + mPanoProgressBar.setIndicatorWidth(20); + mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); + mPanoProgressBar.setVisibility(View.VISIBLE); + mDeviceOrientationAtCapture = mDeviceOrientation; + keepScreenOn(); + mActivity.getOrientationManager().lockOrientation(); + setupProgressDirectionMatrix(); + } + + void setupProgressDirectionMatrix() { + int degrees = Util.getDisplayRotation(mActivity); + int cameraId = CameraHolder.instance().getBackCameraId(); + int orientation = Util.getDisplayOrientation(degrees, cameraId); + mProgressDirectionMatrix.reset(); + mProgressDirectionMatrix.postRotate(orientation); + } + + private void stopCapture(boolean aborted) { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mCaptureIndicator.setVisibility(View.GONE); + hideTooFastIndication(); + hideDirectionIndicators(); + + mMosaicFrameProcessor.setProgressListener(null); + stopCameraPreview(); + + mCameraTexture.setOnFrameAvailableListener(null); + + if (!aborted && !mThreadRunning) { + mRotateDialog.showWaitingDialog(mPreparePreviewString); + // Hide shutter button, shutter icon, etc when waiting for + // panorama to stitch + mActivity.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(); + } + + private void showTooFastIndication() { + mTooFastPrompt.setVisibility(View.VISIBLE); + // The PreviewArea also contains the border for "too fast" indication. + mPreviewArea.setVisibility(View.VISIBLE); + mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); + mLeftIndicator.setEnabled(true); + mRightIndicator.setEnabled(true); + } + + private void hideTooFastIndication() { + mTooFastPrompt.setVisibility(View.GONE); + // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout + // information so we can know the size and position for mCameraScreenNail. + mPreviewArea.setVisibility(View.INVISIBLE); + mPanoProgressBar.setIndicatorColor(mIndicatorColor); + mLeftIndicator.setEnabled(false); + mRightIndicator.setEnabled(false); + } + + private void updateProgress(float panningRateXInDegree, float panningRateYInDegree, + float progressHorizontalAngle, float progressVerticalAngle) { + mGLRootView.requestRender(); + + if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD) + || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) { + showTooFastIndication(); + } else { + hideTooFastIndication(); + } + + // 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]; + mPanoProgressBar.setProgress((angleInMajorDirection)); + } + + private void setViews(Resources appRes) { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar); + mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); + mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); + mPanoProgressBar.setIndicatorColor(mIndicatorColor); + mPanoProgressBar.setOnDirectionChangeListener( + new PanoProgressBar.OnDirectionChangeListener () { + @Override + public void onDirectionChange(int direction) { + if (mCaptureState == CAPTURE_STATE_MOSAIC) { + showDirectionIndicators(direction); + } + } + }); + + 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); + // This mPreviewArea also shows the border for visual "too fast" indication. + mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area); + mPreviewArea.setOnLayoutChangeListener(this); + + 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)); + + mCaptureIndicator = mRootView.findViewById(R.id.pano_capture_indicator); + + mReviewLayout = mRootView.findViewById(R.id.pano_review_layout); + mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea); + mReview.setBackgroundColor(mReviewBackground); + View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button); + cancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + if (mPaused || mCameraTexture == null) return; + cancelHighResComputation(); + } + }); + + mShutterButton = mActivity.getShutterButton(); + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mShutterButton.setOnShutterButtonListener(this); + + if (mActivity.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT) { + Rotatable view = (Rotatable) mRootView.findViewById(R.id.pano_rotate_reviewarea); + view.setOrientation(270, false); + } + } + + private void createContentView() { + mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView); + Resources appRes = mActivity.getResources(); + mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root); + mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); + mReviewBackground = appRes.getColor(R.color.review_background); + mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); + mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout); + mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog); + setViews(appRes); + } + + @Override + public void onShutterButtonClick() { + // 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 + // right now. + switch (mCaptureState) { + case CAPTURE_STATE_VIEWFINDER: + if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return; + mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); + startCapture(); + break; + case CAPTURE_STATE_MOSAIC: + mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); + stopCapture(false); + } + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + } + + public void reportProgress() { + mSavingProgressBar.reset(); + mSavingProgressBar.setRightIncreasing(true); + 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() { + mSavingProgressBar.setProgress(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 + // in its EXIF header, so the image can be displayed correctly. + // The orientation is calculated from compensating the + // device orientation at capture and the camera orientation respective to + // the natural orientation of the device. + int orientation; + if (mUsingFrontCamera) { + // mCameraOrientation is negative with respect to the front facing camera. + // See document of android.hardware.Camera.Parameters.setRotation. + orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360; + } else { + orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360; + } + return orientation; + } + + 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(); + Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); + if (uri != null) { + mActivity.addSecureAlbumItemIfNeeded(false, uri); + Util.broadcastNewPicture(mActivity, 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; + mRotateDialog.dismissDialog(); + } + + 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; + + mActivity.getOrientationManager().unlockOrientation(); + // We should set mGLRootView visible too. However, since there might be no + // frame available yet, setting mGLRootView visible should be done right after + // the first camera frame is available and therefore it is done by + // mOnFirstFrameAvailableRunnable. + mActivity.setSwipingEnabled(true); + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mReviewLayout.setVisibility(View.GONE); + mPanoProgressBar.setVisibility(View.GONE); + mGLRootView.setVisibility(View.VISIBLE); + // Orientation change will trigger onLayoutChange->configMosaicPreview-> + // resetToPreview. Do not show the capture UI in film strip. + if (mActivity.mShowCameraAppView) { + mCaptureLayout.setVisibility(View.VISIBLE); + mActivity.showUI(); + } + mMosaicFrameProcessor.reset(); + } + + private void resetToPreview() { + reset(); + if (!mPaused) startCameraPreview(); + } + + private static class FlipBitmapDrawable extends BitmapDrawable { + + public FlipBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + int cx = bounds.centerX(); + int cy = bounds.centerY(); + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.rotate(180, cx, cy); + super.draw(canvas); + canvas.restore(); + } + } + + private void showFinalMosaic(Bitmap bitmap) { + if (bitmap != null) { + int orientation = getCaptureOrientation(); + if (orientation >= 180) { + // We need to flip the drawable to compensate + mReview.setImageDrawable(new FlipBitmapDrawable( + mActivity.getResources(), bitmap)); + } else { + mReview.setImageBitmap(bitmap); + } + } + + mCaptureLayout.setVisibility(View.GONE); + mReviewLayout.setVisibility(View.VISIBLE); + } + + private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { + if (jpegData != null) { + String filename = PanoUtil.createName( + mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); + String filepath = Storage.generateFilepath(filename); + + ExifOutputStream out = null; + InputStream is = null; + try { + is = new ByteArrayInputStream(jpegData); + ExifReader reader = new ExifReader(); + ExifData data = reader.read(is); + + // Add Exif tags. + data.addGpsDateTimeStampTag(mTimeTaken); + data.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, mTimeTaken, TimeZone.getDefault()); + data.addTag(ExifTag.TAG_ORIENTATION). + setValue(getExifOrientation(orientation)); + + out = new ExifOutputStream(new FileOutputStream(filepath)); + out.setExifData(data); + out.write(jpegData); + } catch (IOException e) { + Log.e(TAG, "Cannot set EXIF for " + filepath, e); + Storage.writeFile(filepath, jpegData); + } catch (ExifInvalidFormatException e) { + Log.e(TAG, "Cannot set EXIF for " + filepath, e); + Storage.writeFile(filepath, jpegData); + } finally { + Util.closeSilently(out); + Util.closeSilently(is); + } + + int jpegLength = (int) (new File(filepath).length()); + return Storage.addImage(mContentResolver, filename, mTimeTaken, + null, orientation, jpegLength, filepath, width, height); + } + return null; + } + + private static int getExifOrientation(int orientation) { + switch (orientation) { + case 0: + return ExifTag.Orientation.TOP_LEFT; + case 90: + return ExifTag.Orientation.RIGHT_TOP; + case 180: + return ExifTag.Orientation.BOTTOM_LEFT; + case 270: + return ExifTag.Orientation.RIGHT_BOTTOM; + default: + throw new AssertionError("invalid: " + orientation); + } + } + + 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; + mMosaicFrameProcessor.initialize( + mPreviewWidth, mPreviewHeight, getPreviewBufSize()); + mMosaicFrameProcessorInitialized = true; + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + } + + @Override + public void onPauseAfterSuper() { + mOrientationEventListener.disable(); + if (mCameraDevice == null) { + // Camera open failed. Nothing should be done here. + return; + } + // Stop the capturing first. + if (mCaptureState == CAPTURE_STATE_MOSAIC) { + stopCapture(true); + reset(); + } + + releaseCamera(); + 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(); + if (mSoundPlayer != null) { + mSoundPlayer.release(); + mSoundPlayer = null; + } + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + screenNail.releaseSurfaceTexture(); + System.gc(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + + Drawable lowResReview = null; + if (mThreadRunning) lowResReview = mReview.getDrawable(); + + // Change layout in response to configuration change + mCaptureLayout.setOrientation( + newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE + ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + mCaptureLayout.removeAllViews(); + LayoutInflater inflater = mActivity.getLayoutInflater(); + inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout); + + mPanoLayout.removeView(mReviewLayout); + inflater.inflate(R.layout.pano_review, mPanoLayout); + + setViews(mActivity.getResources()); + if (mThreadRunning) { + mReview.setImageDrawable(lowResReview); + mCaptureLayout.setVisibility(View.GONE); + mReviewLayout.setVisibility(View.VISIBLE); + } + } + + @Override + public void onOrientationChanged(int orientation) { + } + + @Override + public void onResumeBeforeSuper() { + mPaused = false; + } + + @Override + public void onResumeAfterSuper() { + mOrientationEventListener.enable(); + + mCaptureState = CAPTURE_STATE_VIEWFINDER; + + try { + setupCamera(); + } catch (CameraHardwareException e) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + + // Set up sound playback for shutter button + mSoundPlayer = SoundClips.getPlayer(mActivity); + + // Check if another panorama instance is using the mosaic frame processor. + mRotateDialog.dismissDialog(); + if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { + mGLRootView.setVisibility(View.GONE); + mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString); + // If stitching is still going on, make sure switcher and shutter button + // are not showing + mActivity.hideUI(); + mWaitProcessorTask = new WaitProcessorTask().execute(); + } else { + mGLRootView.setVisibility(View.VISIBLE); + // Camera must be initialized before MosaicFrameProcessor is + // initialized. The preview size has to be decided by camera device. + initMosaicFrameProcessorIfNeeded(); + int w = mPreviewArea.getWidth(); + int h = mPreviewArea.getHeight(); + if (w != 0 && h != 0) { // The layout has been calculated. + configMosaicPreview(w, h); + } + } + keepScreenOnAwhile(); + + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + mRootView.requestLayout(); + } + + /** + * 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(); + } + + 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.v(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() { + if (mCameraDevice == null) { + // Camera open failed. Return. + return; + } + + // 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 mPreviewWidth x mPreviewHeight + // image data from SurfaceTexture. + mCameraDevice.setDisplayOrientation(0); + + mCameraTexture.setOnFrameAvailableListener(this); + mCameraDevice.setPreviewTextureAsync(mCameraTexture); + } + mCameraDevice.startPreviewAsync(); + mCameraState = PREVIEW_ACTIVE; + } + + private void stopCameraPreview() { + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + Log.v(TAG, "stopPreview"); + mCameraDevice.stopPreview(); + } + mCameraState = PREVIEW_STOPPED; + } + + @Override + public void onUserInteraction() { + if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); + } + + @Override + 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; + return false; + } + + private void resetScreenOn() { + mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void keepScreenOnAwhile() { + mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); + } + + private void keepScreenOn() { + mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); + 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 + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + mWaitProcessorTask = null; + mRotateDialog.dismissDialog(); + mGLRootView.setVisibility(View.VISIBLE); + initMosaicFrameProcessorIfNeeded(); + int w = mPreviewArea.getWidth(); + int h = mPreviewArea.getHeight(); + if (w != 0 && h != 0) { // The layout has been calculated. + configMosaicPreview(w, h); + } + resetToPreview(); + } + } + + @Override + public void onFullScreenChanged(boolean full) { + } + + + @Override + public void onStop() { + } + + @Override + public void installIntentFilter() { + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + @Override + public void onSingleTapUp(View view, int x, int y) { + } + + @Override + public void onPreviewTextureCopied() { + } + + @Override + public void onCaptureTextureCopied() { + } + + @Override + public boolean updateStorageHintOnResume() { + return false; + } + + @Override + public void updateCameraAppView() { + } + + @Override + public boolean collapseCameraControls() { + return false; + } + + @Override + public boolean needsSwitcher() { + return true; + } + + @Override + public void onShowSwitcherPopup() { + } +} |