blob: 1a5a5aa553fd8dfdbff8940a9f9b4fdf13e0984d [file] [log] [blame]
/*
* 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.panorama;
import com.android.camera.ActivityBase;
import com.android.camera.CameraDisabledException;
import com.android.camera.CameraHardwareException;
import com.android.camera.CameraHolder;
import com.android.camera.Exif;
import com.android.camera.MenuHelper;
import com.android.camera.ModePicker;
import com.android.camera.OnClickAttr;
import com.android.camera.R;
import com.android.camera.RotateDialogController;
import com.android.camera.ShutterButton;
import com.android.camera.Storage;
import com.android.camera.Thumbnail;
import com.android.camera.Util;
import com.android.camera.ui.PopupManager;
import com.android.camera.ui.Rotatable;
import com.android.camera.ui.RotateImageView;
import com.android.camera.ui.RotateLayout;
import com.android.camera.ui.SharePopup;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.graphics.YuvImage;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.hardware.CameraSound;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.OrientationEventListener;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Activity to handle panorama capturing.
*/
public class PanoramaActivity extends ActivityBase implements
ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener,
ShutterButton.OnShutterButtonListener,
MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
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_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
private static final int MSG_RESET_TO_PREVIEW = 4;
private static final int MSG_CLEAR_SCREEN_DELAY = 5;
private static final int SCREEN_DELAY = 2 * 60 * 1000;
private static final String TAG = "PanoramaActivity";
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;
// Speed is in unit of deg/sec
private static final float PANNING_SPEED_THRESHOLD = 20f;
// Ratio of nanosecond to second
private static final float NS2S = 1.0f / 1000000000.0f;
private boolean mPausing;
private View mPanoLayout;
private View mCaptureLayout;
private View mReviewLayout;
private ImageView mReview;
private RotateLayout mCaptureIndicator;
private PanoProgressBar mPanoProgressBar;
private PanoProgressBar mSavingProgressBar;
private View mFastIndicationBorder;
private View mLeftIndicator;
private View mRightIndicator;
private MosaicRendererSurfaceView mMosaicView;
private TextView mTooFastPrompt;
private ShutterButton mShutterButton;
private Object mWaitObject = new Object();
private String mPreparePreviewString;
private String mDialogTitle;
private String mDialogOkString;
private String mDialogPanoramaFailedString;
private int mIndicatorColor;
private int mIndicatorColorFast;
private float mCompassValueX;
private float mCompassValueY;
private float mCompassValueXStart;
private float mCompassValueYStart;
private float mCompassValueXStartBuffer;
private float mCompassValueYStartBuffer;
private int mCompassThreshold;
private int mTraversedAngleX;
private int mTraversedAngleY;
private long mTimestamp;
private RotateImageView mThumbnailView;
private Thumbnail mThumbnail;
private SharePopup mSharePopup;
private int mPreviewWidth;
private int mPreviewHeight;
private int mCameraState;
private int mCaptureState;
private SensorManager mSensorManager;
private Sensor mSensor;
private ModePicker mModePicker;
private MosaicFrameProcessor mMosaicFrameProcessor;
private long mTimeTaken;
private Handler mMainHandler;
private SurfaceTexture mSurfaceTexture;
private boolean mThreadRunning;
private boolean mCancelComputation;
private float[] mTransformMatrix;
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 CameraSound mCameraSound;
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(PanoramaActivity.this);
if (mOrientationCompensation != orientationCompensation) {
mOrientationCompensation = orientationCompensation;
setOrientationIndicator(mOrientationCompensation);
}
}
}
private void setOrientationIndicator(int degree) {
if (mSharePopup != null) mSharePopup.setOrientation(degree);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
addBaseMenuItems(menu);
return true;
}
private void addBaseMenuItems(Menu menu) {
MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() {
public void run() {
switchToOtherMode(ModePicker.MODE_CAMERA);
}
});
MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_VIDEO, new Runnable() {
public void run() {
switchToOtherMode(ModePicker.MODE_VIDEO);
}
});
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Window window = getWindow();
Util.enterLightsOutMode(window);
Util.initializeScreenBrightness(window, getContentResolver());
createContentView();
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
if (mSensor == null) {
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
}
mOrientationEventListener = new PanoOrientationEventListener(this);
mTransformMatrix = new float[16];
mPreparePreviewString =
getResources().getString(R.string.pano_dialog_prepare_preview);
mDialogTitle = getResources().getString(R.string.pano_dialog_title);
mDialogOkString = getResources().getString(R.string.dialog_ok);
mDialogPanoramaFailedString =
getResources().getString(R.string.pano_dialog_panorama_failed);
mCameraSound = new CameraSound();
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_RESET_TO_PREVIEW_WITH_THUMBNAIL:
onBackgroundThreadFinished();
// Set the thumbnail bitmap here because mThumbnailView must be accessed
// from the UI thread.
updateThumbnailButton();
// Share popup may still have the reference to the old thumbnail. Clear it.
mSharePopup = null;
resetToPreview();
break;
case MSG_GENERATE_FINAL_MOSAIC_ERROR:
onBackgroundThreadFinished();
if (mPausing) {
resetToPreview();
} else {
mRotateDialog.showAlertDialog(
mDialogTitle, mDialogPanoramaFailedString,
mDialogOkString, new Runnable() {
@Override
public void run() {
resetToPreview();
}},
null, null);
}
break;
case MSG_RESET_TO_PREVIEW:
onBackgroundThreadFinished();
resetToPreview();
break;
case MSG_CLEAR_SCREEN_DELAY:
getWindow().clearFlags(WindowManager.LayoutParams.
FLAG_KEEP_SCREEN_ON);
break;
}
clearMosaicFrameProcessorIfNeeded();
}
};
}
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 backCameraId = CameraHolder.instance().getBackCameraId();
mCameraDevice = Util.openCamera(this, backCameraId);
mCameraOrientation = Util.getCameraOrientation(backCameraId);
}
private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
boolean needSmaller) {
int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
boolean hasFound = false;
for (Size size : supportedSizes) {
int h = size.height;
int w = size.width;
// we only want 4:3 format.
int d = DEFAULT_CAPTURE_PIXELS - h * w;
if (needSmaller && d < 0) { // no bigger preview than 960x720.
continue;
}
if (need4To3 && (h * 4 != w * 3)) {
continue;
}
d = Math.abs(d);
if (d < pixelsDiff) {
mPreviewWidth = w;
mPreviewHeight = h;
pixelsDiff = d;
hasFound = true;
}
}
return hasFound;
}
private void setupCaptureParams(Parameters parameters) {
List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
if (!findBestPreviewSize(supportedSizes, true, true)) {
Log.w(TAG, "No 4:3 ratio preview size supported.");
if (!findBestPreviewSize(supportedSizes, false, true)) {
Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
findBestPreviewSize(supportedSizes, false, false);
}
}
Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
int last = frameRates.size() - 1;
int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
parameters.setPreviewFpsRange(minFps, maxFps);
Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
List<String> supportedFocusModes = parameters.getSupportedFocusModes();
if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
parameters.setFocusMode(mTargetFocusMode);
} else {
// Use the default focus mode and log a message
Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
" becuase the mode is not supported.");
}
parameters.setRecordingHint(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 boolean switchToOtherMode(int mode) {
if (isFinishing()) {
return false;
}
MenuHelper.gotoMode(mode, this);
finish();
return true;
}
public boolean onModeChanged(int mode) {
if (mode != ModePicker.MODE_PANORAMA) {
return switchToOtherMode(mode);
} else {
return true;
}
}
@Override
public void onMosaicSurfaceChanged() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!mPausing) {
startCameraPreview();
}
}
});
}
@Override
public void onMosaicSurfaceCreated(final int textureID) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mSurfaceTexture != null) {
mSurfaceTexture.release();
}
mSurfaceTexture = new SurfaceTexture(textureID);
if (!mPausing) {
mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
}
}
});
}
public void runViewFinder() {
mMosaicView.setWarping(false);
// Call preprocess to render it to low-res and high-res RGB textures.
mMosaicView.preprocess(mTransformMatrix);
mMosaicView.setReady();
mMosaicView.requestRender();
}
public void runMosaicCapture() {
mMosaicView.setWarping(true);
// Call preprocess to render it to low-res and high-res RGB textures.
mMosaicView.preprocess(mTransformMatrix);
// Lock the conditional variable to ensure the order of transferGPUtoCPU and
// mMosaicFrame.processFrame().
mMosaicView.lockPreviewReadyFlag();
// Now, transfer the textures from GPU to CPU memory for processing
mMosaicView.transferGPUtoCPU();
// Wait on the condition variable (will be opened when GPU->CPU transfer is done).
mMosaicView.waitUntilPreviewReady();
mMosaicFrameProcessor.processFrame();
}
public synchronized void onFrameAvailable(SurfaceTexture surface) {
/* This function may be called by some random thread,
* so let's be safe and use synchronize. No OpenGL calls can be done here.
*/
// Frames might still be available after the activity is paused. If we call onFrameAvailable
// after pausing, the GL thread will crash.
if (mPausing) return;
// Updating the texture should be done in the GL thread which mMosaicView is attached.
mMosaicView.queueEvent(new Runnable() {
@Override
public void run() {
// Check if the activity is paused here can speed up the onPause() process.
if (mPausing) return;
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(mTransformMatrix);
}
});
// Update the transformation matrix for mosaic pre-process.
if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
runViewFinder();
} else {
runMosaicCapture();
}
}
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();
mCaptureState = CAPTURE_STATE_MOSAIC;
mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording);
mCaptureIndicator.setVisibility(View.VISIBLE);
showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
mThumbnailView.setEnabled(false);
mCompassValueXStart = mCompassValueXStartBuffer;
mCompassValueYStart = mCompassValueYStartBuffer;
mTimestamp = 0;
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);
}
}
});
if (mModePicker != null) mModePicker.setEnabled(false);
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();
}
private void stopCapture(boolean aborted) {
mCaptureState = CAPTURE_STATE_VIEWFINDER;
mCaptureIndicator.setVisibility(View.GONE);
hideTooFastIndication();
hideDirectionIndicators();
mThumbnailView.setEnabled(true);
mMosaicFrameProcessor.setProgressListener(null);
stopCameraPreview();
mSurfaceTexture.setOnFrameAvailableListener(null);
if (!aborted && !mThreadRunning) {
mRotateDialog.showWaitingDialog(mPreparePreviewString);
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_RESET_TO_PREVIEW));
}
}
});
}
// do we have to wait for the thread to complete before enabling this?
if (mModePicker != null) mModePicker.setEnabled(true);
keepScreenOnAwhile();
}
private void showTooFastIndication() {
mTooFastPrompt.setVisibility(View.VISIBLE);
mFastIndicationBorder.setVisibility(View.VISIBLE);
mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
mLeftIndicator.setEnabled(true);
mRightIndicator.setEnabled(true);
}
private void hideTooFastIndication() {
mTooFastPrompt.setVisibility(View.GONE);
mFastIndicationBorder.setVisibility(View.GONE);
mPanoProgressBar.setIndicatorColor(mIndicatorColor);
mLeftIndicator.setEnabled(false);
mRightIndicator.setEnabled(false);
}
private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
float progressHorizontalAngle, float progressVerticalAngle) {
mMosaicView.setReady();
mMosaicView.requestRender();
// TODO: Now we just display warning message by the panning speed.
// Since we only support horizontal panning, we should display a warning message
// in UI when there're significant vertical movements.
if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
|| (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
showTooFastIndication();
} else {
hideTooFastIndication();
}
int angleInMajorDirection =
(Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle))
? (int) progressHorizontalAngle
: (int) progressVerticalAngle;
mPanoProgressBar.setProgress((angleInMajorDirection));
}
private void createContentView() {
setContentView(R.layout.panorama);
mCaptureState = CAPTURE_STATE_VIEWFINDER;
Resources appRes = getResources();
mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
mPanoProgressBar = (PanoProgressBar) 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));
mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
mPanoProgressBar.setIndicatorColor(mIndicatorColor);
mPanoProgressBar.setOnDirectionChangeListener(
new PanoProgressBar.OnDirectionChangeListener () {
@Override
public void onDirectionChange(int direction) {
if (mCaptureState == CAPTURE_STATE_MOSAIC) {
showDirectionIndicators(direction);
}
}
});
mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator);
mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator);
mLeftIndicator.setEnabled(false);
mRightIndicator.setEnabled(false);
mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border);
mSavingProgressBar = (PanoProgressBar) 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 = (RotateLayout) findViewById(R.id.pano_capture_indicator);
mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
mThumbnailView.enableFilter(false);
mReviewLayout = (View) findViewById(R.id.pano_review_layout);
mReview = (ImageView) findViewById(R.id.pano_reviewarea);
mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
mModePicker = (ModePicker) findViewById(R.id.mode_picker);
mModePicker.setVisibility(View.VISIBLE);
mModePicker.setOnModeChangeListener(this);
mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
mShutterButton.setOnShutterButtonListener(this);
mPanoLayout = findViewById(R.id.pano_layout);
mRotateDialog = new RotateDialogController(this, R.layout.rotate_dialog);
if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
Rotatable[] rotateLayout = {
(Rotatable) findViewById(R.id.pano_pan_progress_bar_layout),
(Rotatable) findViewById(R.id.pano_capture_too_fast_textview_layout),
(Rotatable) findViewById(R.id.pano_review_saving_indication_layout),
(Rotatable) findViewById(R.id.pano_saving_progress_bar_layout),
(Rotatable) findViewById(R.id.pano_review_cancel_button_layout),
(Rotatable) findViewById(R.id.pano_rotate_reviewarea),
(Rotatable) mRotateDialog,
(Rotatable) mCaptureIndicator,
(Rotatable) mModePicker,
(Rotatable) mThumbnailView};
for (Rotatable r : rotateLayout) {
r.setOrientation(270);
}
}
}
@Override
public void onShutterButtonClick() {
// If mSurfaceTexture == null then GL setup is not finished yet.
// No buttons can be pressed.
if (mPausing || mThreadRunning || mSurfaceTexture == 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:
mCameraSound.playSound(CameraSound.START_VIDEO_RECORDING);
startCapture();
break;
case CAPTURE_STATE_MOSAIC:
mCameraSound.playSound(CameraSound.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
runOnUiThread(new Runnable() {
public void run() {
mSavingProgressBar.setProgress(progress);
}
});
}
}
};
t.start();
}
private void initThumbnailButton() {
// Load the thumbnail from the disk.
if (mThumbnail == null) {
mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
}
updateThumbnailButton();
}
private void updateThumbnailButton() {
// Update last image if URI is invalid and the storage is ready.
ContentResolver contentResolver = getContentResolver();
if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) {
mThumbnail = Thumbnail.getLastThumbnail(contentResolver);
}
if (mThumbnail != null) {
mThumbnailView.setBitmap(mThumbnail.getBitmap());
} else {
mThumbnailView.setBitmap(null);
}
}
public void saveHighResMosaic() {
runBackgroundThread(new Thread() {
@Override
public void run() {
MosaicJpeg jpeg = generateFinalMosaic(true);
if (jpeg == null) { // Cancelled by user.
mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
} else if (!jpeg.isValid) { // Error when generating mosaic.
mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
} else {
// The panorama image returned from the library is orientated 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 = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
if (uri != null) {
// Create a thumbnail whose width or height is equal or bigger
// than the screen's width or height.
int widthRatio = (int) Math.ceil((double) jpeg.width
/ mPanoLayout.getWidth());
int heightRatio = (int) Math.ceil((double) jpeg.height
/ mPanoLayout.getHeight());
int inSampleSize = Integer.highestOneBit(
Math.max(widthRatio, heightRatio));
mThumbnail = Thumbnail.createThumbnail(
jpeg.data, orientation, inSampleSize, uri);
Util.broadcastNewPicture(PanoramaActivity.this, uri);
}
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
}
}
});
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();
}
}
@OnClickAttr
public void onCancelButtonClicked(View v) {
if (mPausing || mSurfaceTexture == null) return;
cancelHighResComputation();
}
@OnClickAttr
public void onThumbnailClicked(View v) {
if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
showSharePopup();
}
private void showSharePopup() {
if (mThumbnail == null) return;
Uri uri = mThumbnail.getUri();
if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
// The orientation compensation is set to 0 here because we only support landscape.
mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
mOrientationCompensation,
findViewById(R.id.frame_layout));
}
mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
}
private void reset() {
mCaptureState = CAPTURE_STATE_VIEWFINDER;
mReviewLayout.setVisibility(View.GONE);
mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
mPanoProgressBar.setVisibility(View.GONE);
mCaptureLayout.setVisibility(View.VISIBLE);
mMosaicFrameProcessor.reset();
mSurfaceTexture.setOnFrameAvailableListener(this);
}
private void resetToPreview() {
reset();
if (!mPausing) startCameraPreview();
}
private void showFinalMosaic(Bitmap bitmap) {
if (bitmap != null) {
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(
getResources().getString(R.string.pano_file_name_format), mTimeTaken);
Uri uri = Storage.addImage(getContentResolver(), filename, mTimeTaken, null,
orientation, jpegData, width, height);
if (uri != null && orientation != 0) {
String filepath = Storage.generateFilepath(filename);
try {
// Save the orientation in EXIF.
ExifInterface exif = new ExifInterface(filepath);
exif.setAttribute(ExifInterface.TAG_ORIENTATION,
getExifOrientation(orientation));
exif.saveAttributes();
} catch (IOException e) {
Log.e(TAG, "cannot set exif data: " + filepath);
}
}
return uri;
}
return null;
}
private static String getExifOrientation(int orientation) {
switch (orientation) {
case 0:
return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
case 90:
return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
case 180:
return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
case 270:
return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
default:
throw new AssertionError("invalid: " + orientation);
}
}
private void clearMosaicFrameProcessorIfNeeded() {
if (!mPausing || mThreadRunning) return;
mMosaicFrameProcessor.clear();
}
private void initMosaicFrameProcessorIfNeeded() {
if (mPausing || mThreadRunning) return;
if (mMosaicFrameProcessor == null) {
// Start the activity for the first time.
mMosaicFrameProcessor = new MosaicFrameProcessor(
mPreviewWidth, mPreviewHeight, getPreviewBufSize());
}
mMosaicFrameProcessor.initialize();
}
@Override
protected void onPause() {
super.onPause();
mPausing = true;
cancelHighResComputation();
// Stop the capturing first.
if (mCaptureState == CAPTURE_STATE_MOSAIC) {
stopCapture(true);
reset();
}
if (mSharePopup != null) mSharePopup.dismiss();
if (mThumbnail != null && !mThumbnail.fromFile()) {
mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
}
releaseCamera();
mMosaicView.onPause();
clearMosaicFrameProcessorIfNeeded();
mOrientationEventListener.disable();
resetScreenOn();
mCameraSound.release();
System.gc();
}
@Override
protected void doOnResume() {
mPausing = false;
mOrientationEventListener.enable();
mCaptureState = CAPTURE_STATE_VIEWFINDER;
try {
setupCamera();
// Camera must be initialized before MosaicFrameProcessor is initialized.
// The preview size has to be decided by camera device.
initMosaicFrameProcessorIfNeeded();
mMosaicView.onResume();
initThumbnailButton();
keepScreenOnAwhile();
} catch (CameraHardwareException e) {
Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
return;
} catch (CameraDisabledException e) {
Util.showErrorAndFinish(this, R.string.camera_disabled);
return;
}
// Dismiss open menu if exists.
PopupManager.getInstance(this).notifyShowPopup(null);
}
/**
* 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 a error meesage 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 setPreviewTexture(SurfaceTexture surface) {
try {
mCameraDevice.setPreviewTexture(surface);
} catch (Throwable ex) {
releaseCamera();
throw new RuntimeException("setPreviewTexture failed", ex);
}
}
private void startCameraPreview() {
// 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);
setPreviewTexture(mSurfaceTexture);
try {
Log.v(TAG, "startPreview");
mCameraDevice.startPreview();
} catch (Throwable ex) {
releaseCamera();
throw new RuntimeException("startPreview failed", ex);
}
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() {
super.onUserInteraction();
if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
}
private void resetScreenOn() {
mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void keepScreenOnAwhile() {
mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
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);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}