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