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