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