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