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