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