PanoramaModule.java revision 032dea1d8406cde556ec0a441e4c90409edf9d63
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; 18 19import android.annotation.TargetApi; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.Intent; 23import android.content.res.Configuration; 24import android.content.res.Resources; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.graphics.ImageFormat; 28import android.graphics.PixelFormat; 29import android.graphics.Rect; 30import android.graphics.SurfaceTexture; 31import android.graphics.YuvImage; 32import android.graphics.drawable.Drawable; 33import android.hardware.Camera.Parameters; 34import android.hardware.Camera.Size; 35import android.media.ExifInterface; 36import android.net.Uri; 37import android.os.AsyncTask; 38import android.os.Handler; 39import android.os.Message; 40import android.os.PowerManager; 41import android.util.Log; 42import android.view.KeyEvent; 43import android.view.LayoutInflater; 44import android.view.MotionEvent; 45import android.view.OrientationEventListener; 46import android.view.View; 47import android.view.View.OnClickListener; 48import android.view.ViewGroup; 49import android.view.WindowManager; 50import android.widget.ImageView; 51import android.widget.LinearLayout; 52import android.widget.TextView; 53 54import com.android.camera.CameraManager.CameraProxy; 55import com.android.camera.ui.LayoutChangeNotifier; 56import com.android.camera.ui.LayoutNotifyView; 57import com.android.camera.ui.PopupManager; 58import com.android.camera.ui.Rotatable; 59import com.android.camera.ui.RotateLayout; 60import com.android.gallery3d.common.ApiHelper; 61import com.android.gallery3d.ui.GLRootView; 62 63import java.io.ByteArrayOutputStream; 64import java.io.IOException; 65import java.text.DateFormat; 66import java.text.SimpleDateFormat; 67import java.util.List; 68import java.util.TimeZone; 69 70/** 71 * Activity to handle panorama capturing. 72 */ 73@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture 74public class PanoramaModule implements CameraModule, 75 SurfaceTexture.OnFrameAvailableListener, 76 ShutterButton.OnShutterButtonListener, 77 LayoutChangeNotifier.Listener { 78 79 public static final int DEFAULT_SWEEP_ANGLE = 160; 80 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 81 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 82 83 private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1; 84 private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2; 85 private static final int MSG_RESET_TO_PREVIEW = 3; 86 private static final int MSG_CLEAR_SCREEN_DELAY = 4; 87 88 private static final int SCREEN_DELAY = 2 * 60 * 1000; 89 90 private static final String TAG = "CAM PanoModule"; 91 private static final int PREVIEW_STOPPED = 0; 92 private static final int PREVIEW_ACTIVE = 1; 93 private static final int CAPTURE_STATE_VIEWFINDER = 0; 94 private static final int CAPTURE_STATE_MOSAIC = 1; 95 96 private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; 97 private static final String GPS_TIME_FORMAT_STR = "kk/1,mm/1,ss/1"; 98 private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss"; 99 100 // Speed is in unit of deg/sec 101 private static final float PANNING_SPEED_THRESHOLD = 25f; 102 103 private ContentResolver mContentResolver; 104 105 private GLRootView mGLRootView; 106 private ViewGroup mPanoLayout; 107 private LinearLayout mCaptureLayout; 108 private View mReviewLayout; 109 private ImageView mReview; 110 private RotateLayout mCaptureIndicator; 111 private PanoProgressBar mPanoProgressBar; 112 private PanoProgressBar mSavingProgressBar; 113 private LayoutNotifyView mPreviewArea; 114 private View mLeftIndicator; 115 private View mRightIndicator; 116 private MosaicPreviewRenderer mMosaicPreviewRenderer; 117 private TextView mTooFastPrompt; 118 private ShutterButton mShutterButton; 119 private Object mWaitObject = new Object(); 120 121 private DateFormat mGPSDateStampFormat; 122 private DateFormat mGPSTimeStampFormat; 123 private DateFormat mDateTimeStampFormat; 124 125 private String mPreparePreviewString; 126 private String mDialogTitle; 127 private String mDialogOkString; 128 private String mDialogPanoramaFailedString; 129 private String mDialogWaitingPreviousString; 130 131 private int mIndicatorColor; 132 private int mIndicatorColorFast; 133 134 private boolean mUsingFrontCamera; 135 private int mPreviewWidth; 136 private int mPreviewHeight; 137 private int mCameraState; 138 private int mCaptureState; 139 private PowerManager.WakeLock mPartialWakeLock; 140 private MosaicFrameProcessor mMosaicFrameProcessor; 141 private boolean mMosaicFrameProcessorInitialized; 142 private AsyncTask <Void, Void, Void> mWaitProcessorTask; 143 private long mTimeTaken; 144 private Handler mMainHandler; 145 private SurfaceTexture mCameraTexture; 146 private boolean mThreadRunning; 147 private boolean mCancelComputation; 148 private float mHorizontalViewAngle; 149 private float mVerticalViewAngle; 150 151 // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of 152 // getting a better image quality by the former. 153 private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY; 154 155 private PanoOrientationEventListener mOrientationEventListener; 156 // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise 157 // respectively. 158 private int mDeviceOrientation; 159 private int mDeviceOrientationAtCapture; 160 private int mCameraOrientation; 161 private int mOrientationCompensation; 162 163 private RotateDialogController mRotateDialog; 164 165 private SoundClips.Player mSoundPlayer; 166 167 private Runnable mOnFrameAvailableRunnable; 168 169 private CameraActivity mActivity; 170 private View mRootView; 171 private CameraProxy mCameraDevice; 172 private boolean mPaused; 173 174 private class MosaicJpeg { 175 public MosaicJpeg(byte[] data, int width, int height) { 176 this.data = data; 177 this.width = width; 178 this.height = height; 179 this.isValid = true; 180 } 181 182 public MosaicJpeg() { 183 this.data = null; 184 this.width = 0; 185 this.height = 0; 186 this.isValid = false; 187 } 188 189 public final byte[] data; 190 public final int width; 191 public final int height; 192 public final boolean isValid; 193 } 194 195 private class PanoOrientationEventListener extends OrientationEventListener { 196 public PanoOrientationEventListener(Context context) { 197 super(context); 198 } 199 200 @Override 201 public void onOrientationChanged(int orientation) { 202 // We keep the last known orientation. So if the user first orient 203 // the camera then point the camera to floor or sky, we still have 204 // the correct orientation. 205 if (orientation == ORIENTATION_UNKNOWN) return; 206 mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation); 207 // When the screen is unlocked, display rotation may change. Always 208 // calculate the up-to-date orientationCompensation. 209 int orientationCompensation = mDeviceOrientation 210 + Util.getDisplayRotation(mActivity) % 360; 211 if (mOrientationCompensation != orientationCompensation) { 212 mOrientationCompensation = orientationCompensation; 213 } 214 } 215 } 216 217 @Override 218 public void init(CameraActivity activity, View parent, boolean reuseScreenNail) { 219 mActivity = activity; 220 mRootView = (ViewGroup) parent; 221 222 createContentView(); 223 224 mContentResolver = mActivity.getContentResolver(); 225 if (reuseScreenNail) { 226 mActivity.reuseCameraScreenNail(true); 227 } else { 228 mActivity.createCameraScreenNail(true); 229 } 230 231 // This runs in UI thread. 232 mOnFrameAvailableRunnable = new Runnable() { 233 @Override 234 public void run() { 235 // Frames might still be available after the activity is paused. 236 // If we call onFrameAvailable after pausing, the GL thread will crash. 237 if (mPaused) return; 238 239 if (mGLRootView.getVisibility() != View.VISIBLE) { 240 mMosaicPreviewRenderer.showPreviewFrameSync(); 241 mGLRootView.setVisibility(View.VISIBLE); 242 } else { 243 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { 244 mMosaicPreviewRenderer.showPreviewFrame(); 245 } else { 246 mMosaicPreviewRenderer.alignFrame(); 247 mMosaicFrameProcessor.processFrame(); 248 } 249 } 250 } 251 }; 252 253 mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR); 254 mGPSTimeStampFormat = new SimpleDateFormat(GPS_TIME_FORMAT_STR); 255 mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR); 256 TimeZone tzUTC = TimeZone.getTimeZone("UTC"); 257 mGPSDateStampFormat.setTimeZone(tzUTC); 258 mGPSTimeStampFormat.setTimeZone(tzUTC); 259 260 PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); 261 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama"); 262 263 mOrientationEventListener = new PanoOrientationEventListener(mActivity); 264 265 mMosaicFrameProcessor = MosaicFrameProcessor.getInstance(); 266 267 Resources appRes = mActivity.getResources(); 268 mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview); 269 mDialogTitle = appRes.getString(R.string.pano_dialog_title); 270 mDialogOkString = appRes.getString(R.string.dialog_ok); 271 mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed); 272 mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous); 273 274 mGLRootView = (GLRootView) mActivity.getGLRoot(); 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_GENERATE_FINAL_MOSAIC_ERROR: 286 onBackgroundThreadFinished(); 287 if (mPaused) { 288 resetToPreview(); 289 } else { 290 mRotateDialog.showAlertDialog( 291 mDialogTitle, mDialogPanoramaFailedString, 292 mDialogOkString, new Runnable() { 293 @Override 294 public void run() { 295 resetToPreview(); 296 }}, 297 null, null); 298 } 299 clearMosaicFrameProcessorIfNeeded(); 300 break; 301 case MSG_RESET_TO_PREVIEW: 302 onBackgroundThreadFinished(); 303 resetToPreview(); 304 clearMosaicFrameProcessorIfNeeded(); 305 break; 306 case MSG_CLEAR_SCREEN_DELAY: 307 mActivity.getWindow().clearFlags(WindowManager.LayoutParams. 308 FLAG_KEEP_SCREEN_ON); 309 break; 310 } 311 } 312 }; 313 } 314 315 @Override 316 public boolean dispatchTouchEvent(MotionEvent m) { 317 return mActivity.superDispatchTouchEvent(m); 318 } 319 320 private void setupCamera() throws CameraHardwareException, CameraDisabledException { 321 openCamera(); 322 Parameters parameters = mCameraDevice.getParameters(); 323 setupCaptureParams(parameters); 324 configureCamera(parameters); 325 } 326 327 private void releaseCamera() { 328 if (mCameraDevice != null) { 329 mCameraDevice.setPreviewCallbackWithBuffer(null); 330 CameraHolder.instance().release(); 331 mCameraDevice = null; 332 mCameraState = PREVIEW_STOPPED; 333 } 334 } 335 336 private void openCamera() throws CameraHardwareException, CameraDisabledException { 337 int cameraId = CameraHolder.instance().getBackCameraId(); 338 // If there is no back camera, use the first camera. Camera id starts 339 // from 0. Currently if a camera is not back facing, it is front facing. 340 // This is also forward compatible if we have a new facing other than 341 // back or front in the future. 342 if (cameraId == -1) cameraId = 0; 343 mCameraDevice = Util.openCamera(mActivity, cameraId); 344 mCameraOrientation = Util.getCameraOrientation(cameraId); 345 if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true; 346 } 347 348 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 349 boolean needSmaller) { 350 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 351 boolean hasFound = false; 352 for (Size size : supportedSizes) { 353 int h = size.height; 354 int w = size.width; 355 // we only want 4:3 format. 356 int d = DEFAULT_CAPTURE_PIXELS - h * w; 357 if (needSmaller && d < 0) { // no bigger preview than 960x720. 358 continue; 359 } 360 if (need4To3 && (h * 4 != w * 3)) { 361 continue; 362 } 363 d = Math.abs(d); 364 if (d < pixelsDiff) { 365 mPreviewWidth = w; 366 mPreviewHeight = h; 367 pixelsDiff = d; 368 hasFound = true; 369 } 370 } 371 return hasFound; 372 } 373 374 private void setupCaptureParams(Parameters parameters) { 375 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 376 if (!findBestPreviewSize(supportedSizes, true, true)) { 377 Log.w(TAG, "No 4:3 ratio preview size supported."); 378 if (!findBestPreviewSize(supportedSizes, false, true)) { 379 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 380 findBestPreviewSize(supportedSizes, false, false); 381 } 382 } 383 Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); 384 parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 385 386 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 387 int last = frameRates.size() - 1; 388 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 389 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 390 parameters.setPreviewFpsRange(minFps, maxFps); 391 Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); 392 393 List<String> supportedFocusModes = parameters.getSupportedFocusModes(); 394 if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) { 395 parameters.setFocusMode(mTargetFocusMode); 396 } else { 397 // Use the default focus mode and log a message 398 Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode + 399 " becuase the mode is not supported."); 400 } 401 402 parameters.set(Util.RECORDING_HINT, Util.FALSE); 403 404 mHorizontalViewAngle = parameters.getHorizontalViewAngle(); 405 mVerticalViewAngle = parameters.getVerticalViewAngle(); 406 } 407 408 public int getPreviewBufSize() { 409 PixelFormat pixelInfo = new PixelFormat(); 410 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 411 // TODO: remove this extra 32 byte after the driver bug is fixed. 412 return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 413 } 414 415 private void configureCamera(Parameters parameters) { 416 mCameraDevice.setParameters(parameters); 417 } 418 419 private void configMosaicPreview(int w, int h) { 420 stopCameraPreview(); 421 CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; 422 screenNail.setSize(w, h); 423 if (screenNail.getSurfaceTexture() == null) { 424 screenNail.acquireSurfaceTexture(); 425 } else { 426 screenNail.releaseSurfaceTexture(); 427 screenNail.acquireSurfaceTexture(); 428 mActivity.notifyScreenNailChanged(); 429 } 430 boolean isLandscape = (mActivity.getResources().getConfiguration().orientation 431 == Configuration.ORIENTATION_LANDSCAPE); 432 if (mMosaicPreviewRenderer != null) mMosaicPreviewRenderer.release(); 433 mMosaicPreviewRenderer = new MosaicPreviewRenderer( 434 screenNail.getSurfaceTexture(), w, h, isLandscape); 435 436 mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture(); 437 if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) { 438 resetToPreview(); 439 } 440 } 441 442 // Receives the layout change event from the preview area. So we can set 443 // the camera preview screennail to the same size and initialize the mosaic 444 // preview renderer. 445 @Override 446 public void onLayoutChange(View v, int l, int t, int r, int b) { 447 Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t)); 448 mActivity.onLayoutChange(v, l, t, r, b); 449 configMosaicPreview(r - l, b - t); 450 } 451 452 @Override 453 public void onFrameAvailable(SurfaceTexture surface) { 454 /* This function may be called by some random thread, 455 * so let's be safe and jump back to ui thread. 456 * No OpenGL calls can be done here. */ 457 mActivity.runOnUiThread(mOnFrameAvailableRunnable); 458 } 459 460 private void hideDirectionIndicators() { 461 mLeftIndicator.setVisibility(View.GONE); 462 mRightIndicator.setVisibility(View.GONE); 463 } 464 465 private void showDirectionIndicators(int direction) { 466 switch (direction) { 467 case PanoProgressBar.DIRECTION_NONE: 468 mLeftIndicator.setVisibility(View.VISIBLE); 469 mRightIndicator.setVisibility(View.VISIBLE); 470 break; 471 case PanoProgressBar.DIRECTION_LEFT: 472 mLeftIndicator.setVisibility(View.VISIBLE); 473 mRightIndicator.setVisibility(View.GONE); 474 break; 475 case PanoProgressBar.DIRECTION_RIGHT: 476 mLeftIndicator.setVisibility(View.GONE); 477 mRightIndicator.setVisibility(View.VISIBLE); 478 break; 479 } 480 } 481 482 public void startCapture() { 483 // Reset values so we can do this again. 484 mCancelComputation = false; 485 mTimeTaken = System.currentTimeMillis(); 486 mActivity.setSwipingEnabled(false); 487 mActivity.hideSwitcher(); 488 mShutterButton.setImageResource(R.drawable.btn_shutter_recording); 489 mCaptureState = CAPTURE_STATE_MOSAIC; 490 mCaptureIndicator.setVisibility(View.VISIBLE); 491 showDirectionIndicators(PanoProgressBar.DIRECTION_NONE); 492 493 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 494 @Override 495 public void onProgress(boolean isFinished, float panningRateX, float panningRateY, 496 float progressX, float progressY) { 497 float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle; 498 float accumulatedVerticalAngle = progressY * mVerticalViewAngle; 499 if (isFinished 500 || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE) 501 || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) { 502 stopCapture(false); 503 } else { 504 float panningRateXInDegree = panningRateX * mHorizontalViewAngle; 505 float panningRateYInDegree = panningRateY * mVerticalViewAngle; 506 updateProgress(panningRateXInDegree, panningRateYInDegree, 507 accumulatedHorizontalAngle, accumulatedVerticalAngle); 508 } 509 } 510 }); 511 512 mPanoProgressBar.reset(); 513 // TODO: calculate the indicator width according to different devices to reflect the actual 514 // angle of view of the camera device. 515 mPanoProgressBar.setIndicatorWidth(20); 516 mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); 517 mPanoProgressBar.setVisibility(View.VISIBLE); 518 mDeviceOrientationAtCapture = mDeviceOrientation; 519 keepScreenOn(); 520 mActivity.getOrientationManager().lockOrientation(); 521 } 522 523 private void stopCapture(boolean aborted) { 524 mCaptureState = CAPTURE_STATE_VIEWFINDER; 525 mCaptureIndicator.setVisibility(View.GONE); 526 hideTooFastIndication(); 527 hideDirectionIndicators(); 528 529 mMosaicFrameProcessor.setProgressListener(null); 530 stopCameraPreview(); 531 532 mCameraTexture.setOnFrameAvailableListener(null); 533 534 if (!aborted && !mThreadRunning) { 535 mRotateDialog.showWaitingDialog(mPreparePreviewString); 536 // Hide shutter button, shutter icon, etc when waiting for 537 // panorama to stitch 538 mActivity.hideUI(); 539 runBackgroundThread(new Thread() { 540 @Override 541 public void run() { 542 MosaicJpeg jpeg = generateFinalMosaic(false); 543 544 if (jpeg != null && jpeg.isValid) { 545 Bitmap bitmap = null; 546 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 547 mMainHandler.sendMessage(mMainHandler.obtainMessage( 548 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 549 } else { 550 mMainHandler.sendMessage(mMainHandler.obtainMessage( 551 MSG_RESET_TO_PREVIEW)); 552 } 553 } 554 }); 555 } 556 keepScreenOnAwhile(); 557 mActivity.getOrientationManager().unlockOrientation(); 558 } 559 560 private void showTooFastIndication() { 561 mTooFastPrompt.setVisibility(View.VISIBLE); 562 // The PreviewArea also contains the border for "too fast" indication. 563 mPreviewArea.setVisibility(View.VISIBLE); 564 mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); 565 mLeftIndicator.setEnabled(true); 566 mRightIndicator.setEnabled(true); 567 } 568 569 private void hideTooFastIndication() { 570 mTooFastPrompt.setVisibility(View.GONE); 571 // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout 572 // information so we can know the size and position for mCameraScreenNail. 573 mPreviewArea.setVisibility(View.INVISIBLE); 574 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 575 mLeftIndicator.setEnabled(false); 576 mRightIndicator.setEnabled(false); 577 } 578 579 private void updateProgress(float panningRateXInDegree, float panningRateYInDegree, 580 float progressHorizontalAngle, float progressVerticalAngle) { 581 mGLRootView.requestRender(); 582 583 // TODO: Now we just display warning message by the panning speed. 584 // Since we only support horizontal panning, we should display a warning message 585 // in UI when there're significant vertical movements. 586 if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD) 587 || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) { 588 showTooFastIndication(); 589 } else { 590 hideTooFastIndication(); 591 } 592 int angleInMajorDirection = 593 (Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle)) 594 ? (int) progressHorizontalAngle 595 : (int) progressVerticalAngle; 596 mPanoProgressBar.setProgress((angleInMajorDirection)); 597 } 598 599 private void setViews(Resources appRes) { 600 mCaptureState = CAPTURE_STATE_VIEWFINDER; 601 mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar); 602 mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 603 mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); 604 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 605 mPanoProgressBar.setOnDirectionChangeListener( 606 new PanoProgressBar.OnDirectionChangeListener () { 607 @Override 608 public void onDirectionChange(int direction) { 609 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 610 showDirectionIndicators(direction); 611 } 612 } 613 }); 614 615 mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator); 616 mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator); 617 mLeftIndicator.setEnabled(false); 618 mRightIndicator.setEnabled(false); 619 mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview); 620 // This mPreviewArea also shows the border for visual "too fast" indication. 621 mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area); 622 mPreviewArea.setOnLayoutChangeListener(this); 623 624 mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar); 625 mSavingProgressBar.setIndicatorWidth(0); 626 mSavingProgressBar.setMaxProgress(100); 627 mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 628 mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication)); 629 630 mCaptureIndicator = (RotateLayout) mRootView.findViewById(R.id.pano_capture_indicator); 631 632 mReviewLayout = mRootView.findViewById(R.id.pano_review_layout); 633 mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea); 634 View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button); 635 cancelButton.setOnClickListener(new OnClickListener() { 636 @Override 637 public void onClick(View arg0) { 638 if (mPaused || mCameraTexture == null) return; 639 cancelHighResComputation(); 640 } 641 }); 642 643 mShutterButton = mActivity.getShutterButton(); 644 mShutterButton.setImageResource(R.drawable.btn_new_shutter); 645 mShutterButton.setOnShutterButtonListener(this); 646 647 if (mActivity.getResources().getConfiguration().orientation 648 == Configuration.ORIENTATION_PORTRAIT) { 649 Rotatable[] rotateLayout = { 650 (Rotatable) mRootView.findViewById(R.id.pano_pan_progress_bar_layout), 651 (Rotatable) mRootView.findViewById(R.id.pano_capture_too_fast_textview_layout), 652 (Rotatable) mRootView.findViewById(R.id.pano_review_saving_indication_layout), 653 (Rotatable) mRootView.findViewById(R.id.pano_saving_progress_bar_layout), 654 (Rotatable) mRootView.findViewById(R.id.pano_review_cancel_button_layout), 655 (Rotatable) mRootView.findViewById(R.id.pano_rotate_reviewarea), 656 mRotateDialog, 657 mCaptureIndicator, 658 }; 659 for (Rotatable r : rotateLayout) { 660 r.setOrientation(270, false); 661 } 662 } else { 663 // Even if the orientation is 0, we still need to set because it might be previously 664 // set when the configuration is portrait. 665 mRotateDialog.setOrientation(0, false); 666 } 667 } 668 669 private void createContentView() { 670 mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView); 671 Resources appRes = mActivity.getResources(); 672 mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root); 673 mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); 674 mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); 675 mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout); 676 mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog); 677 setViews(appRes); 678 } 679 680 @Override 681 public void onShutterButtonClick() { 682 // If mCameraTexture == null then GL setup is not finished yet. 683 // No buttons can be pressed. 684 if (mPaused || mThreadRunning || mCameraTexture == null) return; 685 // Since this button will stay on the screen when capturing, we need to check the state 686 // right now. 687 switch (mCaptureState) { 688 case CAPTURE_STATE_VIEWFINDER: 689 if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return; 690 mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); 691 startCapture(); 692 break; 693 case CAPTURE_STATE_MOSAIC: 694 mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); 695 stopCapture(false); 696 } 697 } 698 699 @Override 700 public void onShutterButtonFocus(boolean pressed) { 701 } 702 703 public void reportProgress() { 704 mSavingProgressBar.reset(); 705 mSavingProgressBar.setRightIncreasing(true); 706 Thread t = new Thread() { 707 @Override 708 public void run() { 709 while (mThreadRunning) { 710 final int progress = mMosaicFrameProcessor.reportProgress( 711 true, mCancelComputation); 712 713 try { 714 synchronized (mWaitObject) { 715 mWaitObject.wait(50); 716 } 717 } catch (InterruptedException e) { 718 throw new RuntimeException("Panorama reportProgress failed", e); 719 } 720 // Update the progress bar 721 mActivity.runOnUiThread(new Runnable() { 722 @Override 723 public void run() { 724 mSavingProgressBar.setProgress(progress); 725 } 726 }); 727 } 728 } 729 }; 730 t.start(); 731 } 732 733 public void saveHighResMosaic() { 734 runBackgroundThread(new Thread() { 735 @Override 736 public void run() { 737 mPartialWakeLock.acquire(); 738 MosaicJpeg jpeg; 739 try { 740 jpeg = generateFinalMosaic(true); 741 } finally { 742 mPartialWakeLock.release(); 743 } 744 745 if (jpeg == null) { // Cancelled by user. 746 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 747 } else if (!jpeg.isValid) { // Error when generating mosaic. 748 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 749 } else { 750 // The panorama image returned from the library is oriented based on the 751 // natural orientation of a camera. We need to set an orientation for the image 752 // in its EXIF header, so the image can be displayed correctly. 753 // The orientation is calculated from compensating the 754 // device orientation at capture and the camera orientation respective to 755 // the natural orientation of the device. 756 int orientation; 757 if (mUsingFrontCamera) { 758 // mCameraOrientation is negative with respect to the front facing camera. 759 // See document of android.hardware.Camera.Parameters.setRotation. 760 orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360; 761 } else { 762 orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360; 763 } 764 Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); 765 if (uri != null) { 766 mActivity.addSecureAlbumItemIfNeeded(false, uri); 767 Util.broadcastNewPicture(mActivity, uri); 768 } 769 mMainHandler.sendMessage( 770 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW)); 771 } 772 } 773 }); 774 reportProgress(); 775 } 776 777 private void runBackgroundThread(Thread thread) { 778 mThreadRunning = true; 779 thread.start(); 780 } 781 782 private void onBackgroundThreadFinished() { 783 mThreadRunning = false; 784 mRotateDialog.dismissDialog(); 785 } 786 787 private void cancelHighResComputation() { 788 mCancelComputation = true; 789 synchronized (mWaitObject) { 790 mWaitObject.notify(); 791 } 792 } 793 794 // This function will be called upon the first camera frame is available. 795 private void reset() { 796 mCaptureState = CAPTURE_STATE_VIEWFINDER; 797 798 // We should set mGLRootView visible too. However, since there might be no 799 // frame available yet, setting mGLRootView visible should be done right after 800 // the first camera frame is available and therefore it is done by 801 // mOnFirstFrameAvailableRunnable. 802 mActivity.setSwipingEnabled(true); 803 mShutterButton.setImageResource(R.drawable.btn_new_shutter); 804 mReviewLayout.setVisibility(View.GONE); 805 mPanoProgressBar.setVisibility(View.GONE); 806 // Orientation change will trigger onLayoutChange->configMosaicPreview-> 807 // resetToPreview. Do not show the capture UI in film strip. 808 if (mActivity.mShowCameraAppView) { 809 mCaptureLayout.setVisibility(View.VISIBLE); 810 mActivity.showUI(); 811 } 812 mMosaicFrameProcessor.reset(); 813 } 814 815 private void resetToPreview() { 816 reset(); 817 if (!mPaused) startCameraPreview(); 818 } 819 820 private void showFinalMosaic(Bitmap bitmap) { 821 if (bitmap != null) { 822 mReview.setImageBitmap(bitmap); 823 } 824 825 mGLRootView.setVisibility(View.GONE); 826 mCaptureLayout.setVisibility(View.GONE); 827 mReviewLayout.setVisibility(View.VISIBLE); 828 } 829 830 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 831 if (jpegData != null) { 832 String filename = PanoUtil.createName( 833 mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); 834 Uri uri = Storage.addImage(mContentResolver, filename, mTimeTaken, null, 835 orientation, jpegData, width, height); 836 if (uri != null) { 837 String filepath = Storage.generateFilepath(filename); 838 try { 839 ExifInterface exif = new ExifInterface(filepath); 840 841 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, 842 mGPSDateStampFormat.format(mTimeTaken)); 843 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, 844 mGPSTimeStampFormat.format(mTimeTaken)); 845 exif.setAttribute(ExifInterface.TAG_DATETIME, 846 mDateTimeStampFormat.format(mTimeTaken)); 847 exif.setAttribute(ExifInterface.TAG_ORIENTATION, 848 getExifOrientation(orientation)); 849 850 exif.saveAttributes(); 851 } catch (IOException e) { 852 Log.e(TAG, "Cannot set EXIF for " + filepath, e); 853 } 854 } 855 return uri; 856 } 857 return null; 858 } 859 860 private static String getExifOrientation(int orientation) { 861 switch (orientation) { 862 case 0: 863 return String.valueOf(ExifInterface.ORIENTATION_NORMAL); 864 case 90: 865 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90); 866 case 180: 867 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180); 868 case 270: 869 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270); 870 default: 871 throw new AssertionError("invalid: " + orientation); 872 } 873 } 874 875 private void clearMosaicFrameProcessorIfNeeded() { 876 if (!mPaused || mThreadRunning) return; 877 // Only clear the processor if it is initialized by this activity 878 // instance. Other activity instances may be using it. 879 if (mMosaicFrameProcessorInitialized) { 880 mMosaicFrameProcessor.clear(); 881 mMosaicFrameProcessorInitialized = false; 882 } 883 } 884 885 private void initMosaicFrameProcessorIfNeeded() { 886 if (mPaused || mThreadRunning) return; 887 mMosaicFrameProcessor.initialize( 888 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 889 mMosaicFrameProcessorInitialized = true; 890 } 891 892 @Override 893 public void onPauseBeforeSuper() { 894 mPaused = true; 895 } 896 897 @Override 898 public void onPauseAfterSuper() { 899 mOrientationEventListener.disable(); 900 if (mCameraDevice == null) { 901 // Camera open failed. Nothing should be done here. 902 return; 903 } 904 // Stop the capturing first. 905 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 906 stopCapture(true); 907 reset(); 908 } 909 910 releaseCamera(); 911 mCameraTexture = null; 912 913 // The preview renderer might not have a chance to be initialized before 914 // onPause(). 915 if (mMosaicPreviewRenderer != null) { 916 mMosaicPreviewRenderer.release(); 917 mMosaicPreviewRenderer = null; 918 } 919 920 clearMosaicFrameProcessorIfNeeded(); 921 if (mWaitProcessorTask != null) { 922 mWaitProcessorTask.cancel(true); 923 mWaitProcessorTask = null; 924 } 925 resetScreenOn(); 926 if (mSoundPlayer != null) { 927 mSoundPlayer.release(); 928 mSoundPlayer = null; 929 } 930 CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; 931 if (screenNail.getSurfaceTexture() != null) { 932 screenNail.releaseSurfaceTexture(); 933 } 934 System.gc(); 935 } 936 937 @Override 938 public void onConfigurationChanged(Configuration newConfig) { 939 940 Drawable lowResReview = null; 941 if (mThreadRunning) lowResReview = mReview.getDrawable(); 942 943 // Change layout in response to configuration change 944 mCaptureLayout.setOrientation( 945 newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE 946 ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); 947 mCaptureLayout.removeAllViews(); 948 LayoutInflater inflater = mActivity.getLayoutInflater(); 949 inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout); 950 951 mPanoLayout.removeView(mReviewLayout); 952 inflater.inflate(R.layout.pano_review, mPanoLayout); 953 954 setViews(mActivity.getResources()); 955 if (mThreadRunning) { 956 mReview.setImageDrawable(lowResReview); 957 mCaptureLayout.setVisibility(View.GONE); 958 mReviewLayout.setVisibility(View.VISIBLE); 959 } 960 } 961 962 @Override 963 public void onOrientationChanged(int orientation) { 964 } 965 966 @Override 967 public void onResumeBeforeSuper() { 968 mPaused = false; 969 } 970 971 @Override 972 public void onResumeAfterSuper() { 973 mOrientationEventListener.enable(); 974 975 mCaptureState = CAPTURE_STATE_VIEWFINDER; 976 977 try { 978 setupCamera(); 979 } catch (CameraHardwareException e) { 980 Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); 981 return; 982 } catch (CameraDisabledException e) { 983 Util.showErrorAndFinish(mActivity, R.string.camera_disabled); 984 return; 985 } 986 987 // Set up sound playback for shutter button 988 mSoundPlayer = SoundClips.getPlayer(mActivity); 989 990 // Check if another panorama instance is using the mosaic frame processor. 991 mRotateDialog.dismissDialog(); 992 if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { 993 mGLRootView.setVisibility(View.GONE); 994 mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString); 995 // If stitching is still going on, make sure switcher and shutter button 996 // are not showing 997 mActivity.hideUI(); 998 mWaitProcessorTask = new WaitProcessorTask().execute(); 999 } else { 1000 if (!mThreadRunning) mGLRootView.setVisibility(View.VISIBLE); 1001 // Camera must be initialized before MosaicFrameProcessor is 1002 // initialized. The preview size has to be decided by camera device. 1003 initMosaicFrameProcessorIfNeeded(); 1004 int w = mPreviewArea.getWidth(); 1005 int h = mPreviewArea.getHeight(); 1006 if (w != 0 && h != 0) { // The layout has been calculated. 1007 configMosaicPreview(w, h); 1008 } 1009 } 1010 keepScreenOnAwhile(); 1011 1012 // Dismiss open menu if exists. 1013 PopupManager.getInstance(mActivity).notifyShowPopup(null); 1014 mRootView.requestLayout(); 1015 } 1016 1017 /** 1018 * Generate the final mosaic image. 1019 * 1020 * @param highRes flag to indicate whether we want to get a high-res version. 1021 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation 1022 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there 1023 * is an error in generating the final mosaic. 1024 */ 1025 public MosaicJpeg generateFinalMosaic(boolean highRes) { 1026 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes); 1027 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) { 1028 return null; 1029 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) { 1030 return new MosaicJpeg(); 1031 } 1032 1033 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 1034 if (imageData == null) { 1035 Log.e(TAG, "getFinalMosaicNV21() returned null."); 1036 return new MosaicJpeg(); 1037 } 1038 1039 int len = imageData.length - 8; 1040 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 1041 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 1042 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 1043 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 1044 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 1045 1046 if (width <= 0 || height <= 0) { 1047 // TODO: pop up an error message indicating that the final result is not generated. 1048 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 1049 height); 1050 return new MosaicJpeg(); 1051 } 1052 1053 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 1054 ByteArrayOutputStream out = new ByteArrayOutputStream(); 1055 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 1056 try { 1057 out.close(); 1058 } catch (Exception e) { 1059 Log.e(TAG, "Exception in storing final mosaic", e); 1060 return new MosaicJpeg(); 1061 } 1062 return new MosaicJpeg(out.toByteArray(), width, height); 1063 } 1064 1065 private void startCameraPreview() { 1066 if (mCameraDevice == null) { 1067 // Camera open failed. Return. 1068 return; 1069 } 1070 1071 // This works around a driver issue. startPreview may fail if 1072 // stopPreview/setPreviewTexture/startPreview are called several times 1073 // in a row. mCameraTexture can be null after pressing home during 1074 // mosaic generation and coming back. Preview will be started later in 1075 // onLayoutChange->configMosaicPreview. This also reduces the latency. 1076 if (mCameraTexture == null) return; 1077 1078 // If we're previewing already, stop the preview first (this will blank 1079 // the screen). 1080 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 1081 1082 // Set the display orientation to 0, so that the underlying mosaic library 1083 // can always get undistorted mPreviewWidth x mPreviewHeight image data 1084 // from SurfaceTexture. 1085 mCameraDevice.setDisplayOrientation(0); 1086 1087 if (mCameraTexture != null) mCameraTexture.setOnFrameAvailableListener(this); 1088 mCameraDevice.setPreviewTextureAsync(mCameraTexture); 1089 1090 mCameraDevice.startPreviewAsync(); 1091 mCameraState = PREVIEW_ACTIVE; 1092 } 1093 1094 private void stopCameraPreview() { 1095 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 1096 Log.v(TAG, "stopPreview"); 1097 mCameraDevice.stopPreview(); 1098 } 1099 mCameraState = PREVIEW_STOPPED; 1100 } 1101 1102 @Override 1103 public void onUserInteraction() { 1104 if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); 1105 } 1106 1107 @Override 1108 public boolean onBackPressed() { 1109 // If panorama is generating low res or high res mosaic, ignore back 1110 // key. So the activity will not be destroyed. 1111 if (mThreadRunning) return true; 1112 return false; 1113 } 1114 1115 private void resetScreenOn() { 1116 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1117 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1118 } 1119 1120 private void keepScreenOnAwhile() { 1121 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1122 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1123 mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1124 } 1125 1126 private void keepScreenOn() { 1127 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1128 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1129 } 1130 1131 private class WaitProcessorTask extends AsyncTask<Void, Void, Void> { 1132 @Override 1133 protected Void doInBackground(Void... params) { 1134 synchronized (mMosaicFrameProcessor) { 1135 while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { 1136 try { 1137 mMosaicFrameProcessor.wait(); 1138 } catch (Exception e) { 1139 // ignore 1140 } 1141 } 1142 } 1143 return null; 1144 } 1145 1146 @Override 1147 protected void onPostExecute(Void result) { 1148 mWaitProcessorTask = null; 1149 mRotateDialog.dismissDialog(); 1150 mGLRootView.setVisibility(View.VISIBLE); 1151 initMosaicFrameProcessorIfNeeded(); 1152 int w = mPreviewArea.getWidth(); 1153 int h = mPreviewArea.getHeight(); 1154 if (w != 0 && h != 0) { // The layout has been calculated. 1155 configMosaicPreview(w, h); 1156 } 1157 resetToPreview(); 1158 } 1159 } 1160 1161 @Override 1162 public void onFullScreenChanged(boolean full) { 1163 } 1164 1165 1166 @Override 1167 public void onStop() { 1168 } 1169 1170 @Override 1171 public void installIntentFilter() { 1172 } 1173 1174 @Override 1175 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1176 } 1177 1178 1179 @Override 1180 public boolean onKeyDown(int keyCode, KeyEvent event) { 1181 return false; 1182 } 1183 1184 @Override 1185 public boolean onKeyUp(int keyCode, KeyEvent event) { 1186 return false; 1187 } 1188 1189 @Override 1190 public void onSingleTapUp(View view, int x, int y) { 1191 } 1192 1193 @Override 1194 public void onPreviewTextureCopied() { 1195 } 1196 1197 @Override 1198 public void onCaptureTextureCopied() { 1199 } 1200 1201 @Override 1202 public boolean updateStorageHintOnResume() { 1203 return false; 1204 } 1205 1206 @Override 1207 public void updateCameraAppView() { 1208 } 1209 1210 @Override 1211 public boolean collapseCameraControls() { 1212 return false; 1213 } 1214 1215 @Override 1216 public boolean needsSwitcher() { 1217 return true; 1218 } 1219} 1220