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