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