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