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