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