PanoramaActivity.java revision 66188959498860c8b43ab7b9119c7b5c890c83dc
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 public static int roundOrientation(int orientation) { 194 return ((orientation + 45) / 90 * 90) % 360; 195 } 196 197 private class PanoOrientationEventListener extends OrientationEventListener { 198 public PanoOrientationEventListener(Context context) { 199 super(context); 200 } 201 202 @Override 203 public void onOrientationChanged(int orientation) { 204 // We keep the last known orientation. So if the user first orient 205 // the camera then point the camera to floor or sky, we still have 206 // the correct orientation. 207 if (orientation == ORIENTATION_UNKNOWN) return; 208 mDeviceOrientation = roundOrientation(orientation); 209 // When the screen is unlocked, display rotation may change. Always 210 // calculate the up-to-date orientationCompensation. 211 int orientationCompensation = mDeviceOrientation 212 + Util.getDisplayRotation(PanoramaActivity.this); 213 if (mOrientationCompensation != orientationCompensation) { 214 mOrientationCompensation = orientationCompensation; 215 setOrientationIndicator(mOrientationCompensation); 216 } 217 } 218 } 219 220 private void setOrientationIndicator(int degree) { 221 if (mSharePopup != null) mSharePopup.setOrientation(degree); 222 } 223 224 @Override 225 public boolean onCreateOptionsMenu(Menu menu) { 226 super.onCreateOptionsMenu(menu); 227 228 addBaseMenuItems(menu); 229 return true; 230 } 231 232 private void addBaseMenuItems(Menu menu) { 233 MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() { 234 public void run() { 235 switchToOtherMode(ModePicker.MODE_CAMERA); 236 } 237 }); 238 MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_VIDEO, new Runnable() { 239 public void run() { 240 switchToOtherMode(ModePicker.MODE_VIDEO); 241 } 242 }); 243 } 244 245 @Override 246 public void onCreate(Bundle icicle) { 247 super.onCreate(icicle); 248 249 Window window = getWindow(); 250 window.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 251 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 252 Util.enterLightsOutMode(window); 253 Util.initializeScreenBrightness(window, getContentResolver()); 254 255 createContentView(); 256 257 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 258 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 259 if (mSensor == null) { 260 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 261 } 262 263 mOrientationEventListener = new PanoOrientationEventListener(this); 264 265 mTransformMatrix = new float[16]; 266 267 mPreparePreviewString = 268 getResources().getString(R.string.pano_dialog_prepare_preview); 269 mDialogTitle = getResources().getString(R.string.pano_dialog_title); 270 mDialogOk = getResources().getString(R.string.dialog_ok); 271 272 mMainHandler = new Handler() { 273 @Override 274 public void handleMessage(Message msg) { 275 switch (msg.what) { 276 case MSG_LOW_RES_FINAL_MOSAIC_READY: 277 onBackgroundThreadFinished(); 278 showFinalMosaic((Bitmap) msg.obj); 279 saveHighResMosaic(); 280 break; 281 case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL: 282 onBackgroundThreadFinished(); 283 // Set the thumbnail bitmap here because mThumbnailView must be accessed 284 // from the UI thread. 285 updateThumbnailButton(); 286 287 // Share popup may still have the reference to the old thumbnail. Clear it. 288 mSharePopup = null; 289 resetToPreview(); 290 break; 291 case MSG_GENERATE_FINAL_MOSAIC_ERROR: 292 onBackgroundThreadFinished(); 293 if (mPausing) { 294 resetToPreview(); 295 } else { 296 mAlertDialog.show(); 297 } 298 break; 299 case MSG_RESET_TO_PREVIEW: 300 onBackgroundThreadFinished(); 301 resetToPreview(); 302 } 303 clearMosaicFrameProcessorIfNeeded(); 304 } 305 }; 306 307 mAlertDialog = (new AlertDialog.Builder(this)) 308 .setTitle(mDialogTitle) 309 .setMessage(R.string.pano_dialog_panorama_failed) 310 .create(); 311 mAlertDialog.setCancelable(false); 312 mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, mDialogOk, 313 new DialogInterface.OnClickListener() { 314 @Override 315 public void onClick(DialogInterface dialog, int which) { 316 dialog.dismiss(); 317 resetToPreview(); 318 } 319 }); 320 } 321 322 @Override 323 public void onStart() { 324 super.onStart(); 325 updateThumbnailButton(); 326 } 327 328 private void setupCamera() { 329 openCamera(); 330 Parameters parameters = mCameraDevice.getParameters(); 331 setupCaptureParams(parameters); 332 configureCamera(parameters); 333 } 334 335 private void releaseCamera() { 336 if (mCameraDevice != null) { 337 mCameraDevice.setPreviewCallbackWithBuffer(null); 338 CameraHolder.instance().release(); 339 mCameraDevice = null; 340 mCameraState = PREVIEW_STOPPED; 341 } 342 } 343 344 private void openCamera() { 345 try { 346 mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId()); 347 } catch (CameraHardwareException e) { 348 Util.showErrorAndFinish(this, R.string.cannot_connect_camera); 349 return; 350 } catch (CameraDisabledException e) { 351 Util.showErrorAndFinish(this, R.string.camera_disabled); 352 return; 353 } 354 } 355 356 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 357 boolean needSmaller) { 358 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 359 boolean hasFound = false; 360 for (Size size : supportedSizes) { 361 int h = size.height; 362 int w = size.width; 363 // we only want 4:3 format. 364 int d = DEFAULT_CAPTURE_PIXELS - h * w; 365 if (needSmaller && d < 0) { // no bigger preview than 960x720. 366 continue; 367 } 368 if (need4To3 && (h * 4 != w * 3)) { 369 continue; 370 } 371 d = Math.abs(d); 372 if (d < pixelsDiff) { 373 mPreviewWidth = w; 374 mPreviewHeight = h; 375 pixelsDiff = d; 376 hasFound = true; 377 } 378 } 379 return hasFound; 380 } 381 382 private void setupCaptureParams(Parameters parameters) { 383 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 384 if (!findBestPreviewSize(supportedSizes, true, true)) { 385 Log.w(TAG, "No 4:3 ratio preview size supported."); 386 if (!findBestPreviewSize(supportedSizes, false, true)) { 387 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 388 findBestPreviewSize(supportedSizes, false, false); 389 } 390 } 391 Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); 392 parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 393 394 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 395 int last = frameRates.size() - 1; 396 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 397 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 398 parameters.setPreviewFpsRange(minFps, maxFps); 399 Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); 400 401 List<String> supportedFocusModes = parameters.getSupportedFocusModes(); 402 if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) { 403 parameters.setFocusMode(mTargetFocusMode); 404 } else { 405 // Use the default focus mode and log a message 406 Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode + 407 " becuase the mode is not supported."); 408 } 409 410 parameters.setRecordingHint(false); 411 412 mHorizontalViewAngle = (((mDeviceOrientation / 90) % 2) == 0) ? 413 parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle(); 414 } 415 416 public int getPreviewBufSize() { 417 PixelFormat pixelInfo = new PixelFormat(); 418 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 419 // TODO: remove this extra 32 byte after the driver bug is fixed. 420 return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 421 } 422 423 private void configureCamera(Parameters parameters) { 424 mCameraDevice.setParameters(parameters); 425 } 426 427 private boolean switchToOtherMode(int mode) { 428 if (isFinishing()) { 429 return false; 430 } 431 MenuHelper.gotoMode(mode, this); 432 finish(); 433 return true; 434 } 435 436 public boolean onModeChanged(int mode) { 437 if (mode != ModePicker.MODE_PANORAMA) { 438 return switchToOtherMode(mode); 439 } else { 440 return true; 441 } 442 } 443 444 @Override 445 public void onMosaicSurfaceChanged() { 446 runOnUiThread(new Runnable() { 447 @Override 448 public void run() { 449 if (!mPausing) { 450 startCameraPreview(); 451 } 452 } 453 }); 454 } 455 456 @Override 457 public void onMosaicSurfaceCreated(final int textureID) { 458 runOnUiThread(new Runnable() { 459 @Override 460 public void run() { 461 if (mSurfaceTexture != null) { 462 mSurfaceTexture.release(); 463 } 464 mSurfaceTexture = new SurfaceTexture(textureID); 465 if (!mPausing) { 466 mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this); 467 } 468 } 469 }); 470 } 471 472 public void runViewFinder() { 473 mMosaicView.setWarping(false); 474 // Call preprocess to render it to low-res and high-res RGB textures. 475 mMosaicView.preprocess(mTransformMatrix); 476 mMosaicView.setReady(); 477 mMosaicView.requestRender(); 478 } 479 480 public void runMosaicCapture() { 481 mMosaicView.setWarping(true); 482 // Call preprocess to render it to low-res and high-res RGB textures. 483 mMosaicView.preprocess(mTransformMatrix); 484 // Lock the conditional variable to ensure the order of transferGPUtoCPU and 485 // mMosaicFrame.processFrame(). 486 mMosaicView.lockPreviewReadyFlag(); 487 // Now, transfer the textures from GPU to CPU memory for processing 488 mMosaicView.transferGPUtoCPU(); 489 // Wait on the condition variable (will be opened when GPU->CPU transfer is done). 490 mMosaicView.waitUntilPreviewReady(); 491 mMosaicFrameProcessor.processFrame(); 492 } 493 494 public synchronized void onFrameAvailable(SurfaceTexture surface) { 495 /* This function may be called by some random thread, 496 * so let's be safe and use synchronize. No OpenGL calls can be done here. 497 */ 498 // Updating the texture should be done in the GL thread which mMosaicView is attached. 499 mMosaicView.queueEvent(new Runnable() { 500 @Override 501 public void run() { 502 mSurfaceTexture.updateTexImage(); 503 mSurfaceTexture.getTransformMatrix(mTransformMatrix); 504 } 505 }); 506 // Update the transformation matrix for mosaic pre-process. 507 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { 508 runViewFinder(); 509 } else { 510 runMosaicCapture(); 511 } 512 } 513 514 private void hideDirectionIndicators() { 515 mLeftIndicator.setVisibility(View.GONE); 516 mRightIndicator.setVisibility(View.GONE); 517 } 518 519 private void showDirectionIndicators(int direction) { 520 switch (direction) { 521 case PanoProgressBar.DIRECTION_NONE: 522 mLeftIndicator.setVisibility(View.VISIBLE); 523 mRightIndicator.setVisibility(View.VISIBLE); 524 break; 525 case PanoProgressBar.DIRECTION_LEFT: 526 mLeftIndicator.setVisibility(View.VISIBLE); 527 mRightIndicator.setVisibility(View.GONE); 528 break; 529 case PanoProgressBar.DIRECTION_RIGHT: 530 mLeftIndicator.setVisibility(View.GONE); 531 mRightIndicator.setVisibility(View.VISIBLE); 532 break; 533 } 534 } 535 536 public void startCapture() { 537 // Reset values so we can do this again. 538 mCancelComputation = false; 539 mTimeTaken = System.currentTimeMillis(); 540 mCaptureState = CAPTURE_STATE_MOSAIC; 541 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording); 542 mCaptureIndicator.setVisibility(View.VISIBLE); 543 showDirectionIndicators(PanoProgressBar.DIRECTION_NONE); 544 mThumbnailView.setEnabled(false); 545 546 mCompassValueXStart = mCompassValueXStartBuffer; 547 mCompassValueYStart = mCompassValueYStartBuffer; 548 mMinAngleX = 0; 549 mMaxAngleX = 0; 550 mMinAngleY = 0; 551 mMaxAngleY = 0; 552 mTimestamp = 0; 553 554 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 555 @Override 556 public void onProgress(boolean isFinished, float panningRateX, float panningRateY, 557 float progressX, float progressY) { 558 if (isFinished 559 || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE) 560 || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) { 561 stopCapture(false); 562 } else { 563 updateProgress(panningRateX, progressX, progressY); 564 } 565 } 566 }); 567 568 if (mModePicker != null) mModePicker.setEnabled(false); 569 570 mPanoProgressBar.reset(); 571 // TODO: calculate the indicator width according to different devices to reflect the actual 572 // angle of view of the camera device. 573 mPanoProgressBar.setIndicatorWidth(20); 574 mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); 575 mPanoProgressBar.setVisibility(View.VISIBLE); 576 } 577 578 private void stopCapture(boolean aborted) { 579 mCaptureState = CAPTURE_STATE_VIEWFINDER; 580 mCaptureIndicator.setVisibility(View.GONE); 581 hideTooFastIndication(); 582 hideDirectionIndicators(); 583 mThumbnailView.setEnabled(true); 584 585 mMosaicFrameProcessor.setProgressListener(null); 586 stopCameraPreview(); 587 588 mSurfaceTexture.setOnFrameAvailableListener(null); 589 590 if (!aborted && !mThreadRunning) { 591 showDialog(mPreparePreviewString); 592 runBackgroundThread(new Thread() { 593 @Override 594 public void run() { 595 MosaicJpeg jpeg = generateFinalMosaic(false); 596 597 if (jpeg != null && jpeg.isValid) { 598 Bitmap bitmap = null; 599 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 600 mMainHandler.sendMessage(mMainHandler.obtainMessage( 601 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 602 } else { 603 mMainHandler.sendMessage(mMainHandler.obtainMessage( 604 MSG_RESET_TO_PREVIEW)); 605 } 606 } 607 }); 608 } 609 // do we have to wait for the thread to complete before enabling this? 610 if (mModePicker != null) mModePicker.setEnabled(true); 611 } 612 613 private void showTooFastIndication() { 614 mTooFastPrompt.setVisibility(View.VISIBLE); 615 mFastIndicationBorder.setVisibility(View.VISIBLE); 616 mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); 617 mLeftIndicator.setEnabled(true); 618 mRightIndicator.setEnabled(true); 619 } 620 621 private void hideTooFastIndication() { 622 mTooFastPrompt.setVisibility(View.GONE); 623 mFastIndicationBorder.setVisibility(View.GONE); 624 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 625 mLeftIndicator.setEnabled(false); 626 mRightIndicator.setEnabled(false); 627 } 628 629 private void updateProgress(float panningRate, float progressX, float progressY) { 630 mMosaicView.setReady(); 631 mMosaicView.requestRender(); 632 633 // TODO: Now we just display warning message by the panning speed. 634 // Since we only support horizontal panning, we should display a warning message 635 // in UI when there're significant vertical movements. 636 if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) { 637 showTooFastIndication(); 638 } else { 639 hideTooFastIndication(); 640 } 641 mPanoProgressBar.setProgress((int) (progressX * mHorizontalViewAngle)); 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 or height is equal or bigger 780 // than the screen's width or height. 781 int widthRatio = (int) Math.ceil((double) jpeg.width 782 / mPanoLayout.getWidth()); 783 int heightRatio = (int) Math.ceil((double) jpeg.height 784 / mPanoLayout.getHeight()); 785 int inSampleSize = Integer.highestOneBit( 786 Math.max(widthRatio, heightRatio)); 787 mThumbnail = Thumbnail.createThumbnail( 788 jpeg.data, orientation, inSampleSize, uri); 789 } 790 mMainHandler.sendMessage( 791 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL)); 792 } 793 } 794 }); 795 reportProgress(); 796 } 797 798 private void showDialog(String str) { 799 mProgressDialog = new ProgressDialog(this); 800 mProgressDialog.setMessage(str); 801 mProgressDialog.show(); 802 } 803 804 private void runBackgroundThread(Thread thread) { 805 mThreadRunning = true; 806 thread.start(); 807 } 808 809 private void onBackgroundThreadFinished() { 810 mThreadRunning = false; 811 if (mProgressDialog != null) { 812 mProgressDialog.dismiss(); 813 mProgressDialog = null; 814 } 815 } 816 817 private void cancelHighResComputation() { 818 mCancelComputation = true; 819 synchronized (mWaitObject) { 820 mWaitObject.notify(); 821 } 822 } 823 824 @OnClickAttr 825 public void onCancelButtonClicked(View v) { 826 if (mPausing || mSurfaceTexture == null) return; 827 cancelHighResComputation(); 828 } 829 830 @OnClickAttr 831 public void onThumbnailClicked(View v) { 832 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 833 showSharePopup(); 834 } 835 836 private void showSharePopup() { 837 if (mThumbnail == null) return; 838 Uri uri = mThumbnail.getUri(); 839 if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) { 840 // The orientation compensation is set to 0 here because we only support landscape. 841 mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 842 mOrientationCompensation, 843 findViewById(R.id.frame_layout)); 844 } 845 mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0); 846 } 847 848 private void reset() { 849 mCaptureState = CAPTURE_STATE_VIEWFINDER; 850 851 mReviewLayout.setVisibility(View.GONE); 852 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 853 mPanoProgressBar.setVisibility(View.GONE); 854 mCaptureLayout.setVisibility(View.VISIBLE); 855 mMosaicFrameProcessor.reset(); 856 857 mSurfaceTexture.setOnFrameAvailableListener(this); 858 } 859 860 private void resetToPreview() { 861 reset(); 862 if (!mPausing) startCameraPreview(); 863 } 864 865 private void showFinalMosaic(Bitmap bitmap) { 866 if (bitmap != null) { 867 mReview.setImageBitmap(bitmap); 868 } 869 mCaptureLayout.setVisibility(View.GONE); 870 mReviewLayout.setVisibility(View.VISIBLE); 871 } 872 873 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 874 if (jpegData != null) { 875 String imagePath = PanoUtil.createName( 876 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 877 return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 878 orientation, jpegData, width, height); 879 } 880 return null; 881 } 882 883 private void clearMosaicFrameProcessorIfNeeded() { 884 if (!mPausing || mThreadRunning) return; 885 mMosaicFrameProcessor.clear(); 886 } 887 888 private void initMosaicFrameProcessorIfNeeded() { 889 if (mPausing || mThreadRunning) return; 890 if (mMosaicFrameProcessor == null) { 891 // Start the activity for the first time. 892 mMosaicFrameProcessor = new MosaicFrameProcessor( 893 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 894 } 895 mMosaicFrameProcessor.initialize(); 896 } 897 898 @Override 899 protected void onPause() { 900 super.onPause(); 901 902 mPausing = true; 903 cancelHighResComputation(); 904 // Stop the capturing first. 905 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 906 stopCapture(true); 907 reset(); 908 } 909 if (mSharePopup != null) mSharePopup.dismiss(); 910 911 if (mThumbnail != null && !mThumbnail.fromFile()) { 912 mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME)); 913 } 914 915 releaseCamera(); 916 mMosaicView.onPause(); 917 clearMosaicFrameProcessorIfNeeded(); 918 mOrientationEventListener.disable(); 919 System.gc(); 920 } 921 922 @Override 923 protected void onResume() { 924 super.onResume(); 925 926 mPausing = false; 927 mOrientationEventListener.enable(); 928 929 mCaptureState = CAPTURE_STATE_VIEWFINDER; 930 setupCamera(); 931 932 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 933 // has to be decided by camera device. 934 initMosaicFrameProcessorIfNeeded(); 935 mMosaicView.onResume(); 936 } 937 938 public MosaicJpeg generateFinalMosaic(boolean highRes) { 939 if (mMosaicFrameProcessor.createMosaic(highRes) == Mosaic.MOSAIC_RET_CANCELLED) { 940 return null; 941 } 942 943 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 944 if (imageData == null) { 945 Log.e(TAG, "getFinalMosaicNV21() returned null."); 946 return new MosaicJpeg(); 947 } 948 949 int len = imageData.length - 8; 950 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 951 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 952 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 953 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 954 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 955 956 if (width <= 0 || height <= 0) { 957 // TODO: pop up a error meesage indicating that the final result is not generated. 958 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 959 height); 960 return new MosaicJpeg(); 961 } 962 963 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 964 ByteArrayOutputStream out = new ByteArrayOutputStream(); 965 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 966 try { 967 out.close(); 968 } catch (Exception e) { 969 Log.e(TAG, "Exception in storing final mosaic", e); 970 return new MosaicJpeg(); 971 } 972 return new MosaicJpeg(out.toByteArray(), width, height); 973 } 974 975 private void setPreviewTexture(SurfaceTexture surface) { 976 try { 977 mCameraDevice.setPreviewTexture(surface); 978 } catch (Throwable ex) { 979 releaseCamera(); 980 throw new RuntimeException("setPreviewTexture failed", ex); 981 } 982 } 983 984 private void startCameraPreview() { 985 // If we're previewing already, stop the preview first (this will blank 986 // the screen). 987 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 988 989 int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this), 990 CameraHolder.instance().getBackCameraId()); 991 mCameraDevice.setDisplayOrientation(orientation); 992 993 setPreviewTexture(mSurfaceTexture); 994 995 try { 996 Log.v(TAG, "startPreview"); 997 mCameraDevice.startPreview(); 998 } catch (Throwable ex) { 999 releaseCamera(); 1000 throw new RuntimeException("startPreview failed", ex); 1001 } 1002 mCameraState = PREVIEW_ACTIVE; 1003 } 1004 1005 private void stopCameraPreview() { 1006 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 1007 Log.v(TAG, "stopPreview"); 1008 mCameraDevice.stopPreview(); 1009 } 1010 mCameraState = PREVIEW_STOPPED; 1011 } 1012} 1013