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