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