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