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