PanoramaActivity.java revision 057040c0e19834d98719143c2627a9f0df3e4c35
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.camera.panorama; 18 19import com.android.camera.CameraDisabledException; 20import com.android.camera.CameraHardwareException; 21import com.android.camera.CameraHolder; 22import com.android.camera.Exif; 23import com.android.camera.MenuHelper; 24import com.android.camera.ModePicker; 25import com.android.camera.OnClickAttr; 26import com.android.camera.R; 27import com.android.camera.ShutterButton; 28import com.android.camera.Storage; 29import com.android.camera.Thumbnail; 30import com.android.camera.Util; 31import com.android.camera.ui.RotateImageView; 32import com.android.camera.ui.SharePopup; 33 34import android.animation.Animator; 35import android.animation.AnimatorInflater; 36import android.animation.AnimatorSet; 37import android.animation.ObjectAnimator; 38import android.animation.ValueAnimator; 39import android.app.Activity; 40import android.app.AlertDialog; 41import android.app.ProgressDialog; 42import android.content.Context; 43import android.content.DialogInterface; 44import android.content.res.Resources; 45import android.graphics.Bitmap; 46import android.graphics.BitmapFactory; 47import android.graphics.ImageFormat; 48import android.graphics.PixelFormat; 49import android.graphics.Rect; 50import android.graphics.SurfaceTexture; 51import android.graphics.YuvImage; 52import android.hardware.Camera; 53import android.hardware.Camera.Parameters; 54import android.hardware.Camera.Size; 55import android.hardware.Sensor; 56import android.hardware.SensorEvent; 57import android.hardware.SensorEventListener; 58import android.hardware.SensorManager; 59import android.net.Uri; 60import android.os.Bundle; 61import android.os.Handler; 62import android.os.Message; 63import android.util.Log; 64import android.view.Gravity; 65import android.view.OrientationEventListener; 66import android.view.View; 67import android.view.WindowManager; 68import android.view.animation.LinearInterpolator; 69import android.widget.ImageView; 70import android.widget.RelativeLayout; 71import android.widget.TextView; 72 73import java.io.ByteArrayOutputStream; 74import java.util.List; 75 76/** 77 * Activity to handle panorama capturing. 78 */ 79public class PanoramaActivity extends Activity implements 80 ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener, 81 ShutterButton.OnShutterButtonListener, 82 MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener { 83 public static final int DEFAULT_SWEEP_ANGLE = 160; 84 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 85 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 86 87 private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1; 88 private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2; 89 private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3; 90 private static final int MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW = 4; 91 private static final int MSG_GENERATE_FINAL_MOSAIC_CANCELLED = 5; 92 93 private static final String TAG = "PanoramaActivity"; 94 private static final int PREVIEW_STOPPED = 0; 95 private static final int PREVIEW_ACTIVE = 1; 96 private static final int CAPTURE_STATE_VIEWFINDER = 0; 97 private static final int CAPTURE_STATE_MOSAIC = 1; 98 99 // Speed is in unit of deg/sec 100 private static final float PANNING_SPEED_THRESHOLD = 20f; 101 102 // Ratio of nanosecond to second 103 private static final float NS2S = 1.0f / 1000000000.0f; 104 105 private boolean mPausing; 106 107 private View mPanoLayout; 108 private View mCaptureLayout; 109 private View mReviewLayout; 110 private ImageView mReview; 111 private PanoProgressBar mPanoProgressBar; 112 private MosaicRendererSurfaceView mMosaicView; 113 private TextView mTooFastPrompt; 114 private ShutterButton mShutterButton; 115 116 private ProgressDialog mProgressDialog; 117 private String mPreparePreviewString; 118 private String mGeneratePanoramaString; 119 private AlertDialog mAlertDialog; 120 private String mDialogTitle; 121 private String mDialogOk; 122 123 // This custom dialog is to follow the UI spec to produce a dialog with a spinner in the top 124 // center part and a text view in the bottom part. The background is a rounded rectangle. The 125 // system dialog cannot be used because there will be a rectangle with 3D-like edges. 126 private RelativeLayout mPanoramaPrepareDialog; 127 private Animator mPanoramaPrepareDialogFadeIn; 128 private Animator mPanoramaPrepareDialogFadeOut; 129 130 private float mCompassValueX; 131 private float mCompassValueY; 132 private float mCompassValueXStart; 133 private float mCompassValueYStart; 134 private float mCompassValueXStartBuffer; 135 private float mCompassValueYStartBuffer; 136 private int mCompassThreshold; 137 private int mTraversedAngleX; 138 private int mTraversedAngleY; 139 private long mTimestamp; 140 // Control variables for the terminate condition. 141 private int mMinAngleX; 142 private int mMaxAngleX; 143 private int mMinAngleY; 144 private int mMaxAngleY; 145 146 private RotateImageView mThumbnailView; 147 private Thumbnail mThumbnail; 148 private SharePopup mSharePopup; 149 150 private AnimatorSet mThumbnailViewAndModePickerOut; 151 private AnimatorSet mThumbnailViewAndModePickerIn; 152 153 private int mPreviewWidth; 154 private int mPreviewHeight; 155 private Camera mCameraDevice; 156 private int mCameraState; 157 private int mCaptureState; 158 private SensorManager mSensorManager; 159 private Sensor mSensor; 160 private ModePicker mModePicker; 161 private MosaicFrameProcessor mMosaicFrameProcessor; 162 private long mTimeTaken; 163 private Handler mMainHandler; 164 private SurfaceTexture mSurfaceTexture; 165 private boolean mThreadRunning; 166 private boolean mCancelComputation; 167 private int mMosaicComputationStatus; 168 private float[] mTransformMatrix; 169 private float mHorizontalViewAngle; 170 171 private PanoOrientationEventListener mOrientationEventListener; 172 // The value could be 0, 1, 2, 3 for the 4 different orientations measured in clockwise 173 // respectively. 174 private int mDeviceOrientation; 175 176 private class MosaicJpeg { 177 public MosaicJpeg(byte[] data, int width, int height) { 178 this.data = data; 179 this.width = width; 180 this.height = height; 181 } 182 183 public final byte[] data; 184 public final int width; 185 public final int height; 186 } 187 188 private class PanoOrientationEventListener extends OrientationEventListener { 189 public PanoOrientationEventListener(Context context) { 190 super(context); 191 } 192 193 @Override 194 public void onOrientationChanged(int orientation) { 195 // Default to the last known orientation. 196 if (orientation == ORIENTATION_UNKNOWN) return; 197 mDeviceOrientation = ((orientation + 45) / 90) % 4; 198 } 199 } 200 201 @Override 202 public void onCreate(Bundle icicle) { 203 super.onCreate(icicle); 204 205 getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 206 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 207 Util.enterLightsOutMode(getWindow()); 208 209 createContentView(); 210 211 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 212 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 213 if (mSensor == null) { 214 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 215 } 216 217 mOrientationEventListener = new PanoOrientationEventListener(this); 218 219 mTransformMatrix = new float[16]; 220 221 mPreparePreviewString = 222 getResources().getString(R.string.pano_dialog_prepare_preview); 223 mGeneratePanoramaString = 224 getResources().getString(R.string.pano_dialog_generate_panorama); 225 mDialogTitle = getResources().getString(R.string.pano_dialog_title); 226 mDialogOk = getResources().getString(R.string.dialog_ok); 227 228 mMainHandler = new Handler() { 229 @Override 230 public void handleMessage(Message msg) { 231 switch (msg.what) { 232 case MSG_LOW_RES_FINAL_MOSAIC_READY: 233 onBackgroundThreadFinished(); 234 showFinalMosaic((Bitmap) msg.obj); 235 break; 236 case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL: 237 onBackgroundThreadFinished(); 238 // Set the thumbnail bitmap here because mThumbnailView must be accessed 239 // from the UI thread. 240 if (mThumbnail != null) { 241 mThumbnailView.setBitmap(mThumbnail.getBitmap()); 242 } 243 resetToPreview(); 244 break; 245 case MSG_GENERATE_FINAL_MOSAIC_ERROR: 246 onBackgroundThreadFinished(); 247 mAlertDialog = (new AlertDialog.Builder(PanoramaActivity.this)) 248 .setTitle(mDialogTitle) 249 .setMessage(R.string.pano_dialog_panorama_failed) 250 .create(); 251 mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, mDialogOk, 252 obtainMessage(MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW)); 253 mAlertDialog.show(); 254 break; 255 case MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW: 256 mAlertDialog.dismiss(); 257 mAlertDialog = null; 258 resetToPreview(); 259 break; 260 case MSG_GENERATE_FINAL_MOSAIC_CANCELLED: 261 onBackgroundThreadFinished(); 262 resetToPreview(); 263 } 264 clearMosaicFrameProcessorIfNeeded(); 265 } 266 }; 267 } 268 269 private void setupCamera() { 270 openCamera(); 271 Parameters parameters = mCameraDevice.getParameters(); 272 setupCaptureParams(parameters); 273 configureCamera(parameters); 274 } 275 276 private void releaseCamera() { 277 if (mCameraDevice != null) { 278 mCameraDevice.setPreviewCallbackWithBuffer(null); 279 CameraHolder.instance().release(); 280 mCameraDevice = null; 281 mCameraState = PREVIEW_STOPPED; 282 } 283 } 284 285 private void openCamera() { 286 try { 287 mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId()); 288 } catch (CameraHardwareException e) { 289 Util.showErrorAndFinish(this, R.string.cannot_connect_camera); 290 return; 291 } catch (CameraDisabledException e) { 292 Util.showErrorAndFinish(this, R.string.camera_disabled); 293 return; 294 } 295 } 296 297 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 298 boolean needSmaller) { 299 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 300 boolean hasFound = false; 301 for (Size size : supportedSizes) { 302 int h = size.height; 303 int w = size.width; 304 // we only want 4:3 format. 305 int d = DEFAULT_CAPTURE_PIXELS - h * w; 306 if (needSmaller && d < 0) { // no bigger preview than 960x720. 307 continue; 308 } 309 if (need4To3 && (h * 4 != w * 3)) { 310 continue; 311 } 312 d = Math.abs(d); 313 if (d < pixelsDiff) { 314 mPreviewWidth = w; 315 mPreviewHeight = h; 316 pixelsDiff = d; 317 hasFound = true; 318 } 319 } 320 return hasFound; 321 } 322 323 private void setupCaptureParams(Parameters parameters) { 324 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 325 if (!findBestPreviewSize(supportedSizes, true, true)) { 326 Log.w(TAG, "No 4:3 ratio preview size supported."); 327 if (!findBestPreviewSize(supportedSizes, false, true)) { 328 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 329 findBestPreviewSize(supportedSizes, false, false); 330 } 331 } 332 Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); 333 parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 334 335 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 336 int last = frameRates.size() - 1; 337 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 338 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 339 parameters.setPreviewFpsRange(minFps, maxFps); 340 Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); 341 342 parameters.setRecordingHint(false); 343 344 mHorizontalViewAngle = ((mDeviceOrientation % 2) == 0) ? 345 parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle(); 346 } 347 348 public int getPreviewBufSize() { 349 PixelFormat pixelInfo = new PixelFormat(); 350 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 351 // TODO: remove this extra 32 byte after the driver bug is fixed. 352 return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 353 } 354 355 private void configureCamera(Parameters parameters) { 356 mCameraDevice.setParameters(parameters); 357 358 int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this), 359 CameraHolder.instance().getBackCameraId()); 360 mCameraDevice.setDisplayOrientation(orientation); 361 } 362 363 private boolean switchToOtherMode(int mode) { 364 if (isFinishing()) { 365 return false; 366 } 367 MenuHelper.gotoMode(mode, this); 368 finish(); 369 return true; 370 } 371 372 public boolean onModeChanged(int mode) { 373 if (mode != ModePicker.MODE_PANORAMA) { 374 return switchToOtherMode(mode); 375 } else { 376 return true; 377 } 378 } 379 380 @Override 381 public void onMosaicSurfaceCreated(final int textureID) { 382 runOnUiThread(new Runnable() { 383 @Override 384 public void run() { 385 if (mSurfaceTexture != null) { 386 mSurfaceTexture.release(); 387 } 388 mSurfaceTexture = new SurfaceTexture(textureID); 389 if (!mPausing) { 390 mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this); 391 startCameraPreview(); 392 } 393 } 394 }); 395 } 396 397 public void runViewFinder() { 398 mMosaicView.setWarping(false); 399 // Call preprocess to render it to low-res and high-res RGB textures. 400 mMosaicView.preprocess(mTransformMatrix); 401 mMosaicView.setReady(); 402 mMosaicView.requestRender(); 403 } 404 405 public void runMosaicCapture() { 406 mMosaicView.setWarping(true); 407 // Call preprocess to render it to low-res and high-res RGB textures. 408 mMosaicView.preprocess(mTransformMatrix); 409 // Lock the conditional variable to ensure the order of transferGPUtoCPU and 410 // mMosaicFrame.processFrame(). 411 mMosaicView.lockPreviewReadyFlag(); 412 // Now, transfer the textures from GPU to CPU memory for processing 413 mMosaicView.transferGPUtoCPU(); 414 // Wait on the condition variable (will be opened when GPU->CPU transfer is done). 415 mMosaicView.waitUntilPreviewReady(); 416 mMosaicFrameProcessor.processFrame(); 417 } 418 419 public synchronized void onFrameAvailable(SurfaceTexture surface) { 420 /* This function may be called by some random thread, 421 * so let's be safe and use synchronize. No OpenGL calls can be done here. 422 */ 423 // Updating the texture should be done in the GL thread which mMosaicView is attached. 424 mMosaicView.queueEvent(new Runnable() { 425 @Override 426 public void run() { 427 mSurfaceTexture.updateTexImage(); 428 mSurfaceTexture.getTransformMatrix(mTransformMatrix); 429 } 430 }); 431 // Update the transformation matrix for mosaic pre-process. 432 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { 433 runViewFinder(); 434 } else { 435 runMosaicCapture(); 436 } 437 } 438 439 public void startCapture() { 440 // Reset values so we can do this again. 441 mTimeTaken = System.currentTimeMillis(); 442 mCaptureState = CAPTURE_STATE_MOSAIC; 443 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording); 444 445 // XML-style animations can not be used here. The Y position has to be calculated runtime. 446 float ystart = mThumbnailView.getY(); 447 ValueAnimator va1 = ObjectAnimator.ofFloat( 448 mThumbnailView, "y", ystart, -mThumbnailView.getHeight()); 449 ValueAnimator va1Reverse = ObjectAnimator.ofFloat( 450 mThumbnailView, "y", -mThumbnailView.getHeight(), ystart); 451 ystart = mModePicker.getY(); 452 float height = mCaptureLayout.getHeight(); 453 ValueAnimator va2 = ObjectAnimator.ofFloat( 454 mModePicker, "y", ystart, height + 1); 455 ValueAnimator va2Reverse = ObjectAnimator.ofFloat( 456 mModePicker, "y", height + 1, ystart); 457 LinearInterpolator li = new LinearInterpolator(); 458 mThumbnailViewAndModePickerOut = new AnimatorSet(); 459 mThumbnailViewAndModePickerOut.play(va1).with(va2); 460 mThumbnailViewAndModePickerOut.setDuration(500); 461 mThumbnailViewAndModePickerOut.setInterpolator(li); 462 mThumbnailViewAndModePickerIn = new AnimatorSet(); 463 mThumbnailViewAndModePickerIn.play(va1Reverse).with(va2Reverse); 464 mThumbnailViewAndModePickerIn.setDuration(500); 465 mThumbnailViewAndModePickerIn.setInterpolator(li); 466 467 mThumbnailViewAndModePickerOut.start(); 468 469 mCompassValueXStart = mCompassValueXStartBuffer; 470 mCompassValueYStart = mCompassValueYStartBuffer; 471 mMinAngleX = 0; 472 mMaxAngleX = 0; 473 mMinAngleY = 0; 474 mMaxAngleY = 0; 475 mTimestamp = 0; 476 477 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 478 @Override 479 public void onProgress(boolean isFinished, float panningRateX, float panningRateY) { 480 if (isFinished 481 || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE) 482 || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) { 483 stopCapture(); 484 } else { 485 updateProgress(panningRateX); 486 } 487 } 488 }); 489 490 mPanoProgressBar.reset(); 491 // TODO: calculate the indicator width according to different devices to reflect the actual 492 // angle of view of the camera device. 493 mPanoProgressBar.setIndicatorWidth(20); 494 mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); 495 mPanoProgressBar.setVisibility(View.VISIBLE); 496 mMosaicView.setVisibility(View.VISIBLE); 497 } 498 499 private void stopCapture() { 500 mCaptureState = CAPTURE_STATE_VIEWFINDER; 501 mTooFastPrompt.setVisibility(View.GONE); 502 503 mMosaicFrameProcessor.setProgressListener(null); 504 stopCameraPreview(); 505 506 mSurfaceTexture.setOnFrameAvailableListener(null); 507 508 if (!mThreadRunning) { 509 runBackgroundThreadAndShowDialog(mPreparePreviewString, false, new Thread() { 510 @Override 511 public void run() { 512 MosaicJpeg jpeg = generateFinalMosaic(false); 513 514 if (mMosaicComputationStatus == Mosaic.MOSAIC_RET_OK) { 515 Bitmap bitmap = null; 516 if (jpeg != null) { 517 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 518 } 519 mMainHandler.sendMessage(mMainHandler.obtainMessage( 520 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 521 } 522 } 523 }); 524 reportProgress(false); 525 } 526 mThumbnailViewAndModePickerIn.start(); 527 } 528 529 private void updateProgress(float panningRate) { 530 mMosaicView.setReady(); 531 mMosaicView.requestRender(); 532 533 // TODO: Now we just display warning message by the panning speed. 534 // Since we only support horizontal panning, we should display a warning message 535 // in UI when there're significant vertical movements. 536 if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) { 537 // TODO: draw speed indication according to the UI spec. 538 mTooFastPrompt.setVisibility(View.VISIBLE); 539 mTooFastPrompt.invalidate(); 540 } else { 541 mTooFastPrompt.setVisibility(View.GONE); 542 mTooFastPrompt.invalidate(); 543 } 544 } 545 546 private void createContentView() { 547 setContentView(R.layout.panorama); 548 549 mCaptureState = CAPTURE_STATE_VIEWFINDER; 550 551 Resources appRes = getResources(); 552 553 mCaptureLayout = (View) findViewById(R.id.pano_capture_layout); 554 mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_capture_view); 555 mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 556 mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); 557 mPanoProgressBar.setIndicatorColor(appRes.getColor(R.color.pano_progress_indication)); 558 mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview); 559 560 mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail); 561 562 mReviewLayout = (View) findViewById(R.id.pano_review_layout); 563 mReview = (ImageView) findViewById(R.id.pano_reviewarea); 564 mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer); 565 mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this); 566 mMosaicView.setVisibility(View.VISIBLE); 567 568 mModePicker = (ModePicker) findViewById(R.id.mode_picker); 569 mModePicker.setVisibility(View.VISIBLE); 570 mModePicker.setOnModeChangeListener(this); 571 mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA); 572 573 mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); 574 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 575 mShutterButton.setOnShutterButtonListener(this); 576 577 mPanoramaPrepareDialog = (RelativeLayout) 578 findViewById(R.id.pano_preview_progress_dialog); 579 580 mPanoramaPrepareDialogFadeIn = AnimatorInflater.loadAnimator(this, R.anim.fade_in_quick); 581 mPanoramaPrepareDialogFadeIn.setTarget(mPanoramaPrepareDialog); 582 mPanoramaPrepareDialogFadeOut = AnimatorInflater.loadAnimator(this, R.anim.fade_out_quick); 583 mPanoramaPrepareDialogFadeOut.setTarget(mPanoramaPrepareDialog); 584 585 mPanoLayout = findViewById(R.id.pano_layout); 586 } 587 588 @Override 589 public void onShutterButtonClick(ShutterButton b) { 590 // If mSurfaceTexture == null then GL setup is not finished yet. 591 // No buttons can be pressed. 592 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 593 // Since this button will stay on the screen when capturing, we need to check the state 594 // right now. 595 switch (mCaptureState) { 596 case CAPTURE_STATE_VIEWFINDER: 597 startCapture(); 598 break; 599 case CAPTURE_STATE_MOSAIC: 600 stopCapture(); 601 } 602 } 603 604 @Override 605 public void onShutterButtonFocus(ShutterButton b, boolean pressed) { 606 } 607 608 public void reportProgress(final boolean highRes) { 609 Thread t = new Thread() { 610 @Override 611 public void run() { 612 while (mThreadRunning) { 613 final int progress = mMosaicFrameProcessor.reportProgress( 614 highRes, mCancelComputation); 615 616 try { 617 Thread.sleep(50); 618 } catch (Exception e) { 619 throw new RuntimeException("Panorama reportProgress failed", e); 620 } 621 // Update the progress bar 622 runOnUiThread(new Runnable() { 623 public void run() { 624 // Check if mProgressDialog is null because the background thread 625 // finished. 626 if (mProgressDialog != null) { 627 mProgressDialog.setProgress(progress); 628 } 629 } 630 }); 631 } 632 } 633 }; 634 t.start(); 635 } 636 637 @OnClickAttr 638 public void onOkButtonClicked(View v) { 639 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 640 runBackgroundThreadAndShowDialog(mGeneratePanoramaString, true, new Thread() { 641 @Override 642 public void run() { 643 MosaicJpeg jpeg = generateFinalMosaic(true); 644 645 if (mMosaicComputationStatus == Mosaic.MOSAIC_RET_CANCELLED) { 646 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_CANCELLED); 647 } else { 648 if (jpeg == null) { 649 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 650 } else { 651 int orientation = Exif.getOrientation(jpeg.data); 652 Uri uri = savePanorama(jpeg.data, orientation); 653 if (uri != null) { 654 // Create a thumbnail whose width is equal or bigger 655 // than the entire screen. 656 int ratio = (int) Math.ceil((double) jpeg.width / 657 mPanoLayout.getWidth()); 658 int inSampleSize = Integer.highestOneBit(ratio); 659 mThumbnail = Thumbnail.createThumbnail( 660 jpeg.data, orientation, inSampleSize, uri); 661 } 662 mMainHandler.sendMessage( 663 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL)); 664 } 665 } 666 } 667 }); 668 reportProgress(true); 669 } 670 671 /** 672 * If the style is horizontal one, the maximum progress is assumed to be 100. 673 */ 674 private void runBackgroundThreadAndShowDialog( 675 String str, boolean showPercentageProgress, Thread thread) { 676 mThreadRunning = true; 677 if (showPercentageProgress) { 678 mProgressDialog = new ProgressDialog(this); 679 mProgressDialog.setMax(100); 680 mProgressDialog.setMessage(str); 681 mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 682 // TODO: update the UI according to specs. 683 mProgressDialog.setCancelable(false); // Don't allow back key to dismiss this dialog. 684 685 mProgressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", 686 new DialogInterface.OnClickListener() { 687 public void onClick(DialogInterface dialog, 688 int whichButton) { 689 mCancelComputation = true; 690 } 691 }); 692 mProgressDialog.show(); 693 } else { 694 mPanoramaPrepareDialogFadeIn.start(); 695 mPanoramaPrepareDialog.setVisibility(View.VISIBLE); 696 } 697 thread.start(); 698 } 699 700 private void onBackgroundThreadFinished() { 701 mThreadRunning = false; 702 if (mProgressDialog != null) { 703 mProgressDialog.dismiss(); 704 mProgressDialog = null; 705 } 706 if (mPanoramaPrepareDialog.getVisibility() == View.VISIBLE) { 707 mPanoramaPrepareDialogFadeOut.start(); 708 mPanoramaPrepareDialog.setVisibility(View.GONE); 709 } 710 } 711 712 @OnClickAttr 713 public void onRetakeButtonClicked(View v) { 714 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 715 resetToPreview(); 716 } 717 718 @OnClickAttr 719 public void onThumbnailClicked(View v) { 720 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 721 showSharePopup(); 722 } 723 724 private void showSharePopup() { 725 if (mThumbnail == null) return; 726 Uri uri = mThumbnail.getUri(); 727 if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) { 728 // The orientation compensation is set to 0 here because we only support landscape. 729 // Panorama picture is long. Use pano_layout so the share popup can be full-screen. 730 mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 0, 731 findViewById(R.id.pano_layout)); 732 } 733 mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0); 734 } 735 736 private void resetToPreview() { 737 mCaptureState = CAPTURE_STATE_VIEWFINDER; 738 mCancelComputation = false; 739 740 mReviewLayout.setVisibility(View.GONE); 741 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 742 mPanoProgressBar.setVisibility(View.GONE); 743 mCaptureLayout.setVisibility(View.VISIBLE); 744 mMosaicFrameProcessor.reset(); 745 746 mSurfaceTexture.setOnFrameAvailableListener(this); 747 748 if (!mPausing) startCameraPreview(); 749 750 mMosaicView.setVisibility(View.VISIBLE); 751 } 752 753 private void showFinalMosaic(Bitmap bitmap) { 754 if (bitmap != null) { 755 mReview.setImageBitmap(bitmap); 756 } 757 mCaptureLayout.setVisibility(View.GONE); 758 mReviewLayout.setVisibility(View.VISIBLE); 759 } 760 761 private Uri savePanorama(byte[] jpegData, int orientation) { 762 if (jpegData != null) { 763 String imagePath = PanoUtil.createName( 764 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 765 return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 766 orientation, jpegData); 767 } 768 return null; 769 } 770 771 private void clearMosaicFrameProcessorIfNeeded() { 772 if (!mPausing || mThreadRunning) return; 773 mMosaicFrameProcessor.clear(); 774 } 775 776 private void initMosaicFrameProcessorIfNeeded() { 777 if (mPausing || mThreadRunning) return; 778 if (mMosaicFrameProcessor == null) { 779 // Start the activity for the first time. 780 mMosaicFrameProcessor = new MosaicFrameProcessor( 781 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 782 } 783 mMosaicFrameProcessor.initialize(); 784 } 785 786 @Override 787 protected void onPause() { 788 super.onPause(); 789 790 releaseCamera(); 791 mPausing = true; 792 mMosaicView.onPause(); 793 mSensorManager.unregisterListener(mListener); 794 clearMosaicFrameProcessorIfNeeded(); 795 mOrientationEventListener.disable(); 796 System.gc(); 797 } 798 799 @Override 800 protected void onResume() { 801 super.onResume(); 802 803 mPausing = false; 804 mOrientationEventListener.enable(); 805 /* 806 * It is not necessary to get accelerometer events at a very high rate, 807 * by using a game rate (SENSOR_DELAY_UI), we get an automatic 808 * low-pass filter, which "extracts" the gravity component of the 809 * acceleration. As an added benefit, we use less power and CPU 810 * resources. 811 */ 812 mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI); 813 mCaptureState = CAPTURE_STATE_VIEWFINDER; 814 setupCamera(); 815 if (mSurfaceTexture != null) { 816 mSurfaceTexture.setOnFrameAvailableListener(this); 817 startCameraPreview(); 818 } 819 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 820 // has to be decided by camera device. 821 initMosaicFrameProcessorIfNeeded(); 822 mMosaicView.onResume(); 823 } 824 825 private void updateCompassValue() { 826 // By what angle has the camera moved since start of capture? 827 mTraversedAngleX = (int) (mCompassValueX - mCompassValueXStart); 828 mTraversedAngleY = (int) (mCompassValueY - mCompassValueYStart); 829 mMinAngleX = Math.min(mMinAngleX, mTraversedAngleX); 830 mMaxAngleX = Math.max(mMaxAngleX, mTraversedAngleX); 831 mMinAngleY = Math.min(mMinAngleY, mTraversedAngleY); 832 mMaxAngleY = Math.max(mMaxAngleY, mTraversedAngleY); 833 834 // Use orientation to identify if the user is panning to the right or the left. 835 switch (mDeviceOrientation) { 836 case 0: 837 mPanoProgressBar.setProgress(-mTraversedAngleX); 838 break; 839 case 1: 840 mPanoProgressBar.setProgress(mTraversedAngleY); 841 break; 842 case 2: 843 mPanoProgressBar.setProgress(mTraversedAngleX); 844 break; 845 case 3: 846 mPanoProgressBar.setProgress(-mTraversedAngleY); 847 break; 848 } 849 mPanoProgressBar.invalidate(); 850 } 851 852 private final SensorEventListener mListener = new SensorEventListener() { 853 public void onSensorChanged(SensorEvent event) { 854 if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { 855 if (mTimestamp != 0) { 856 final float dT = (event.timestamp - mTimestamp) * NS2S; 857 mCompassValueX += event.values[1] * dT * 180.0f / Math.PI; 858 mCompassValueY += event.values[0] * dT * 180.0f / Math.PI; 859 mCompassValueXStartBuffer = mCompassValueX; 860 mCompassValueYStartBuffer = mCompassValueY; 861 updateCompassValue(); 862 } 863 mTimestamp = event.timestamp; 864 865 } 866 } 867 868 @Override 869 public void onAccuracyChanged(Sensor sensor, int accuracy) { 870 } 871 }; 872 873 public MosaicJpeg generateFinalMosaic(boolean highRes) { 874 int ret = mMosaicFrameProcessor.createMosaic(highRes); 875 876 mMosaicComputationStatus = ret; 877 878 if (ret == Mosaic.MOSAIC_RET_CANCELLED) 879 return null; 880 881 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 882 if (imageData == null) { 883 Log.e(TAG, "getFinalMosaicNV21() returned null."); 884 return null; 885 } 886 887 int len = imageData.length - 8; 888 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 889 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 890 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 891 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 892 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 893 894 if (width <= 0 || height <= 0) { 895 // TODO: pop up a error meesage indicating that the final result is not generated. 896 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 897 height); 898 return null; 899 } 900 901 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 902 ByteArrayOutputStream out = new ByteArrayOutputStream(); 903 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 904 try { 905 out.close(); 906 } catch (Exception e) { 907 Log.e(TAG, "Exception in storing final mosaic", e); 908 return null; 909 } 910 return new MosaicJpeg(out.toByteArray(), width, height); 911 } 912 913 private void setPreviewTexture(SurfaceTexture surface) { 914 try { 915 mCameraDevice.setPreviewTexture(surface); 916 } catch (Throwable ex) { 917 releaseCamera(); 918 throw new RuntimeException("setPreviewTexture failed", ex); 919 } 920 } 921 922 private void startCameraPreview() { 923 // If we're previewing already, stop the preview first (this will blank 924 // the screen). 925 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 926 927 setPreviewTexture(mSurfaceTexture); 928 929 try { 930 Log.v(TAG, "startPreview"); 931 mCameraDevice.startPreview(); 932 } catch (Throwable ex) { 933 releaseCamera(); 934 throw new RuntimeException("startPreview failed", ex); 935 } 936 mCameraState = PREVIEW_ACTIVE; 937 } 938 939 private void stopCameraPreview() { 940 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 941 Log.v(TAG, "stopPreview"); 942 mCameraDevice.stopPreview(); 943 } 944 mCameraState = PREVIEW_STOPPED; 945 } 946} 947