PanoramaActivity.java revision c3e797c34a6d3ba04caab6b0afbaecf7027e0bae
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.camera.panorama; 18 19import com.android.camera.CameraDisabledException; 20import com.android.camera.CameraHardwareException; 21import com.android.camera.CameraHolder; 22import com.android.camera.Exif; 23import com.android.camera.MenuHelper; 24import com.android.camera.ModePicker; 25import com.android.camera.OnClickAttr; 26import com.android.camera.R; 27import com.android.camera.ShutterButton; 28import com.android.camera.Storage; 29import com.android.camera.Thumbnail; 30import com.android.camera.Util; 31import com.android.camera.ui.RotateImageView; 32import com.android.camera.ui.SharePopup; 33 34import android.animation.Animator; 35import android.animation.AnimatorInflater; 36import android.animation.AnimatorSet; 37import android.animation.ObjectAnimator; 38import android.animation.ValueAnimator; 39import android.app.Activity; 40import android.app.AlertDialog; 41import android.app.ProgressDialog; 42import android.content.Context; 43import android.content.DialogInterface; 44import android.content.res.Resources; 45import android.graphics.Bitmap; 46import android.graphics.BitmapFactory; 47import android.graphics.ImageFormat; 48import android.graphics.PixelFormat; 49import android.graphics.Rect; 50import android.graphics.SurfaceTexture; 51import android.graphics.YuvImage; 52import android.hardware.Camera; 53import android.hardware.Camera.Parameters; 54import android.hardware.Camera.Size; 55import android.hardware.Sensor; 56import android.hardware.SensorEvent; 57import android.hardware.SensorEventListener; 58import android.hardware.SensorManager; 59import android.net.Uri; 60import android.os.Bundle; 61import android.os.Handler; 62import android.os.Message; 63import android.util.Log; 64import android.view.Gravity; 65import android.view.OrientationEventListener; 66import android.view.View; 67import android.view.WindowManager; 68import android.view.animation.LinearInterpolator; 69import android.widget.ImageView; 70import android.widget.RelativeLayout; 71import android.widget.TextView; 72 73import java.io.ByteArrayOutputStream; 74import java.util.List; 75 76/** 77 * Activity to handle panorama capturing. 78 */ 79public class PanoramaActivity extends Activity implements 80 ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener, 81 ShutterButton.OnShutterButtonListener, 82 MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener { 83 public static final int DEFAULT_SWEEP_ANGLE = 160; 84 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 85 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 86 87 private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1; 88 private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2; 89 private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3; 90 private static final int MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW = 4; 91 private static final int MSG_RESET_TO_PREVIEW = 5; 92 93 private static final String TAG = "PanoramaActivity"; 94 private static final int PREVIEW_STOPPED = 0; 95 private static final int PREVIEW_ACTIVE = 1; 96 private static final int CAPTURE_STATE_VIEWFINDER = 0; 97 private static final int CAPTURE_STATE_MOSAIC = 1; 98 99 // Speed is in unit of deg/sec 100 private static final float PANNING_SPEED_THRESHOLD = 20f; 101 102 // Ratio of nanosecond to second 103 private static final float NS2S = 1.0f / 1000000000.0f; 104 105 private boolean mPausing; 106 107 private View mPanoLayout; 108 private View mCaptureLayout; 109 private View mReviewLayout; 110 private ImageView mReview; 111 private PanoProgressBar mPanoProgressBar; 112 private MosaicRendererSurfaceView mMosaicView; 113 private TextView mTooFastPrompt; 114 private ShutterButton mShutterButton; 115 116 private ProgressDialog mProgressDialog; 117 private String mPreparePreviewString; 118 private String mGeneratePanoramaString; 119 private AlertDialog mAlertDialog; 120 private String mDialogTitle; 121 private String mDialogOk; 122 123 private float mCompassValueX; 124 private float mCompassValueY; 125 private float mCompassValueXStart; 126 private float mCompassValueYStart; 127 private float mCompassValueXStartBuffer; 128 private float mCompassValueYStartBuffer; 129 private int mCompassThreshold; 130 private int mTraversedAngleX; 131 private int mTraversedAngleY; 132 private long mTimestamp; 133 // Control variables for the terminate condition. 134 private int mMinAngleX; 135 private int mMaxAngleX; 136 private int mMinAngleY; 137 private int mMaxAngleY; 138 139 private RotateImageView mThumbnailView; 140 private Thumbnail mThumbnail; 141 private SharePopup mSharePopup; 142 143 private AnimatorSet mThumbnailViewAndModePickerOut; 144 private AnimatorSet mThumbnailViewAndModePickerIn; 145 146 private int mPreviewWidth; 147 private int mPreviewHeight; 148 private Camera mCameraDevice; 149 private int mCameraState; 150 private int mCaptureState; 151 private SensorManager mSensorManager; 152 private Sensor mSensor; 153 private ModePicker mModePicker; 154 private MosaicFrameProcessor mMosaicFrameProcessor; 155 private long mTimeTaken; 156 private Handler mMainHandler; 157 private SurfaceTexture mSurfaceTexture; 158 private boolean mThreadRunning; 159 private boolean mCancelComputation; 160 private float[] mTransformMatrix; 161 private float mHorizontalViewAngle; 162 163 private PanoOrientationEventListener mOrientationEventListener; 164 // The value could be 0, 1, 2, 3 for the 4 different orientations measured in clockwise 165 // respectively. 166 private int mDeviceOrientation; 167 168 private class MosaicJpeg { 169 public MosaicJpeg(byte[] data, int width, int height) { 170 this.data = data; 171 this.width = width; 172 this.height = height; 173 this.isValid = true; 174 } 175 176 public MosaicJpeg() { 177 this.data = null; 178 this.width = 0; 179 this.height = 0; 180 this.isValid = false; 181 } 182 183 public final byte[] data; 184 public final int width; 185 public final int height; 186 public final boolean isValid; 187 } 188 189 private class PanoOrientationEventListener extends OrientationEventListener { 190 public PanoOrientationEventListener(Context context) { 191 super(context); 192 } 193 194 @Override 195 public void onOrientationChanged(int orientation) { 196 // Default to the last known orientation. 197 if (orientation == ORIENTATION_UNKNOWN) return; 198 mDeviceOrientation = ((orientation + 45) / 90) % 4; 199 } 200 } 201 202 @Override 203 public void onCreate(Bundle icicle) { 204 super.onCreate(icicle); 205 206 getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 207 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 208 Util.enterLightsOutMode(getWindow()); 209 210 createContentView(); 211 212 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 213 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 214 if (mSensor == null) { 215 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 216 } 217 218 mOrientationEventListener = new PanoOrientationEventListener(this); 219 220 mTransformMatrix = new float[16]; 221 222 mPreparePreviewString = 223 getResources().getString(R.string.pano_dialog_prepare_preview); 224 mGeneratePanoramaString = 225 getResources().getString(R.string.pano_dialog_generate_panorama); 226 mDialogTitle = getResources().getString(R.string.pano_dialog_title); 227 mDialogOk = getResources().getString(R.string.dialog_ok); 228 229 mMainHandler = new Handler() { 230 @Override 231 public void handleMessage(Message msg) { 232 switch (msg.what) { 233 case MSG_LOW_RES_FINAL_MOSAIC_READY: 234 onBackgroundThreadFinished(); 235 showFinalMosaic((Bitmap) msg.obj); 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 mMosaicView.setVisibility(View.VISIBLE); 499 } 500 501 private void stopCapture() { 502 mCaptureState = CAPTURE_STATE_VIEWFINDER; 503 mTooFastPrompt.setVisibility(View.GONE); 504 505 mMosaicFrameProcessor.setProgressListener(null); 506 stopCameraPreview(); 507 508 mSurfaceTexture.setOnFrameAvailableListener(null); 509 510 if (!mThreadRunning) { 511 runBackgroundThreadAndShowDialog(mPreparePreviewString, false, 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 reportProgress(false); 528 } 529 mThumbnailViewAndModePickerIn.start(); 530 } 531 532 private void updateProgress(float panningRate) { 533 mMosaicView.setReady(); 534 mMosaicView.requestRender(); 535 536 // TODO: Now we just display warning message by the panning speed. 537 // Since we only support horizontal panning, we should display a warning message 538 // in UI when there're significant vertical movements. 539 if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) { 540 // TODO: draw speed indication according to the UI spec. 541 mTooFastPrompt.setVisibility(View.VISIBLE); 542 mTooFastPrompt.invalidate(); 543 } else { 544 mTooFastPrompt.setVisibility(View.GONE); 545 mTooFastPrompt.invalidate(); 546 } 547 } 548 549 private void createContentView() { 550 setContentView(R.layout.panorama); 551 552 mCaptureState = CAPTURE_STATE_VIEWFINDER; 553 554 Resources appRes = getResources(); 555 556 mCaptureLayout = (View) findViewById(R.id.pano_capture_layout); 557 mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_capture_view); 558 mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 559 mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); 560 mPanoProgressBar.setIndicatorColor(appRes.getColor(R.color.pano_progress_indication)); 561 mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview); 562 563 mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail); 564 565 mReviewLayout = (View) findViewById(R.id.pano_review_layout); 566 mReview = (ImageView) findViewById(R.id.pano_reviewarea); 567 mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer); 568 mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this); 569 mMosaicView.setVisibility(View.VISIBLE); 570 571 mModePicker = (ModePicker) findViewById(R.id.mode_picker); 572 mModePicker.setVisibility(View.VISIBLE); 573 mModePicker.setOnModeChangeListener(this); 574 mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA); 575 576 mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); 577 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 578 mShutterButton.setOnShutterButtonListener(this); 579 580 mPanoLayout = findViewById(R.id.pano_layout); 581 } 582 583 @Override 584 public void onShutterButtonClick(ShutterButton b) { 585 // If mSurfaceTexture == null then GL setup is not finished yet. 586 // No buttons can be pressed. 587 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 588 // Since this button will stay on the screen when capturing, we need to check the state 589 // right now. 590 switch (mCaptureState) { 591 case CAPTURE_STATE_VIEWFINDER: 592 startCapture(); 593 break; 594 case CAPTURE_STATE_MOSAIC: 595 stopCapture(); 596 } 597 } 598 599 @Override 600 public void onShutterButtonFocus(ShutterButton b, boolean pressed) { 601 } 602 603 public void reportProgress(final boolean highRes) { 604 Thread t = new Thread() { 605 @Override 606 public void run() { 607 while (mThreadRunning) { 608 final int progress = mMosaicFrameProcessor.reportProgress( 609 highRes, mCancelComputation); 610 611 try { 612 Thread.sleep(50); 613 } catch (Exception e) { 614 throw new RuntimeException("Panorama reportProgress failed", e); 615 } 616 // Update the progress bar 617 runOnUiThread(new Runnable() { 618 public void run() { 619 // Check if mProgressDialog is null because the background thread 620 // finished. 621 if (mProgressDialog != null) { 622 mProgressDialog.setProgress(progress); 623 } 624 } 625 }); 626 } 627 } 628 }; 629 t.start(); 630 } 631 632 @OnClickAttr 633 public void onOkButtonClicked(View v) { 634 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 635 runBackgroundThreadAndShowDialog(mGeneratePanoramaString, true, new Thread() { 636 @Override 637 public void run() { 638 MosaicJpeg jpeg = generateFinalMosaic(true); 639 640 if (jpeg == null) { // Cancelled by user. 641 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 642 } else if (!jpeg.isValid) { // Error when generating mosaic. 643 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 644 } else { 645 int orientation = Exif.getOrientation(jpeg.data); 646 Uri uri = savePanorama(jpeg.data, orientation); 647 if (uri != null) { 648 // Create a thumbnail whose width is equal or bigger 649 // than the entire screen. 650 int ratio = (int) Math.ceil((double) jpeg.width / 651 mPanoLayout.getWidth()); 652 int inSampleSize = Integer.highestOneBit(ratio); 653 mThumbnail = Thumbnail.createThumbnail( 654 jpeg.data, orientation, inSampleSize, uri); 655 } 656 mMainHandler.sendMessage( 657 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL)); 658 } 659 } 660 }); 661 reportProgress(true); 662 } 663 664 /** 665 * If the style is horizontal one, the maximum progress is assumed to be 100. 666 */ 667 private void runBackgroundThreadAndShowDialog( 668 String str, boolean showPercentageProgress, Thread thread) { 669 mThreadRunning = true; 670 mProgressDialog = new ProgressDialog(this); 671 mProgressDialog.setMessage(str); 672 mProgressDialog.setCancelable(false); // Don't allow back key to dismiss this dialog. 673 if (showPercentageProgress) { 674 mProgressDialog.setMax(100); 675 mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 676 // TODO: update the UI according to specs. 677 678 mProgressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", 679 new DialogInterface.OnClickListener() { 680 public void onClick(DialogInterface dialog, 681 int whichButton) { 682 mCancelComputation = true; 683 } 684 }); 685 } else { 686 mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); 687 } 688 mProgressDialog.show(); 689 thread.start(); 690 } 691 692 private void onBackgroundThreadFinished() { 693 mThreadRunning = false; 694 mProgressDialog.dismiss(); 695 mProgressDialog = null; 696 } 697 698 @OnClickAttr 699 public void onRetakeButtonClicked(View v) { 700 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 701 resetToPreview(); 702 } 703 704 @OnClickAttr 705 public void onThumbnailClicked(View v) { 706 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 707 showSharePopup(); 708 } 709 710 private void showSharePopup() { 711 if (mThumbnail == null) return; 712 Uri uri = mThumbnail.getUri(); 713 if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) { 714 // The orientation compensation is set to 0 here because we only support landscape. 715 // Panorama picture is long. Use pano_layout so the share popup can be full-screen. 716 mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 0, 717 findViewById(R.id.pano_layout)); 718 } 719 mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0); 720 } 721 722 private void resetToPreview() { 723 mCaptureState = CAPTURE_STATE_VIEWFINDER; 724 725 mReviewLayout.setVisibility(View.GONE); 726 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 727 mPanoProgressBar.setVisibility(View.GONE); 728 mCaptureLayout.setVisibility(View.VISIBLE); 729 mMosaicFrameProcessor.reset(); 730 731 mSurfaceTexture.setOnFrameAvailableListener(this); 732 733 if (!mPausing) startCameraPreview(); 734 735 mMosaicView.setVisibility(View.VISIBLE); 736 } 737 738 private void showFinalMosaic(Bitmap bitmap) { 739 if (bitmap != null) { 740 mReview.setImageBitmap(bitmap); 741 } 742 mCaptureLayout.setVisibility(View.GONE); 743 mReviewLayout.setVisibility(View.VISIBLE); 744 } 745 746 private Uri savePanorama(byte[] jpegData, int orientation) { 747 if (jpegData != null) { 748 String imagePath = PanoUtil.createName( 749 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 750 return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 751 orientation, jpegData); 752 } 753 return null; 754 } 755 756 private void clearMosaicFrameProcessorIfNeeded() { 757 if (!mPausing || mThreadRunning) return; 758 mMosaicFrameProcessor.clear(); 759 } 760 761 private void initMosaicFrameProcessorIfNeeded() { 762 if (mPausing || mThreadRunning) return; 763 if (mMosaicFrameProcessor == null) { 764 // Start the activity for the first time. 765 mMosaicFrameProcessor = new MosaicFrameProcessor( 766 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 767 } 768 mMosaicFrameProcessor.initialize(); 769 } 770 771 @Override 772 protected void onPause() { 773 super.onPause(); 774 775 releaseCamera(); 776 mPausing = true; 777 mMosaicView.onPause(); 778 mSensorManager.unregisterListener(mListener); 779 clearMosaicFrameProcessorIfNeeded(); 780 mOrientationEventListener.disable(); 781 System.gc(); 782 } 783 784 @Override 785 protected void onResume() { 786 super.onResume(); 787 788 mPausing = false; 789 mOrientationEventListener.enable(); 790 /* 791 * It is not necessary to get accelerometer events at a very high rate, 792 * by using a game rate (SENSOR_DELAY_UI), we get an automatic 793 * low-pass filter, which "extracts" the gravity component of the 794 * acceleration. As an added benefit, we use less power and CPU 795 * resources. 796 */ 797 mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI); 798 mCaptureState = CAPTURE_STATE_VIEWFINDER; 799 setupCamera(); 800 if (mSurfaceTexture != null) { 801 mSurfaceTexture.setOnFrameAvailableListener(this); 802 startCameraPreview(); 803 } 804 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 805 // has to be decided by camera device. 806 initMosaicFrameProcessorIfNeeded(); 807 mMosaicView.onResume(); 808 } 809 810 private void updateCompassValue() { 811 // By what angle has the camera moved since start of capture? 812 mTraversedAngleX = (int) (mCompassValueX - mCompassValueXStart); 813 mTraversedAngleY = (int) (mCompassValueY - mCompassValueYStart); 814 mMinAngleX = Math.min(mMinAngleX, mTraversedAngleX); 815 mMaxAngleX = Math.max(mMaxAngleX, mTraversedAngleX); 816 mMinAngleY = Math.min(mMinAngleY, mTraversedAngleY); 817 mMaxAngleY = Math.max(mMaxAngleY, mTraversedAngleY); 818 819 // Use orientation to identify if the user is panning to the right or the left. 820 switch (mDeviceOrientation) { 821 case 0: 822 mPanoProgressBar.setProgress(-mTraversedAngleX); 823 break; 824 case 1: 825 mPanoProgressBar.setProgress(mTraversedAngleY); 826 break; 827 case 2: 828 mPanoProgressBar.setProgress(mTraversedAngleX); 829 break; 830 case 3: 831 mPanoProgressBar.setProgress(-mTraversedAngleY); 832 break; 833 } 834 mPanoProgressBar.invalidate(); 835 } 836 837 private final SensorEventListener mListener = new SensorEventListener() { 838 public void onSensorChanged(SensorEvent event) { 839 if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { 840 if (mTimestamp != 0) { 841 final float dT = (event.timestamp - mTimestamp) * NS2S; 842 mCompassValueX += event.values[1] * dT * 180.0f / Math.PI; 843 mCompassValueY += event.values[0] * dT * 180.0f / Math.PI; 844 mCompassValueXStartBuffer = mCompassValueX; 845 mCompassValueYStartBuffer = mCompassValueY; 846 updateCompassValue(); 847 } 848 mTimestamp = event.timestamp; 849 850 } 851 } 852 853 @Override 854 public void onAccuracyChanged(Sensor sensor, int accuracy) { 855 } 856 }; 857 858 public MosaicJpeg generateFinalMosaic(boolean highRes) { 859 if (mMosaicFrameProcessor.createMosaic(highRes) == Mosaic.MOSAIC_RET_CANCELLED) { 860 return null; 861 } 862 863 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 864 if (imageData == null) { 865 Log.e(TAG, "getFinalMosaicNV21() returned null."); 866 return new MosaicJpeg(); 867 } 868 869 int len = imageData.length - 8; 870 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 871 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 872 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 873 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 874 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 875 876 if (width <= 0 || height <= 0) { 877 // TODO: pop up a error meesage indicating that the final result is not generated. 878 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 879 height); 880 return new MosaicJpeg(); 881 } 882 883 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 884 ByteArrayOutputStream out = new ByteArrayOutputStream(); 885 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 886 try { 887 out.close(); 888 } catch (Exception e) { 889 Log.e(TAG, "Exception in storing final mosaic", e); 890 return new MosaicJpeg(); 891 } 892 return new MosaicJpeg(out.toByteArray(), width, height); 893 } 894 895 private void setPreviewTexture(SurfaceTexture surface) { 896 try { 897 mCameraDevice.setPreviewTexture(surface); 898 } catch (Throwable ex) { 899 releaseCamera(); 900 throw new RuntimeException("setPreviewTexture failed", ex); 901 } 902 } 903 904 private void startCameraPreview() { 905 // If we're previewing already, stop the preview first (this will blank 906 // the screen). 907 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 908 909 setPreviewTexture(mSurfaceTexture); 910 911 try { 912 Log.v(TAG, "startPreview"); 913 mCameraDevice.startPreview(); 914 } catch (Throwable ex) { 915 releaseCamera(); 916 throw new RuntimeException("startPreview failed", ex); 917 } 918 mCameraState = PREVIEW_ACTIVE; 919 } 920 921 private void stopCameraPreview() { 922 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 923 Log.v(TAG, "stopPreview"); 924 mCameraDevice.stopPreview(); 925 } 926 mCameraState = PREVIEW_STOPPED; 927 } 928} 929