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