PanoramaActivity.java revision 562db5a0ec57f65cf4014ce3f7dad0f9324bb2f1
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) findViewById(R.id.pano_rotate_reviewarea), 714 (Rotatable) mRotateDialog, 715 (Rotatable) mCaptureIndicator, 716 (Rotatable) mModePicker, 717 (Rotatable) mThumbnailView}; 718 for (Rotatable r : rotateLayout) { 719 r.setOrientation(270); 720 } 721 } 722 } 723 724 @Override 725 public void onShutterButtonClick() { 726 // If mSurfaceTexture == null then GL setup is not finished yet. 727 // No buttons can be pressed. 728 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 729 // Since this button will stay on the screen when capturing, we need to check the state 730 // right now. 731 switch (mCaptureState) { 732 case CAPTURE_STATE_VIEWFINDER: 733 if (mRecordSound != null) mRecordSound.play(); 734 startCapture(); 735 break; 736 case CAPTURE_STATE_MOSAIC: 737 if (mRecordSound != null) mRecordSound.play(); 738 stopCapture(false); 739 } 740 } 741 742 @Override 743 public void onShutterButtonFocus(boolean pressed) { 744 } 745 746 public void reportProgress() { 747 mSavingProgressBar.reset(); 748 mSavingProgressBar.setRightIncreasing(true); 749 Thread t = new Thread() { 750 @Override 751 public void run() { 752 while (mThreadRunning) { 753 final int progress = mMosaicFrameProcessor.reportProgress( 754 true, mCancelComputation); 755 756 try { 757 synchronized (mWaitObject) { 758 mWaitObject.wait(50); 759 } 760 } catch (InterruptedException e) { 761 throw new RuntimeException("Panorama reportProgress failed", e); 762 } 763 // Update the progress bar 764 runOnUiThread(new Runnable() { 765 public void run() { 766 mSavingProgressBar.setProgress(progress); 767 } 768 }); 769 } 770 } 771 }; 772 t.start(); 773 } 774 775 private void initThumbnailButton() { 776 // Load the thumbnail from the disk. 777 if (mThumbnail == null) { 778 mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME)); 779 } 780 updateThumbnailButton(); 781 } 782 783 private void updateThumbnailButton() { 784 // Update last image if URI is invalid and the storage is ready. 785 ContentResolver contentResolver = getContentResolver(); 786 if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) { 787 mThumbnail = Thumbnail.getLastThumbnail(contentResolver); 788 } 789 if (mThumbnail != null) { 790 mThumbnailView.setBitmap(mThumbnail.getBitmap()); 791 } else { 792 mThumbnailView.setBitmap(null); 793 } 794 } 795 796 public void saveHighResMosaic() { 797 runBackgroundThread(new Thread() { 798 @Override 799 public void run() { 800 MosaicJpeg jpeg = generateFinalMosaic(true); 801 802 if (jpeg == null) { // Cancelled by user. 803 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 804 } else if (!jpeg.isValid) { // Error when generating mosaic. 805 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 806 } else { 807 int orientation = Exif.getOrientation(jpeg.data); 808 Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); 809 if (uri != null) { 810 // Create a thumbnail whose width or height is equal or bigger 811 // than the screen's width or height. 812 int widthRatio = (int) Math.ceil((double) jpeg.width 813 / mPanoLayout.getWidth()); 814 int heightRatio = (int) Math.ceil((double) jpeg.height 815 / mPanoLayout.getHeight()); 816 int inSampleSize = Integer.highestOneBit( 817 Math.max(widthRatio, heightRatio)); 818 mThumbnail = Thumbnail.createThumbnail( 819 jpeg.data, orientation, inSampleSize, uri); 820 } 821 mMainHandler.sendMessage( 822 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL)); 823 } 824 } 825 }); 826 reportProgress(); 827 } 828 829 private void runBackgroundThread(Thread thread) { 830 mThreadRunning = true; 831 thread.start(); 832 } 833 834 private void onBackgroundThreadFinished() { 835 mThreadRunning = false; 836 mRotateDialog.dismissDialog(); 837 } 838 839 private void cancelHighResComputation() { 840 mCancelComputation = true; 841 synchronized (mWaitObject) { 842 mWaitObject.notify(); 843 } 844 } 845 846 @OnClickAttr 847 public void onCancelButtonClicked(View v) { 848 if (mPausing || mSurfaceTexture == null) return; 849 cancelHighResComputation(); 850 } 851 852 @OnClickAttr 853 public void onThumbnailClicked(View v) { 854 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 855 showSharePopup(); 856 } 857 858 private void showSharePopup() { 859 if (mThumbnail == null) return; 860 Uri uri = mThumbnail.getUri(); 861 if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) { 862 // The orientation compensation is set to 0 here because we only support landscape. 863 mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 864 mOrientationCompensation, 865 findViewById(R.id.frame_layout)); 866 } 867 mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0); 868 } 869 870 private void reset() { 871 mCaptureState = CAPTURE_STATE_VIEWFINDER; 872 873 mReviewLayout.setVisibility(View.GONE); 874 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 875 mPanoProgressBar.setVisibility(View.GONE); 876 mCaptureLayout.setVisibility(View.VISIBLE); 877 mMosaicFrameProcessor.reset(); 878 879 mSurfaceTexture.setOnFrameAvailableListener(this); 880 } 881 882 private void resetToPreview() { 883 reset(); 884 if (!mPausing) startCameraPreview(); 885 } 886 887 private void showFinalMosaic(Bitmap bitmap) { 888 if (bitmap != null) { 889 mReview.setImageBitmap(bitmap); 890 } 891 mCaptureLayout.setVisibility(View.GONE); 892 mReviewLayout.setVisibility(View.VISIBLE); 893 } 894 895 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 896 if (jpegData != null) { 897 String imagePath = PanoUtil.createName( 898 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 899 return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 900 orientation, jpegData, width, height); 901 } 902 return null; 903 } 904 905 private void clearMosaicFrameProcessorIfNeeded() { 906 if (!mPausing || mThreadRunning) return; 907 mMosaicFrameProcessor.clear(); 908 } 909 910 private void initMosaicFrameProcessorIfNeeded() { 911 if (mPausing || mThreadRunning) return; 912 if (mMosaicFrameProcessor == null) { 913 // Start the activity for the first time. 914 mMosaicFrameProcessor = new MosaicFrameProcessor( 915 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 916 } 917 mMosaicFrameProcessor.initialize(); 918 } 919 920 private void initSoundRecorder() { 921 // Construct sound player; use enforced sound output if necessary 922 File recordSoundFile = new File(VIDEO_RECORD_SOUND); 923 try { 924 ParcelFileDescriptor recordSoundParcel = 925 ParcelFileDescriptor.open(recordSoundFile, 926 ParcelFileDescriptor.MODE_READ_ONLY); 927 AssetFileDescriptor recordSoundAsset = 928 new AssetFileDescriptor(recordSoundParcel, 0, 929 AssetFileDescriptor.UNKNOWN_LENGTH); 930 if (SystemProperties.get("ro.camera.sound.forced", "0").equals("0")) { 931 mRecordSound = new SoundPlayer(recordSoundAsset, false); 932 } else { 933 mRecordSound = new SoundPlayer(recordSoundAsset, true); 934 } 935 } catch (java.io.FileNotFoundException e) { 936 Log.e(TAG, "System video record sound not found"); 937 mRecordSound = null; 938 } 939 } 940 941 private void releaseSoundRecorder() { 942 if (mRecordSound != null) { 943 mRecordSound.release(); 944 mRecordSound = null; 945 } 946 } 947 948 @Override 949 protected void onPause() { 950 super.onPause(); 951 952 mPausing = true; 953 cancelHighResComputation(); 954 // Stop the capturing first. 955 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 956 stopCapture(true); 957 reset(); 958 } 959 if (mSharePopup != null) mSharePopup.dismiss(); 960 961 if (mThumbnail != null && !mThumbnail.fromFile()) { 962 mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME)); 963 } 964 965 releaseCamera(); 966 releaseSoundRecorder(); 967 mMosaicView.onPause(); 968 clearMosaicFrameProcessorIfNeeded(); 969 mOrientationEventListener.disable(); 970 resetScreenOn(); 971 System.gc(); 972 } 973 974 @Override 975 protected void doOnResume() { 976 mPausing = false; 977 mOrientationEventListener.enable(); 978 979 mCaptureState = CAPTURE_STATE_VIEWFINDER; 980 setupCamera(); 981 982 initSoundRecorder(); 983 984 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 985 // has to be decided by camera device. 986 initMosaicFrameProcessorIfNeeded(); 987 mMosaicView.onResume(); 988 989 initThumbnailButton(); 990 keepScreenOnAwhile(); 991 } 992 993 /** 994 * Generate the final mosaic image. 995 * 996 * @param highRes flag to indicate whether we want to get a high-res version. 997 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation 998 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there 999 * is an error in generating the final mosaic. 1000 */ 1001 public MosaicJpeg generateFinalMosaic(boolean highRes) { 1002 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes); 1003 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) { 1004 return null; 1005 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) { 1006 return new MosaicJpeg(); 1007 } 1008 1009 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 1010 if (imageData == null) { 1011 Log.e(TAG, "getFinalMosaicNV21() returned null."); 1012 return new MosaicJpeg(); 1013 } 1014 1015 int len = imageData.length - 8; 1016 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 1017 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 1018 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 1019 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 1020 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 1021 1022 if (width <= 0 || height <= 0) { 1023 // TODO: pop up a error meesage indicating that the final result is not generated. 1024 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 1025 height); 1026 return new MosaicJpeg(); 1027 } 1028 1029 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 1030 ByteArrayOutputStream out = new ByteArrayOutputStream(); 1031 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 1032 try { 1033 out.close(); 1034 } catch (Exception e) { 1035 Log.e(TAG, "Exception in storing final mosaic", e); 1036 return new MosaicJpeg(); 1037 } 1038 return new MosaicJpeg(out.toByteArray(), width, height); 1039 } 1040 1041 private void setPreviewTexture(SurfaceTexture surface) { 1042 try { 1043 mCameraDevice.setPreviewTexture(surface); 1044 } catch (Throwable ex) { 1045 releaseCamera(); 1046 throw new RuntimeException("setPreviewTexture failed", ex); 1047 } 1048 } 1049 1050 private void startCameraPreview() { 1051 // If we're previewing already, stop the preview first (this will blank 1052 // the screen). 1053 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 1054 1055 // Set the display orientation to 0, so that the underlying mosaic library 1056 // can always get undistorted mPreviewWidth x mPreviewHeight image data 1057 // from SurfaceTexture. 1058 mCameraDevice.setDisplayOrientation(0); 1059 1060 setPreviewTexture(mSurfaceTexture); 1061 1062 try { 1063 Log.v(TAG, "startPreview"); 1064 mCameraDevice.startPreview(); 1065 } catch (Throwable ex) { 1066 releaseCamera(); 1067 throw new RuntimeException("startPreview failed", ex); 1068 } 1069 mCameraState = PREVIEW_ACTIVE; 1070 } 1071 1072 private void stopCameraPreview() { 1073 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 1074 Log.v(TAG, "stopPreview"); 1075 mCameraDevice.stopPreview(); 1076 } 1077 mCameraState = PREVIEW_STOPPED; 1078 } 1079 1080 @Override 1081 public void onUserInteraction() { 1082 super.onUserInteraction(); 1083 if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); 1084 } 1085 1086 private void resetScreenOn() { 1087 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1088 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1089 } 1090 1091 private void keepScreenOnAwhile() { 1092 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1093 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1094 mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1095 } 1096 1097 private void keepScreenOn() { 1098 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1099 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1100 } 1101} 1102