| /* |
| * 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.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; |
| private boolean mPreviewFocused; |
| |
| @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 onPreviewFocusChanged(boolean previewFocused) { |
| mPreviewFocused = previewFocused; |
| mUI.onPreviewFocusChanged(previewFocused); |
| } |
| |
| @Override |
| public boolean arePreviewControlsVisible() { |
| return mUI.arePreviewControlsVisible(); |
| } |
| |
| /** |
| * Opens camera and sets the parameters. |
| * |
| * @return Whether the camera was opened successfully. |
| */ |
| private boolean setupCamera() { |
| if (!openCamera()) { |
| return false; |
| } |
| Parameters parameters = mCameraDevice.getParameters(); |
| setupCaptureParams(parameters); |
| configureCamera(parameters); |
| return true; |
| } |
| |
| private void releaseCamera() { |
| if (mCameraDevice != null) { |
| CameraHolder.instance().release(); |
| mCameraDevice = null; |
| mCameraState = PREVIEW_STOPPED; |
| } |
| } |
| |
| /** |
| * Opens the camera device. The back camera has priority over the front |
| * one. |
| * |
| * @return Whether the camera was opened successfully. |
| */ |
| private boolean openCamera() { |
| 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, |
| mMainHandler, mActivity.getCameraOpenErrorCallback()); |
| if (mCameraDevice == null) { |
| return false; |
| } |
| mCameraOrientation = CameraUtil.getCameraOrientation(cameraId); |
| if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true; |
| return 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); |
| mUI.flipPreviewIfNeeded(); |
| 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: |
| final long storageSpaceBytes = mActivity.getStorageSpaceBytes(); |
| if(storageSpaceBytes <= Storage.LOW_STORAGE_THRESHOLD_BYTES) { |
| Log.w(TAG, "Low storage warning: " + storageSpaceBytes); |
| return; |
| } |
| mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); |
| startCapture(); |
| break; |
| case CAPTURE_STATE_MOSAIC: |
| mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); |
| stopCapture(false); |
| break; |
| default: |
| Log.w(TAG, "Unknown capture state: " + mCaptureState); |
| break; |
| } |
| } |
| |
| 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; |
| } |
| |
| /** The orientation of the camera image. The value is the angle that the camera |
| * image needs to be rotated clockwise so it shows correctly on the display |
| * in its natural orientation. It should be 0, 90, 180, or 270.*/ |
| public int getCameraOrientation() { |
| return mCameraOrientation; |
| } |
| |
| public void saveHighResMosaic() { |
| runBackgroundThread(new Thread() { |
| @Override |
| public void run() { |
| mPartialWakeLock.acquire(); |
| MosaicJpeg jpeg; |
| try { |
| jpeg = generateFinalMosaic(true); |
| } finally { |
| mPartialWakeLock.release(); |
| } |
| |
| if (jpeg == null) { // Cancelled by user. |
| mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW); |
| } else if (!jpeg.isValid) { // Error when generating mosaic. |
| mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); |
| } else { |
| int orientation = getCaptureOrientation(); |
| final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); |
| if (uri != null) { |
| mActivity.runOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mActivity.notifyNewMedia(uri); |
| } |
| }); |
| } |
| mMainHandler.sendMessage( |
| mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW)); |
| } |
| } |
| }); |
| reportProgress(); |
| } |
| |
| private void runBackgroundThread(Thread thread) { |
| mThreadRunning = true; |
| thread.start(); |
| } |
| |
| private void onBackgroundThreadFinished() { |
| mThreadRunning = false; |
| mUI.dismissAllDialogs(); |
| } |
| |
| private void cancelHighResComputation() { |
| mCancelComputation = true; |
| synchronized (mWaitObject) { |
| mWaitObject.notify(); |
| } |
| } |
| |
| // This function will be called upon the first camera frame is available. |
| private void reset() { |
| mCaptureState = CAPTURE_STATE_VIEWFINDER; |
| |
| mOrientationManager.unlockOrientation(); |
| mUI.reset(); |
| mActivity.setSwipingEnabled(true); |
| // Orientation change will trigger onLayoutChange->configMosaicPreview-> |
| // resetToPreview. Do not show the capture UI in film strip. |
| if (mPreviewFocused) { |
| mUI.showPreviewUI(); |
| } |
| 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); |
| mOrientationManager.pause(); |
| } |
| |
| @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(); |
| mUI.removeDisplayChangeListener(); |
| 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; |
| |
| if (!setupCamera()) { |
| Log.e(TAG, "Failed to open camera, aborting"); |
| 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(); |
| mActivity.updateStorageSpaceAndHint(); |
| } |
| keepScreenOnAwhile(); |
| |
| mOrientationManager.resume(); |
| // Initialize location service. |
| boolean recordLocation = RecordLocationPreference.get(mPreferences, |
| mContentResolver); |
| mLocationManager.recordLocation(recordLocation); |
| mUI.initDisplayChangeListener(); |
| 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 |
| } |
| } |
| } |
| mActivity.updateStorageSpace(); |
| return null; |
| } |
| |
| @Override |
| protected void onPostExecute(Void result) { |
| mWaitProcessorTask = null; |
| mUI.dismissAllDialogs(); |
| // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE); |
| initMosaicFrameProcessorIfNeeded(); |
| Point size = mUI.getPreviewAreaSize(); |
| mPreviewUIWidth = size.x; |
| mPreviewUIHeight = size.y; |
| configMosaicPreview(); |
| resetToPreviewIfPossible(); |
| mActivity.updateStorageHint(mActivity.getStorageSpaceBytes()); |
| } |
| } |
| |
| @Override |
| public void cancelHighResStitching() { |
| if (mPaused || mCameraTexture == null) return; |
| cancelHighResComputation(); |
| } |
| |
| @Override |
| 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 onShowSwitcherPopup() { |
| } |
| |
| @Override |
| public void onMediaSaveServiceConnected(MediaSaveService s) { |
| // do nothing. |
| } |
| } |