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