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