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