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.ShutterButton; 29 import com.android.camera.SoundPlayer; 30 import com.android.camera.Storage; 31 import com.android.camera.Thumbnail; 32 import com.android.camera.Util; 33 import com.android.camera.ui.RotateImageView; 34 import com.android.camera.ui.SharePopup; 35 36 import android.app.AlertDialog; 37 import android.app.ProgressDialog; 38 import android.content.ContentResolver; 39 import android.content.Context; 40 import android.content.DialogInterface; 41 import android.content.res.AssetFileDescriptor; 42 import android.content.res.Resources; 43 import android.graphics.Bitmap; 44 import android.graphics.BitmapFactory; 45 import android.graphics.ImageFormat; 46 import android.graphics.PixelFormat; 47 import android.graphics.Rect; 48 import android.graphics.SurfaceTexture; 49 import android.graphics.YuvImage; 50 import android.hardware.Camera; 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.net.Uri; 56 import android.os.Bundle; 57 import android.os.Handler; 58 import android.os.Message; 59 import android.os.ParcelFileDescriptor; 60 import android.os.SystemProperties; 61 import android.util.Log; 62 import android.view.Gravity; 63 import android.view.Menu; 64 import android.view.OrientationEventListener; 65 import android.view.View; 66 import android.view.Window; 67 import android.view.WindowManager; 68 import android.widget.ImageView; 69 import android.widget.TextView; 70 71 import java.io.ByteArrayOutputStream; 72 import java.io.FileNotFoundException; 73 import java.io.File; 74 import java.util.List; 75 76 /** 77 * Activity to handle panorama capturing. 78 */ 79 public 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 92 private static final String TAG = "PanoramaActivity"; 93 private static final int PREVIEW_STOPPED = 0; 94 private static final int PREVIEW_ACTIVE = 1; 95 private static final int CAPTURE_STATE_VIEWFINDER = 0; 96 private static final int CAPTURE_STATE_MOSAIC = 1; 97 98 // Speed is in unit of deg/sec 99 private static final float PANNING_SPEED_THRESHOLD = 20f; 100 101 // Ratio of nanosecond to second 102 private static final float NS2S = 1.0f / 1000000000.0f; 103 104 private static final String VIDEO_RECORD_SOUND = "/system/media/audio/ui/VideoRecord.ogg"; 105 106 private boolean mPausing; 107 108 private View mPanoLayout; 109 private View mCaptureLayout; 110 private View mReviewLayout; 111 private ImageView mReview; 112 private TextView 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 AlertDialog mAlertDialog; 125 private ProgressDialog mProgressDialog; 126 private String mDialogTitle; 127 private String mDialogOk; 128 129 private int mIndicatorColor; 130 private int mIndicatorColorFast; 131 132 private float mCompassValueX; 133 private float mCompassValueY; 134 private float mCompassValueXStart; 135 private float mCompassValueYStart; 136 private float mCompassValueXStartBuffer; 137 private float mCompassValueYStartBuffer; 138 private int mCompassThreshold; 139 private int mTraversedAngleX; 140 private int mTraversedAngleY; 141 private long mTimestamp; 142 // Control variables for the terminate condition. 143 private int mMinAngleX; 144 private int mMaxAngleX; 145 private int mMinAngleY; 146 private int mMaxAngleY; 147 148 private RotateImageView mThumbnailView; 149 private Thumbnail mThumbnail; 150 private SharePopup mSharePopup; 151 152 private int mPreviewWidth; 153 private int mPreviewHeight; 154 private int mCameraState; 155 private int mCaptureState; 156 private SensorManager mSensorManager; 157 private Sensor mSensor; 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 168 private SoundPlayer mRecordSound; 169 170 // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of 171 // getting a better image quality by the former. 172 private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY; 173 174 private PanoOrientationEventListener mOrientationEventListener; 175 // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise 176 // respectively. 177 private int mDeviceOrientation; 178 private int mOrientationCompensation; 179 180 private class MosaicJpeg { 181 public MosaicJpeg(byte[] data, int width, int height) { 182 this.data = data; 183 this.width = width; 184 this.height = height; 185 this.isValid = true; 186 } 187 188 public MosaicJpeg() { 189 this.data = null; 190 this.width = 0; 191 this.height = 0; 192 this.isValid = false; 193 } 194 195 public final byte[] data; 196 public final int width; 197 public final int height; 198 public final boolean isValid; 199 } 200 201 private class PanoOrientationEventListener extends OrientationEventListener { 202 public PanoOrientationEventListener(Context context) { 203 super(context); 204 } 205 206 @Override 207 public void onOrientationChanged(int orientation) { 208 // We keep the last known orientation. So if the user first orient 209 // the camera then point the camera to floor or sky, we still have 210 // the correct orientation. 211 if (orientation == ORIENTATION_UNKNOWN) return; 212 mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation); 213 // When the screen is unlocked, display rotation may change. Always 214 // calculate the up-to-date orientationCompensation. 215 int orientationCompensation = mDeviceOrientation 216 + Util.getDisplayRotation(PanoramaActivity.this); 217 if (mOrientationCompensation != orientationCompensation) { 218 mOrientationCompensation = orientationCompensation; 219 setOrientationIndicator(mOrientationCompensation); 220 } 221 } 222 } 223 224 private void setOrientationIndicator(int degree) { 225 if (mSharePopup != null) mSharePopup.setOrientation(degree); 226 } 227 228 @Override 229 public boolean onCreateOptionsMenu(Menu menu) { 230 super.onCreateOptionsMenu(menu); 231 232 addBaseMenuItems(menu); 233 return true; 234 } 235 236 private void addBaseMenuItems(Menu menu) { 237 MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() { 238 public void run() { 239 switchToOtherMode(ModePicker.MODE_CAMERA); 240 } 241 }); 242 MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_VIDEO, new Runnable() { 243 public void run() { 244 switchToOtherMode(ModePicker.MODE_VIDEO); 245 } 246 }); 247 } 248 249 @Override 250 public void onCreate(Bundle icicle) { 251 super.onCreate(icicle); 252 253 Window window = getWindow(); 254 window.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 255 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 256 Util.enterLightsOutMode(window); 257 Util.initializeScreenBrightness(window, getContentResolver()); 258 259 createContentView(); 260 261 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 262 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 263 if (mSensor == null) { 264 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 265 } 266 267 mOrientationEventListener = new PanoOrientationEventListener(this); 268 269 mTransformMatrix = new float[16]; 270 271 mPreparePreviewString = 272 getResources().getString(R.string.pano_dialog_prepare_preview); 273 mDialogTitle = getResources().getString(R.string.pano_dialog_title); 274 mDialogOk = getResources().getString(R.string.dialog_ok); 275 276 mMainHandler = new Handler() { 277 @Override 278 public void handleMessage(Message msg) { 279 switch (msg.what) { 280 case MSG_LOW_RES_FINAL_MOSAIC_READY: 281 onBackgroundThreadFinished(); 282 showFinalMosaic((Bitmap) msg.obj); 283 saveHighResMosaic(); 284 break; 285 case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL: 286 onBackgroundThreadFinished(); 287 // Set the thumbnail bitmap here because mThumbnailView must be accessed 288 // from the UI thread. 289 updateThumbnailButton(); 290 291 // Share popup may still have the reference to the old thumbnail. Clear it. 292 mSharePopup = null; 293 resetToPreview(); 294 break; 295 case MSG_GENERATE_FINAL_MOSAIC_ERROR: 296 onBackgroundThreadFinished(); 297 if (mPausing) { 298 resetToPreview(); 299 } else { 300 mAlertDialog.show(); 301 } 302 break; 303 case MSG_RESET_TO_PREVIEW: 304 onBackgroundThreadFinished(); 305 resetToPreview(); 306 } 307 clearMosaicFrameProcessorIfNeeded(); 308 } 309 }; 310 311 mAlertDialog = (new AlertDialog.Builder(this)) 312 .setTitle(mDialogTitle) 313 .setMessage(R.string.pano_dialog_panorama_failed) 314 .create(); 315 mAlertDialog.setCancelable(false); 316 mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, mDialogOk, 317 new DialogInterface.OnClickListener() { 318 @Override 319 public void onClick(DialogInterface dialog, int which) { 320 dialog.dismiss(); 321 resetToPreview(); 322 } 323 }); 324 } 325 326 @Override 327 public void onStart() { 328 super.onStart(); 329 updateThumbnailButton(); 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 } 581 582 private void stopCapture(boolean aborted) { 583 mCaptureState = CAPTURE_STATE_VIEWFINDER; 584 mCaptureIndicator.setVisibility(View.GONE); 585 hideTooFastIndication(); 586 hideDirectionIndicators(); 587 mThumbnailView.setEnabled(true); 588 589 mMosaicFrameProcessor.setProgressListener(null); 590 stopCameraPreview(); 591 592 mSurfaceTexture.setOnFrameAvailableListener(null); 593 594 if (!aborted && !mThreadRunning) { 595 showDialog(mPreparePreviewString); 596 runBackgroundThread(new Thread() { 597 @Override 598 public void run() { 599 MosaicJpeg jpeg = generateFinalMosaic(false); 600 601 if (jpeg != null && jpeg.isValid) { 602 Bitmap bitmap = null; 603 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 604 mMainHandler.sendMessage(mMainHandler.obtainMessage( 605 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 606 } else { 607 mMainHandler.sendMessage(mMainHandler.obtainMessage( 608 MSG_RESET_TO_PREVIEW)); 609 } 610 } 611 }); 612 } 613 // do we have to wait for the thread to complete before enabling this? 614 if (mModePicker != null) mModePicker.setEnabled(true); 615 } 616 617 private void showTooFastIndication() { 618 mTooFastPrompt.setVisibility(View.VISIBLE); 619 mFastIndicationBorder.setVisibility(View.VISIBLE); 620 mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); 621 mLeftIndicator.setEnabled(true); 622 mRightIndicator.setEnabled(true); 623 } 624 625 private void hideTooFastIndication() { 626 mTooFastPrompt.setVisibility(View.GONE); 627 mFastIndicationBorder.setVisibility(View.GONE); 628 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 629 mLeftIndicator.setEnabled(false); 630 mRightIndicator.setEnabled(false); 631 } 632 633 private void updateProgress(float panningRate, float progressX, float progressY) { 634 mMosaicView.setReady(); 635 mMosaicView.requestRender(); 636 637 // TODO: Now we just display warning message by the panning speed. 638 // Since we only support horizontal panning, we should display a warning message 639 // in UI when there're significant vertical movements. 640 if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) { 641 showTooFastIndication(); 642 } else { 643 hideTooFastIndication(); 644 } 645 mPanoProgressBar.setProgress((int) (progressX * mHorizontalViewAngle)); 646 } 647 648 private void createContentView() { 649 setContentView(R.layout.panorama); 650 651 mCaptureState = CAPTURE_STATE_VIEWFINDER; 652 653 Resources appRes = getResources(); 654 655 mCaptureLayout = (View) findViewById(R.id.pano_capture_layout); 656 mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar); 657 mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 658 mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); 659 mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); 660 mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); 661 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 662 mPanoProgressBar.setOnDirectionChangeListener( 663 new PanoProgressBar.OnDirectionChangeListener () { 664 @Override 665 public void onDirectionChange(int direction) { 666 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 667 showDirectionIndicators(direction); 668 } 669 } 670 }); 671 672 mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator); 673 mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator); 674 mLeftIndicator.setEnabled(false); 675 mRightIndicator.setEnabled(false); 676 mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview); 677 mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border); 678 679 mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar); 680 mSavingProgressBar.setIndicatorWidth(0); 681 mSavingProgressBar.setMaxProgress(100); 682 mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 683 mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication)); 684 685 mCaptureIndicator = (TextView) findViewById(R.id.pano_capture_indicator); 686 687 mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail); 688 mThumbnailView.enableFilter(false); 689 690 mReviewLayout = (View) findViewById(R.id.pano_review_layout); 691 mReview = (ImageView) findViewById(R.id.pano_reviewarea); 692 mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer); 693 mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this); 694 695 mModePicker = (ModePicker) findViewById(R.id.mode_picker); 696 mModePicker.setVisibility(View.VISIBLE); 697 mModePicker.setOnModeChangeListener(this); 698 mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA); 699 700 mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); 701 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 702 mShutterButton.setOnShutterButtonListener(this); 703 704 mPanoLayout = findViewById(R.id.pano_layout); 705 } 706 707 @Override 708 public void onShutterButtonClick() { 709 // If mSurfaceTexture == null then GL setup is not finished yet. 710 // No buttons can be pressed. 711 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 712 // Since this button will stay on the screen when capturing, we need to check the state 713 // right now. 714 switch (mCaptureState) { 715 case CAPTURE_STATE_VIEWFINDER: 716 if (mRecordSound != null) mRecordSound.play(); 717 startCapture(); 718 break; 719 case CAPTURE_STATE_MOSAIC: 720 if (mRecordSound != null) mRecordSound.play(); 721 stopCapture(false); 722 } 723 } 724 725 @Override 726 public void onShutterButtonFocus(boolean pressed) { 727 } 728 729 public void reportProgress() { 730 mSavingProgressBar.reset(); 731 mSavingProgressBar.setRightIncreasing(true); 732 Thread t = new Thread() { 733 @Override 734 public void run() { 735 while (mThreadRunning) { 736 final int progress = mMosaicFrameProcessor.reportProgress( 737 true, mCancelComputation); 738 739 try { 740 synchronized (mWaitObject) { 741 mWaitObject.wait(50); 742 } 743 } catch (InterruptedException e) { 744 throw new RuntimeException("Panorama reportProgress failed", e); 745 } 746 // Update the progress bar 747 runOnUiThread(new Runnable() { 748 public void run() { 749 mSavingProgressBar.setProgress(progress); 750 } 751 }); 752 } 753 } 754 }; 755 t.start(); 756 } 757 758 private void updateThumbnailButton() { 759 // Update last image if URI is invalid and the storage is ready. 760 ContentResolver contentResolver = getContentResolver(); 761 if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) { 762 mThumbnail = Thumbnail.getLastThumbnail(contentResolver); 763 } 764 if (mThumbnail != null) { 765 mThumbnailView.setBitmap(mThumbnail.getBitmap()); 766 } else { 767 mThumbnailView.setBitmap(null); 768 } 769 } 770 771 public void saveHighResMosaic() { 772 runBackgroundThread(new Thread() { 773 @Override 774 public void run() { 775 MosaicJpeg jpeg = generateFinalMosaic(true); 776 777 if (jpeg == null) { // Cancelled by user. 778 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 779 } else if (!jpeg.isValid) { // Error when generating mosaic. 780 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 781 } else { 782 int orientation = Exif.getOrientation(jpeg.data); 783 Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); 784 if (uri != null) { 785 // Create a thumbnail whose width or height is equal or bigger 786 // than the screen's width or height. 787 int widthRatio = (int) Math.ceil((double) jpeg.width 788 / mPanoLayout.getWidth()); 789 int heightRatio = (int) Math.ceil((double) jpeg.height 790 / mPanoLayout.getHeight()); 791 int inSampleSize = Integer.highestOneBit( 792 Math.max(widthRatio, heightRatio)); 793 mThumbnail = Thumbnail.createThumbnail( 794 jpeg.data, orientation, inSampleSize, uri); 795 } 796 mMainHandler.sendMessage( 797 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL)); 798 } 799 } 800 }); 801 reportProgress(); 802 } 803 804 private void showDialog(String str) { 805 mProgressDialog = new ProgressDialog(this); 806 mProgressDialog.setMessage(str); 807 mProgressDialog.show(); 808 } 809 810 private void runBackgroundThread(Thread thread) { 811 mThreadRunning = true; 812 thread.start(); 813 } 814 815 private void onBackgroundThreadFinished() { 816 mThreadRunning = false; 817 if (mProgressDialog != null) { 818 mProgressDialog.dismiss(); 819 mProgressDialog = null; 820 } 821 } 822 823 private void cancelHighResComputation() { 824 mCancelComputation = true; 825 synchronized (mWaitObject) { 826 mWaitObject.notify(); 827 } 828 } 829 830 @OnClickAttr 831 public void onCancelButtonClicked(View v) { 832 if (mPausing || mSurfaceTexture == null) return; 833 cancelHighResComputation(); 834 } 835 836 @OnClickAttr 837 public void onThumbnailClicked(View v) { 838 if (mPausing || mThreadRunning || mSurfaceTexture == null) return; 839 showSharePopup(); 840 } 841 842 private void showSharePopup() { 843 if (mThumbnail == null) return; 844 Uri uri = mThumbnail.getUri(); 845 if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) { 846 // The orientation compensation is set to 0 here because we only support landscape. 847 mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 848 mOrientationCompensation, 849 findViewById(R.id.frame_layout)); 850 } 851 mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0); 852 } 853 854 private void reset() { 855 mCaptureState = CAPTURE_STATE_VIEWFINDER; 856 857 mReviewLayout.setVisibility(View.GONE); 858 mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan); 859 mPanoProgressBar.setVisibility(View.GONE); 860 mCaptureLayout.setVisibility(View.VISIBLE); 861 mMosaicFrameProcessor.reset(); 862 863 mSurfaceTexture.setOnFrameAvailableListener(this); 864 } 865 866 private void resetToPreview() { 867 reset(); 868 if (!mPausing) startCameraPreview(); 869 } 870 871 private void showFinalMosaic(Bitmap bitmap) { 872 if (bitmap != null) { 873 mReview.setImageBitmap(bitmap); 874 } 875 mCaptureLayout.setVisibility(View.GONE); 876 mReviewLayout.setVisibility(View.VISIBLE); 877 } 878 879 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 880 if (jpegData != null) { 881 String imagePath = PanoUtil.createName( 882 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 883 return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 884 orientation, jpegData, width, height); 885 } 886 return null; 887 } 888 889 private void clearMosaicFrameProcessorIfNeeded() { 890 if (!mPausing || mThreadRunning) return; 891 mMosaicFrameProcessor.clear(); 892 } 893 894 private void initMosaicFrameProcessorIfNeeded() { 895 if (mPausing || mThreadRunning) return; 896 if (mMosaicFrameProcessor == null) { 897 // Start the activity for the first time. 898 mMosaicFrameProcessor = new MosaicFrameProcessor( 899 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 900 } 901 mMosaicFrameProcessor.initialize(); 902 } 903 904 private void initSoundRecorder() { 905 // Construct sound player; use enforced sound output if necessary 906 File recordSoundFile = new File(VIDEO_RECORD_SOUND); 907 try { 908 ParcelFileDescriptor recordSoundParcel = 909 ParcelFileDescriptor.open(recordSoundFile, 910 ParcelFileDescriptor.MODE_READ_ONLY); 911 AssetFileDescriptor recordSoundAsset = 912 new AssetFileDescriptor(recordSoundParcel, 0, 913 AssetFileDescriptor.UNKNOWN_LENGTH); 914 if (SystemProperties.get("ro.camera.sound.forced", "0").equals("0")) { 915 mRecordSound = new SoundPlayer(recordSoundAsset, false); 916 } else { 917 mRecordSound = new SoundPlayer(recordSoundAsset, true); 918 } 919 } catch (java.io.FileNotFoundException e) { 920 Log.e(TAG, "System video record sound not found"); 921 mRecordSound = null; 922 } 923 } 924 925 private void releaseSoundRecorder() { 926 if (mRecordSound != null) { 927 mRecordSound.release(); 928 mRecordSound = null; 929 } 930 } 931 932 @Override 933 protected void onPause() { 934 super.onPause(); 935 936 mPausing = true; 937 cancelHighResComputation(); 938 // Stop the capturing first. 939 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 940 stopCapture(true); 941 reset(); 942 } 943 if (mSharePopup != null) mSharePopup.dismiss(); 944 945 if (mThumbnail != null && !mThumbnail.fromFile()) { 946 mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME)); 947 } 948 949 releaseCamera(); 950 releaseSoundRecorder(); 951 mMosaicView.onPause(); 952 clearMosaicFrameProcessorIfNeeded(); 953 mOrientationEventListener.disable(); 954 System.gc(); 955 } 956 957 @Override 958 protected void doOnResume() { 959 mPausing = false; 960 mOrientationEventListener.enable(); 961 962 mCaptureState = CAPTURE_STATE_VIEWFINDER; 963 setupCamera(); 964 965 initSoundRecorder(); 966 967 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 968 // has to be decided by camera device. 969 initMosaicFrameProcessorIfNeeded(); 970 mMosaicView.onResume(); 971 } 972 973 public MosaicJpeg generateFinalMosaic(boolean highRes) { 974 if (mMosaicFrameProcessor.createMosaic(highRes) == Mosaic.MOSAIC_RET_CANCELLED) { 975 return null; 976 } 977 978 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 979 if (imageData == null) { 980 Log.e(TAG, "getFinalMosaicNV21() returned null."); 981 return new MosaicJpeg(); 982 } 983 984 int len = imageData.length - 8; 985 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 986 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 987 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 988 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 989 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 990 991 if (width <= 0 || height <= 0) { 992 // TODO: pop up a error meesage indicating that the final result is not generated. 993 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 994 height); 995 return new MosaicJpeg(); 996 } 997 998 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 999 ByteArrayOutputStream out = new ByteArrayOutputStream(); 1000 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 1001 try { 1002 out.close(); 1003 } catch (Exception e) { 1004 Log.e(TAG, "Exception in storing final mosaic", e); 1005 return new MosaicJpeg(); 1006 } 1007 return new MosaicJpeg(out.toByteArray(), width, height); 1008 } 1009 1010 private void setPreviewTexture(SurfaceTexture surface) { 1011 try { 1012 mCameraDevice.setPreviewTexture(surface); 1013 } catch (Throwable ex) { 1014 releaseCamera(); 1015 throw new RuntimeException("setPreviewTexture failed", ex); 1016 } 1017 } 1018 1019 private void startCameraPreview() { 1020 // If we're previewing already, stop the preview first (this will blank 1021 // the screen). 1022 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 1023 1024 int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this), 1025 CameraHolder.instance().getBackCameraId()); 1026 mCameraDevice.setDisplayOrientation(orientation); 1027 1028 setPreviewTexture(mSurfaceTexture); 1029 1030 try { 1031 Log.v(TAG, "startPreview"); 1032 mCameraDevice.startPreview(); 1033 } catch (Throwable ex) { 1034 releaseCamera(); 1035 throw new RuntimeException("startPreview failed", ex); 1036 } 1037 mCameraState = PREVIEW_ACTIVE; 1038 } 1039 1040 private void stopCameraPreview() { 1041 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 1042 Log.v(TAG, "stopPreview"); 1043 mCameraDevice.stopPreview(); 1044 } 1045 mCameraState = PREVIEW_STOPPED; 1046 } 1047 } 1048