/* * 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.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.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.R; import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.ui.GLRootView; import com.android.gallery3d.util.UsageStatistics; 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 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 LocationManager mLocationManager; private ComboPreferences mPreferences; 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) { if (mMosaicPreviewRenderer == null) { return; } renderer = mMosaicPreviewRenderer; } 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(); mPreferences = new ComboPreferences(mActivity); CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); mLocationManager = new LocationManager(mActivity, null); 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 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 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 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 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(); return; } mIsCreatingRenderer = true; } 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) { 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, true); Resources appRes = mActivity.getResources(); mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app); 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.camera_app_root); 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); Location loc = mLocationManager.getCurrentLocation(); ExifInterface exif = new ExifInterface(); try { exif.readExif(jpegData); exif.addGpsDateTimeStampTag(mTimeTaken); exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken, TimeZone.getDefault()); exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, ExifInterface.getOrientationValueForRotation(orientation))); writeLocation(loc, exif); exif.writeExif(jpegData, filepath); } catch (IOException e) { Log.e(TAG, "Cannot set exif for " + filepath, e); Storage.writeFile(filepath, jpegData); } int jpegLength = (int) (new File(filepath).length()); return Storage.addImage(mContentResolver, filename, mTimeTaken, loc, orientation, jpegLength, filepath, width, height); } return null; } private static void writeLocation(Location location, ExifInterface exif) { if (location == null) { return; } exif.addGpsTags(location.getLatitude(), location.getLongitude()); 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; mMosaicFrameProcessor.initialize( mPreviewWidth, mPreviewHeight, getPreviewBufSize()); mMosaicFrameProcessorInitialized = true; } @Override public void onPauseBeforeSuper() { mPaused = true; if (mLocationManager != null) mLocationManager.recordLocation(false); } @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(); // Initialize location service. boolean recordLocation = RecordLocationPreference.get(mPreferences, mContentResolver); mLocationManager.recordLocation(recordLocation); // Dismiss open menu if exists. PopupManager.getInstance(mActivity).notifyShowPopup(null); mRootView.requestLayout(); UsageStatistics.onContentViewChanged( UsageStatistics.COMPONENT_CAMERA, "PanoramaModule"); } /** * 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 { @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 needsSwitcher() { return true; } @Override public boolean needsPieMenu() { return false; } @Override public void onShowSwitcherPopup() { } @Override public void onMediaSaveServiceConnected(MediaSaveService s) { // do nothing. } }