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