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