PanoramaActivity.java revision 948e75e8e3fd2ec98a08e65efdb6e5eb6d02c33b
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 // Control variables for the terminate condition. 144 private int mMinAngleX; 145 private int mMaxAngleX; 146 private int mMinAngleY; 147 private int mMaxAngleY; 148 149 private RotateImageView mThumbnailView; 150 private Thumbnail mThumbnail; 151 private SharePopup mSharePopup; 152 153 private int mPreviewWidth; 154 private int mPreviewHeight; 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 mDeviceOrientationAtCapture; 178 private int mCameraOrientation; 179 private int mOrientationCompensation; 180 181 private RotateDialogController mRotateDialog; 182 183 private class MosaicJpeg { 184 public MosaicJpeg(byte[] data, int width, int height) { 185 this.data = data; 186 this.width = width; 187 this.height = height; 188 this.isValid = true; 189 } 190 191 public MosaicJpeg() { 192 this.data = null; 193 this.width = 0; 194 this.height = 0; 195 this.isValid = false; 196 } 197 198 public final byte[] data; 199 public final int width; 200 public final int height; 201 public final boolean isValid; 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 = Util.roundOrientation(orientation, mDeviceOrientation); 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 Util.enterLightsOutMode(window); 258 Util.initializeScreenBrightness(window, getContentResolver()); 259 260 createContentView(); 261 262 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 263 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 264 if (mSensor == null) { 265 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 266 } 267 268 mOrientationEventListener = new PanoOrientationEventListener(this); 269 270 mTransformMatrix = new float[16]; 271 272 mPreparePreviewString = 273 getResources().getString(R.string.pano_dialog_prepare_preview); 274 mDialogTitle = getResources().getString(R.string.pano_dialog_title); 275 mDialogOkString = getResources().getString(R.string.dialog_ok); 276 mDialogPanoramaFailedString = 277 getResources().getString(R.string.pano_dialog_panorama_failed); 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 mRotateDialog.showAlertDialog( 304 mDialogTitle, mDialogPanoramaFailedString, 305 mDialogOkString, new Runnable() { 306 @Override 307 public void run() { 308 resetToPreview(); 309 }}, 310 null, null); 311 } 312 break; 313 case MSG_RESET_TO_PREVIEW: 314 onBackgroundThreadFinished(); 315 resetToPreview(); 316 break; 317 case MSG_CLEAR_SCREEN_DELAY: 318 getWindow().clearFlags(WindowManager.LayoutParams. 319 FLAG_KEEP_SCREEN_ON); 320 break; 321 } 322 clearMosaicFrameProcessorIfNeeded(); 323 } 324 }; 325 } 326 327 private void setupCamera() { 328 openCamera(); 329 Parameters parameters = mCameraDevice.getParameters(); 330 setupCaptureParams(parameters); 331 configureCamera(parameters); 332 } 333 334 private void releaseCamera() { 335 if (mCameraDevice != null) { 336 mCameraDevice.setPreviewCallbackWithBuffer(null); 337 CameraHolder.instance().release(); 338 mCameraDevice = null; 339 mCameraState = PREVIEW_STOPPED; 340 } 341 } 342 343 private void openCamera() { 344 try { 345 int backCameraId = CameraHolder.instance().getBackCameraId(); 346 mCameraDevice = Util.openCamera(this, backCameraId); 347 mCameraOrientation = Util.getCameraOrientation(backCameraId); 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 mThumbnailView.setEnabled(false); 546 547 mCompassValueXStart = mCompassValueXStartBuffer; 548 mCompassValueYStart = mCompassValueYStartBuffer; 549 mMinAngleX = 0; 550 mMaxAngleX = 0; 551 mMinAngleY = 0; 552 mMaxAngleY = 0; 553 mTimestamp = 0; 554 555 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 556 @Override 557 public void onProgress(boolean isFinished, float panningRateX, float panningRateY, 558 float progressX, float progressY) { 559 if (isFinished 560 || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE) 561 || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) { 562 stopCapture(false); 563 } else { 564 updateProgress(panningRateX, progressX, progressY); 565 } 566 } 567 }); 568 569 if (mModePicker != null) mModePicker.setEnabled(false); 570 571 mPanoProgressBar.reset(); 572 // TODO: calculate the indicator width according to different devices to reflect the actual 573 // angle of view of the camera device. 574 mPanoProgressBar.setIndicatorWidth(20); 575 mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); 576 mPanoProgressBar.setVisibility(View.VISIBLE); 577 mDeviceOrientationAtCapture = mDeviceOrientation; 578 keepScreenOn(); 579 } 580 581 private void stopCapture(boolean aborted) { 582 mCaptureState = CAPTURE_STATE_VIEWFINDER; 583 mCaptureIndicator.setVisibility(View.GONE); 584 hideTooFastIndication(); 585 hideDirectionIndicators(); 586 mThumbnailView.setEnabled(true); 587 588 mMosaicFrameProcessor.setProgressListener(null); 589 stopCameraPreview(); 590 591 mSurfaceTexture.setOnFrameAvailableListener(null); 592 593 if (!aborted && !mThreadRunning) { 594 mRotateDialog.showWaitingDialog(mPreparePreviewString); 595 runBackgroundThread(new Thread() { 596 @Override 597 public void run() { 598 MosaicJpeg jpeg = generateFinalMosaic(false); 599 600 if (jpeg != null && jpeg.isValid) { 601 Bitmap bitmap = null; 602 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 603 mMainHandler.sendMessage(mMainHandler.obtainMessage( 604 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 605 } else { 606 mMainHandler.sendMessage(mMainHandler.obtainMessage( 607 MSG_RESET_TO_PREVIEW)); 608 } 609 } 610 }); 611 } 612 // do we have to wait for the thread to complete before enabling this? 613 if (mModePicker != null) mModePicker.setEnabled(true); 614 keepScreenOnAwhile(); 615 } 616 617 private void showTooFastIndication() { 618 mTooFastPrompt.setVisibility(View.VISIBLE); 619 mFastIndicationBorder.setVisibility(View.VISIBLE); 620 mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); 621 mLeftIndicator.setEnabled(true); 622 mRightIndicator.setEnabled(true); 623 } 624 625 private void hideTooFastIndication() { 626 mTooFastPrompt.setVisibility(View.GONE); 627 mFastIndicationBorder.setVisibility(View.GONE); 628 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 629 mLeftIndicator.setEnabled(false); 630 mRightIndicator.setEnabled(false); 631 } 632 633 private void updateProgress(float panningRate, float progressX, float progressY) { 634 mMosaicView.setReady(); 635 mMosaicView.requestRender(); 636 637 // TODO: Now we just display warning message by the panning speed. 638 // Since we only support horizontal panning, we should display a warning message 639 // in UI when there're significant vertical movements. 640 if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) { 641 showTooFastIndication(); 642 } else { 643 hideTooFastIndication(); 644 } 645 mPanoProgressBar.setProgress((int) (progressX * mHorizontalViewAngle)); 646 } 647 648 private void createContentView() { 649 setContentView(R.layout.panorama); 650 651 mCaptureState = CAPTURE_STATE_VIEWFINDER; 652 653 Resources appRes = getResources(); 654 655 mCaptureLayout = (View) findViewById(R.id.pano_capture_layout); 656 mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar); 657 mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 658 mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); 659 mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); 660 mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); 661 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 662 mPanoProgressBar.setOnDirectionChangeListener( 663 new PanoProgressBar.OnDirectionChangeListener () { 664 @Override 665 public void onDirectionChange(int direction) { 666 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 667 showDirectionIndicators(direction); 668 } 669 } 670 }); 671 672 mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator); 673 mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator); 674 mLeftIndicator.setEnabled(false); 675 mRightIndicator.setEnabled(false); 676 mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview); 677 mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border); 678 679 mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar); 680 mSavingProgressBar.setIndicatorWidth(0); 681 mSavingProgressBar.setMaxProgress(100); 682 mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 683 mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication)); 684 685 mCaptureIndicator = (RotateLayout) findViewById(R.id.pano_capture_indicator); 686 687 mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail); 688 mThumbnailView.enableFilter(false); 689 690 mReviewLayout = (View) findViewById(R.id.pano_review_layout); 691 mReview = (ImageView) findViewById(R.id.pano_reviewarea); 692 mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer); 693 mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this); 694 695 mModePicker = (ModePicker) findViewById(R.id.mode_picker); 696 mModePicker.setVisibility(View.VISIBLE); 697 mModePicker.setOnModeChangeListener(this); 698 mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA); 699 700 mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); 701 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 702 mShutterButton.setOnShutterButtonListener(this); 703 704 mPanoLayout = findViewById(R.id.pano_layout); 705 706 mRotateDialog = new RotateDialogController(this, R.layout.rotate_dialog); 707 708 if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) { 709 Rotatable[] rotateLayout = { 710 (Rotatable) findViewById(R.id.pano_pan_progress_bar_layout), 711 (Rotatable) findViewById(R.id.pano_capture_too_fast_textview_layout), 712 (Rotatable) findViewById(R.id.pano_review_saving_indication_layout), 713 (Rotatable) findViewById(R.id.pano_saving_progress_bar_layout), 714 (Rotatable) findViewById(R.id.pano_review_cancel_button_layout), 715 (Rotatable) findViewById(R.id.pano_rotate_reviewarea), 716 (Rotatable) mRotateDialog, 717 (Rotatable) mCaptureIndicator, 718 (Rotatable) mModePicker, 719 (Rotatable) mThumbnailView}; 720 for (Rotatable r : rotateLayout) { 721 r.setOrientation(270); 722 } 723 } 724 } 725 726 @Override 727 public void onShutterButtonClick() { 728 // If mSurfaceTexture == null then GL setup is not finished yet. 729 // No buttons can be pressed. 730 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 731 // Since this button will stay on the screen when capturing, we need to check the state 732 // right now. 733 switch (mCaptureState) { 734 case CAPTURE_STATE_VIEWFINDER: 735 mCameraDevice.playSound(Sound.START_VIDEO_RECORDING); 736 startCapture(); 737 break; 738 case CAPTURE_STATE_MOSAIC: 739 mCameraDevice.playSound(Sound.STOP_VIDEO_RECORDING); 740 stopCapture(false); 741 } 742 } 743 744 @Override 745 public void onShutterButtonFocus(boolean pressed) { 746 } 747 748 public void reportProgress() { 749 mSavingProgressBar.reset(); 750 mSavingProgressBar.setRightIncreasing(true); 751 Thread t = new Thread() { 752 @Override 753 public void run() { 754 while (mThreadRunning) { 755 final int progress = mMosaicFrameProcessor.reportProgress( 756 true, mCancelComputation); 757 758 try { 759 synchronized (mWaitObject) { 760 mWaitObject.wait(50); 761 } 762 } catch (InterruptedException e) { 763 throw new RuntimeException("Panorama reportProgress failed", e); 764 } 765 // Update the progress bar 766 runOnUiThread(new Runnable() { 767 public void run() { 768 mSavingProgressBar.setProgress(progress); 769 } 770 }); 771 } 772 } 773 }; 774 t.start(); 775 } 776 777 private void initThumbnailButton() { 778 // Load the thumbnail from the disk. 779 if (mThumbnail == null) { 780 mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME)); 781 } 782 updateThumbnailButton(); 783 } 784 785 private void updateThumbnailButton() { 786 // Update last image if URI is invalid and the storage is ready. 787 ContentResolver contentResolver = getContentResolver(); 788 if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) { 789 mThumbnail = Thumbnail.getLastThumbnail(contentResolver); 790 } 791 if (mThumbnail != null) { 792 mThumbnailView.setBitmap(mThumbnail.getBitmap()); 793 } else { 794 mThumbnailView.setBitmap(null); 795 } 796 } 797 798 public void saveHighResMosaic() { 799 runBackgroundThread(new Thread() { 800 @Override 801 public void run() { 802 MosaicJpeg jpeg = generateFinalMosaic(true); 803 804 if (jpeg == null) { // Cancelled by user. 805 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 806 } else if (!jpeg.isValid) { // Error when generating mosaic. 807 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 808 } else { 809 // The panorama image returned from the library is orientated based on the 810 // natural orientation of a camera. We need to set an orientation for the image 811 // in its EXIF header, so the image can be displayed correctly. 812 // The orientation is calculated from compensating the 813 // device orientation at capture and the camera orientation respective to 814 // the natural orientation of the device. 815 int orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360; 816 Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); 817 if (uri != null) { 818 // Create a thumbnail whose width or height is equal or bigger 819 // than the screen's width or height. 820 int widthRatio = (int) Math.ceil((double) jpeg.width 821 / mPanoLayout.getWidth()); 822 int heightRatio = (int) Math.ceil((double) jpeg.height 823 / mPanoLayout.getHeight()); 824 int inSampleSize = Integer.highestOneBit( 825 Math.max(widthRatio, heightRatio)); 826 mThumbnail = Thumbnail.createThumbnail( 827 jpeg.data, orientation, inSampleSize, uri); 828 } 829 mMainHandler.sendMessage( 830 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL)); 831 } 832 } 833 }); 834 reportProgress(); 835 } 836 837 private void runBackgroundThread(Thread thread) { 838 mThreadRunning = true; 839 thread.start(); 840 } 841 842 private void onBackgroundThreadFinished() { 843 mThreadRunning = false; 844 mRotateDialog.dismissDialog(); 845 } 846 847 private void cancelHighResComputation() { 848 mCancelComputation = true; 849 synchronized (mWaitObject) { 850 mWaitObject.notify(); 851 } 852 } 853 854 @OnClickAttr 855 public void onCancelButtonClicked(View v) { 856 if (mPausing || mSurfaceTexture == null) return; 857 cancelHighResComputation(); 858 } 859 860 @OnClickAttr 861 public void onThumbnailClicked(View v) { 862 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 863 showSharePopup(); 864 } 865 866 private void showSharePopup() { 867 if (mThumbnail == null) return; 868 Uri uri = mThumbnail.getUri(); 869 if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) { 870 // The orientation compensation is set to 0 here because we only support landscape. 871 mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 872 mOrientationCompensation, 873 findViewById(R.id.frame_layout)); 874 } 875 mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0); 876 } 877 878 private void reset() { 879 mCaptureState = CAPTURE_STATE_VIEWFINDER; 880 881 mReviewLayout.setVisibility(View.GONE); 882 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 883 mPanoProgressBar.setVisibility(View.GONE); 884 mCaptureLayout.setVisibility(View.VISIBLE); 885 mMosaicFrameProcessor.reset(); 886 887 mSurfaceTexture.setOnFrameAvailableListener(this); 888 } 889 890 private void resetToPreview() { 891 reset(); 892 if (!mPausing) startCameraPreview(); 893 } 894 895 private void showFinalMosaic(Bitmap bitmap) { 896 if (bitmap != null) { 897 mReview.setImageBitmap(bitmap); 898 } 899 mCaptureLayout.setVisibility(View.GONE); 900 mReviewLayout.setVisibility(View.VISIBLE); 901 } 902 903 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 904 if (jpegData != null) { 905 String filename = PanoUtil.createName( 906 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 907 Uri uri = Storage.addImage(getContentResolver(), filename, mTimeTaken, null, 908 orientation, jpegData, width, height); 909 if (uri != null && orientation != 0) { 910 String filepath = Storage.generateFilepath(filename); 911 try { 912 // Save the orientation in EXIF. 913 ExifInterface exif = new ExifInterface(filepath); 914 exif.setAttribute(ExifInterface.TAG_ORIENTATION, 915 getExifOrientation(orientation)); 916 exif.saveAttributes(); 917 } catch (IOException e) { 918 Log.e(TAG, "cannot set exif data: " + filepath); 919 } 920 } 921 return uri; 922 } 923 return null; 924 } 925 926 private static String getExifOrientation(int orientation) { 927 switch (orientation) { 928 case 0: 929 return String.valueOf(ExifInterface.ORIENTATION_NORMAL); 930 case 90: 931 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90); 932 case 180: 933 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180); 934 case 270: 935 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270); 936 default: 937 throw new AssertionError("invalid: " + orientation); 938 } 939 } 940 941 private void clearMosaicFrameProcessorIfNeeded() { 942 if (!mPausing || mThreadRunning) return; 943 mMosaicFrameProcessor.clear(); 944 } 945 946 private void initMosaicFrameProcessorIfNeeded() { 947 if (mPausing || mThreadRunning) return; 948 if (mMosaicFrameProcessor == null) { 949 // Start the activity for the first time. 950 mMosaicFrameProcessor = new MosaicFrameProcessor( 951 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 952 } 953 mMosaicFrameProcessor.initialize(); 954 } 955 956 @Override 957 protected void onPause() { 958 super.onPause(); 959 960 mPausing = true; 961 cancelHighResComputation(); 962 // Stop the capturing first. 963 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 964 stopCapture(true); 965 reset(); 966 } 967 if (mSharePopup != null) mSharePopup.dismiss(); 968 969 if (mThumbnail != null && !mThumbnail.fromFile()) { 970 mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME)); 971 } 972 973 releaseCamera(); 974 mMosaicView.onPause(); 975 clearMosaicFrameProcessorIfNeeded(); 976 mOrientationEventListener.disable(); 977 resetScreenOn(); 978 System.gc(); 979 } 980 981 @Override 982 protected void doOnResume() { 983 mPausing = false; 984 mOrientationEventListener.enable(); 985 986 mCaptureState = CAPTURE_STATE_VIEWFINDER; 987 setupCamera(); 988 989 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 990 // has to be decided by camera device. 991 initMosaicFrameProcessorIfNeeded(); 992 mMosaicView.onResume(); 993 994 initThumbnailButton(); 995 keepScreenOnAwhile(); 996 } 997 998 /** 999 * Generate the final mosaic image. 1000 * 1001 * @param highRes flag to indicate whether we want to get a high-res version. 1002 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation 1003 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there 1004 * is an error in generating the final mosaic. 1005 */ 1006 public MosaicJpeg generateFinalMosaic(boolean highRes) { 1007 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes); 1008 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) { 1009 return null; 1010 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) { 1011 return new MosaicJpeg(); 1012 } 1013 1014 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 1015 if (imageData == null) { 1016 Log.e(TAG, "getFinalMosaicNV21() returned null."); 1017 return new MosaicJpeg(); 1018 } 1019 1020 int len = imageData.length - 8; 1021 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 1022 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 1023 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 1024 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 1025 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 1026 1027 if (width <= 0 || height <= 0) { 1028 // TODO: pop up a error meesage indicating that the final result is not generated. 1029 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 1030 height); 1031 return new MosaicJpeg(); 1032 } 1033 1034 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 1035 ByteArrayOutputStream out = new ByteArrayOutputStream(); 1036 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 1037 try { 1038 out.close(); 1039 } catch (Exception e) { 1040 Log.e(TAG, "Exception in storing final mosaic", e); 1041 return new MosaicJpeg(); 1042 } 1043 return new MosaicJpeg(out.toByteArray(), width, height); 1044 } 1045 1046 private void setPreviewTexture(SurfaceTexture surface) { 1047 try { 1048 mCameraDevice.setPreviewTexture(surface); 1049 } catch (Throwable ex) { 1050 releaseCamera(); 1051 throw new RuntimeException("setPreviewTexture failed", ex); 1052 } 1053 } 1054 1055 private void startCameraPreview() { 1056 // If we're previewing already, stop the preview first (this will blank 1057 // the screen). 1058 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 1059 1060 // Set the display orientation to 0, so that the underlying mosaic library 1061 // can always get undistorted mPreviewWidth x mPreviewHeight image data 1062 // from SurfaceTexture. 1063 mCameraDevice.setDisplayOrientation(0); 1064 1065 setPreviewTexture(mSurfaceTexture); 1066 1067 try { 1068 Log.v(TAG, "startPreview"); 1069 mCameraDevice.startPreview(); 1070 } catch (Throwable ex) { 1071 releaseCamera(); 1072 throw new RuntimeException("startPreview failed", ex); 1073 } 1074 mCameraState = PREVIEW_ACTIVE; 1075 } 1076 1077 private void stopCameraPreview() { 1078 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 1079 Log.v(TAG, "stopPreview"); 1080 mCameraDevice.stopPreview(); 1081 } 1082 mCameraState = PREVIEW_STOPPED; 1083 } 1084 1085 @Override 1086 public void onUserInteraction() { 1087 super.onUserInteraction(); 1088 if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); 1089 } 1090 1091 private void resetScreenOn() { 1092 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1093 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1094 } 1095 1096 private void keepScreenOnAwhile() { 1097 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1098 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1099 mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1100 } 1101 1102 private void keepScreenOn() { 1103 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1104 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1105 } 1106} 1107