diff options
Diffstat (limited to 'src/com/android/camera/WideAnglePanoramaModule.java')
-rw-r--r-- | src/com/android/camera/WideAnglePanoramaModule.java | 1071 |
1 files changed, 1071 insertions, 0 deletions
diff --git a/src/com/android/camera/WideAnglePanoramaModule.java b/src/com/android/camera/WideAnglePanoramaModule.java new file mode 100644 index 000000000..1756e4c70 --- /dev/null +++ b/src/com/android/camera/WideAnglePanoramaModule.java @@ -0,0 +1,1071 @@ +/* + * 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.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.ImageFormat; +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.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.app.OrientationManager; +import com.android.camera.exif.ExifInterface; +import com.android.camera.ui.PopupManager; +import com.android.camera.util.CameraUtil; +import com.android.camera.util.UsageStatistics; +import com.android.camera2.R; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.TimeZone; + +/** + * Activity to handle panorama capturing. + */ +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; + 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_RESET_TO_PREVIEW = 5; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + @SuppressWarnings("unused") + private static final String TAG = "CAM_WidePanoModule"; + private static final int PREVIEW_STOPPED = 0; + private static final int PREVIEW_ACTIVE = 1; + 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 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 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 SoundClips.Player mSoundPlayer; + + private Runnable mOnFrameAvailableRunnable; + + private CameraActivity mActivity; + private View mRootView; + private CameraProxy mCameraDevice; + private boolean mPaused; + + private LocationManager mLocationManager; + private OrientationManager mOrientationManager; + private ComboPreferences mPreferences; + private boolean mMosaicPreviewConfigured; + + @Override + public void onPreviewUIReady() { + configMosaicPreview(); + } + + @Override + public void onPreviewUIDestroyed() { + + } + + 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 = CameraUtil.roundOrientation(orientation, mDeviceOrientation); + // When the screen is unlocked, display rotation may change. Always + // calculate the up-to-date orientationCompensation. + int orientationCompensation = mDeviceOrientation + + CameraUtil.getDisplayRotation(mActivity) % 360; + if (mOrientationCompensation != orientationCompensation) { + mOrientationCompensation = orientationCompensation; + } + } + } + + @Override + public void init(CameraActivity activity, View parent) { + mActivity = activity; + mRootView = parent; + + 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 (mCaptureState == CAPTURE_STATE_MOSAIC) { + mUI.showDirectionIndicators(direction); + } + } + }); + + 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) { + 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()); + 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) { + 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. + FLAG_KEEP_SCREEN_ON); + break; + case MSG_RESET_TO_PREVIEW: + resetToPreviewIfPossible(); + break; + } + } + }; + } + + @Override + public void onSwitchMode(boolean toCamera) { + if (toCamera) { + mUI.showUI(); + } else { + mUI.hideUI(); + } + } + + private void setupCamera() throws CameraHardwareException, CameraDisabledException { + openCamera(); + Parameters parameters = mCameraDevice.getParameters(); + setupCaptureParams(parameters); + configureCamera(parameters); + } + + private void releaseCamera() { + if (mCameraDevice != 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 = CameraUtil.openCamera(mActivity, cameraId); + mCameraOrientation = CameraUtil.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) { + mCameraPreviewWidth = w; + mCameraPreviewHeight = 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.d(TAG, "camera preview h = " + + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth); + parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight); + + 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.d(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(CameraUtil.RECORDING_HINT, CameraUtil.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 (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; + } + + private void configureCamera(Parameters parameters) { + mCameraDevice.setParameters(parameters); + } + + /** + * 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 = + (mActivity.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE); + + MosaicPreviewRenderer renderer = new MosaicPreviewRenderer( + mUI.getSurfaceTexture(), + mPreviewUIWidth, mPreviewUIHeight, isLandscape); + 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. + */ + @Override + public void onPreviewUILayoutChange(int l, int t, int r, int b) { + Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t)); + mPreviewUIWidth = r - l; + mPreviewUIHeight = b - t; + configMosaicPreview(); + } + + @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(); + + 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; + 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(); + // TODO: mActivity.getOrientationManager().lockOrientation(); + mOrientationManager.lockOrientation(); + int degrees = CameraUtil.getDisplayRotation(mActivity); + int cameraId = CameraHolder.instance().getBackCameraId(); + int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId); + mUI.setProgressOrientation(orientation); + } + + private void stopCapture(boolean aborted) { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mUI.onStopCapture(); + + mMosaicFrameProcessor.setProgressListener(null); + stopCameraPreview(); + + 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 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); + } + } + + 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 + // 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(); + 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; + + mOrientationManager.unlockOrientation(); + mUI.reset(); + mActivity.setSwipingEnabled(true); + // Orientation change will trigger onLayoutChange->configMosaicPreview-> + // resetToPreview. Do not show the capture UI in film strip. + /* if (mActivity.mShowCameraAppView) { + mCaptureLayout.setVisibility(View.VISIBLE); */ + mUI.showPreviewUI(); + /*} else { + }*/ + mMosaicFrameProcessor.reset(); + } + + private void resetToPreviewIfPossible() { + if (!mMosaicFrameProcessorInitialized + || mUI.getSurfaceTexture() == null + || !mMosaicPreviewConfigured) { + return; + } + reset(); + if (!mPaused) startCameraPreview(); + } + + private void showFinalMosaic(Bitmap bitmap) { + mUI.showFinalMosaic(bitmap, getCaptureOrientation()); + } + + 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( + mCameraPreviewWidth, mCameraPreviewHeight, 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; + } + System.gc(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + mUI.onConfigurationChanged(newConfig, mThreadRunning); + } + + @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) { + CameraUtil.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + CameraUtil.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. + mUI.dismissAllDialogs(); + 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(); + } + keepScreenOnAwhile(); + + // Initialize location service. + boolean recordLocation = RecordLocationPreference.get(mPreferences, + mContentResolver); + mLocationManager.recordLocation(recordLocation); + + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + 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.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() { + if (mCameraDevice == null) { + // Camera open failed. Return. + return; + } + + if (mUI.getSurfaceTexture() == null) { + // UI is not ready. + 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 mCameraPreviewWidth x mCameraPreviewHeight + // image data from SurfaceTexture. + mCameraDevice.setDisplayOrientation(0); + + mCameraTexture.setOnFrameAvailableListener(this); + mCameraDevice.setPreviewTexture(mCameraTexture); + } + mCameraDevice.startPreview(); + mCameraState = PREVIEW_ACTIVE; + } + + private void stopCameraPreview() { + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + 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; + mUI.dismissAllDialogs(); + // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE); + initMosaicFrameProcessorIfNeeded(); + Point size = mUI.getPreviewAreaSize(); + mPreviewUIWidth = size.x; + mPreviewUIHeight = size.y; + configMosaicPreview(); + resetToPreviewIfPossible(); + } + } + + @Override + public void cancelHighResStitching() { + if (mPaused || mCameraTexture == null) return; + cancelHighResComputation(); + } + + @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 void onShowSwitcherPopup() { + } + + @Override + public void onMediaSaveServiceConnected(MediaSaveService s) { + // do nothing. + } +} |