1 /* 2 * Copyright (C) 2013 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; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.Configuration; 23 import android.content.res.Resources; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.ImageFormat; 27 import android.graphics.PixelFormat; 28 import android.graphics.Point; 29 import android.graphics.Rect; 30 import android.graphics.SurfaceTexture; 31 import android.graphics.YuvImage; 32 import android.hardware.Camera.Parameters; 33 import android.hardware.Camera.Size; 34 import android.location.Location; 35 import android.net.Uri; 36 import android.os.AsyncTask; 37 import android.os.Handler; 38 import android.os.Message; 39 import android.os.PowerManager; 40 import android.util.Log; 41 import android.view.KeyEvent; 42 import android.view.OrientationEventListener; 43 import android.view.View; 44 import android.view.ViewGroup; 45 import android.view.WindowManager; 46 47 import com.android.camera.CameraManager.CameraProxy; 48 import com.android.camera.app.OrientationManager; 49 import com.android.camera.data.LocalData; 50 import com.android.camera.exif.ExifInterface; 51 import com.android.camera.util.CameraUtil; 52 import com.android.camera.util.UsageStatistics; 53 import com.android.camera2.R; 54 55 import java.io.ByteArrayOutputStream; 56 import java.io.File; 57 import java.io.IOException; 58 import java.util.List; 59 import java.util.TimeZone; 60 61 /** 62 * Activity to handle panorama capturing. 63 */ 64 public class WideAnglePanoramaModule 65 implements CameraModule, WideAnglePanoramaController, 66 SurfaceTexture.OnFrameAvailableListener { 67 68 public static final int DEFAULT_SWEEP_ANGLE = 160; 69 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 70 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 71 72 private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1; 73 private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2; 74 private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3; 75 private static final int MSG_CLEAR_SCREEN_DELAY = 4; 76 private static final int MSG_RESET_TO_PREVIEW = 5; 77 78 private static final int SCREEN_DELAY = 2 * 60 * 1000; 79 80 @SuppressWarnings("unused") 81 private static final String TAG = "CAM_WidePanoModule"; 82 private static final int PREVIEW_STOPPED = 0; 83 private static final int PREVIEW_ACTIVE = 1; 84 public static final int CAPTURE_STATE_VIEWFINDER = 0; 85 public static final int CAPTURE_STATE_MOSAIC = 1; 86 87 // The unit of speed is degrees per frame. 88 private static final float PANNING_SPEED_THRESHOLD = 2.5f; 89 private static final boolean DEBUG = false; 90 91 private ContentResolver mContentResolver; 92 private WideAnglePanoramaUI mUI; 93 94 private MosaicPreviewRenderer mMosaicPreviewRenderer; 95 private Object mRendererLock = new Object(); 96 private Object mWaitObject = new Object(); 97 98 private String mPreparePreviewString; 99 private String mDialogTitle; 100 private String mDialogOkString; 101 private String mDialogPanoramaFailedString; 102 private String mDialogWaitingPreviousString; 103 104 private int mPreviewUIWidth; 105 private int mPreviewUIHeight; 106 private boolean mUsingFrontCamera; 107 private int mCameraPreviewWidth; 108 private int mCameraPreviewHeight; 109 private int mCameraState; 110 private int mCaptureState; 111 private PowerManager.WakeLock mPartialWakeLock; 112 private MosaicFrameProcessor mMosaicFrameProcessor; 113 private boolean mMosaicFrameProcessorInitialized; 114 private AsyncTask <Void, Void, Void> mWaitProcessorTask; 115 private long mTimeTaken; 116 private Handler mMainHandler; 117 private SurfaceTexture mCameraTexture; 118 private boolean mThreadRunning; 119 private boolean mCancelComputation; 120 private float mHorizontalViewAngle; 121 private float mVerticalViewAngle; 122 123 // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of 124 // getting a better image quality by the former. 125 private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY; 126 127 private PanoOrientationEventListener mOrientationEventListener; 128 // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise 129 // respectively. 130 private int mDeviceOrientation; 131 private int mDeviceOrientationAtCapture; 132 private int mCameraOrientation; 133 private int mOrientationCompensation; 134 135 private SoundClips.Player mSoundPlayer; 136 137 private Runnable mOnFrameAvailableRunnable; 138 139 private CameraActivity mActivity; 140 private View mRootView; 141 private CameraProxy mCameraDevice; 142 private boolean mPaused; 143 144 private LocationManager mLocationManager; 145 private OrientationManager mOrientationManager; 146 private ComboPreferences mPreferences; 147 private boolean mMosaicPreviewConfigured; 148 private boolean mPreviewFocused = true; 149 150 @Override 151 public void onPreviewUIReady() { 152 configMosaicPreview(); 153 } 154 155 @Override 156 public void onPreviewUIDestroyed() { 157 158 } 159 160 private class MosaicJpeg { 161 public MosaicJpeg(byte[] data, int width, int height) { 162 this.data = data; 163 this.width = width; 164 this.height = height; 165 this.isValid = true; 166 } 167 168 public MosaicJpeg() { 169 this.data = null; 170 this.width = 0; 171 this.height = 0; 172 this.isValid = false; 173 } 174 175 public final byte[] data; 176 public final int width; 177 public final int height; 178 public final boolean isValid; 179 } 180 181 private class PanoOrientationEventListener extends OrientationEventListener { 182 public PanoOrientationEventListener(Context context) { 183 super(context); 184 } 185 186 @Override 187 public void onOrientationChanged(int orientation) { 188 // We keep the last known orientation. So if the user first orient 189 // the camera then point the camera to floor or sky, we still have 190 // the correct orientation. 191 if (orientation == ORIENTATION_UNKNOWN) return; 192 mDeviceOrientation = CameraUtil.roundOrientation(orientation, mDeviceOrientation); 193 // When the screen is unlocked, display rotation may change. Always 194 // calculate the up-to-date orientationCompensation. 195 int orientationCompensation = mDeviceOrientation 196 + CameraUtil.getDisplayRotation(mActivity) % 360; 197 if (mOrientationCompensation != orientationCompensation) { 198 mOrientationCompensation = orientationCompensation; 199 } 200 } 201 } 202 203 @Override 204 public void init(CameraActivity activity, View parent) { 205 mActivity = activity; 206 mRootView = parent; 207 208 mOrientationManager = new OrientationManager(activity); 209 mCaptureState = CAPTURE_STATE_VIEWFINDER; 210 mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView); 211 mUI.setCaptureProgressOnDirectionChangeListener( 212 new PanoProgressBar.OnDirectionChangeListener() { 213 @Override 214 public void onDirectionChange(int direction) { 215 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 216 mUI.showDirectionIndicators(direction); 217 } 218 } 219 }); 220 221 mContentResolver = mActivity.getContentResolver(); 222 // This runs in UI thread. 223 mOnFrameAvailableRunnable = new Runnable() { 224 @Override 225 public void run() { 226 // Frames might still be available after the activity is paused. 227 // If we call onFrameAvailable after pausing, the GL thread will crash. 228 if (mPaused) return; 229 230 MosaicPreviewRenderer renderer = null; 231 synchronized (mRendererLock) { 232 if (mMosaicPreviewRenderer == null) { 233 return; 234 } 235 renderer = mMosaicPreviewRenderer; 236 } 237 if (mRootView.getVisibility() != View.VISIBLE) { 238 renderer.showPreviewFrameSync(); 239 mRootView.setVisibility(View.VISIBLE); 240 } else { 241 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { 242 renderer.showPreviewFrame(); 243 } else { 244 renderer.alignFrameSync(); 245 mMosaicFrameProcessor.processFrame(); 246 } 247 } 248 } 249 }; 250 251 PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); 252 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama"); 253 254 mOrientationEventListener = new PanoOrientationEventListener(mActivity); 255 256 mMosaicFrameProcessor = MosaicFrameProcessor.getInstance(); 257 258 Resources appRes = mActivity.getResources(); 259 mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview); 260 mDialogTitle = appRes.getString(R.string.pano_dialog_title); 261 mDialogOkString = appRes.getString(R.string.dialog_ok); 262 mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed); 263 mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous); 264 265 mPreferences = new ComboPreferences(mActivity); 266 CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); 267 mLocationManager = new LocationManager(mActivity, null); 268 269 mMainHandler = new Handler() { 270 @Override 271 public void handleMessage(Message msg) { 272 switch (msg.what) { 273 case MSG_LOW_RES_FINAL_MOSAIC_READY: 274 onBackgroundThreadFinished(); 275 showFinalMosaic((Bitmap) msg.obj); 276 saveHighResMosaic(); 277 break; 278 case MSG_GENERATE_FINAL_MOSAIC_ERROR: 279 onBackgroundThreadFinished(); 280 if (mPaused) { 281 resetToPreviewIfPossible(); 282 } else { 283 mUI.showAlertDialog( 284 mDialogTitle, mDialogPanoramaFailedString, 285 mDialogOkString, new Runnable() { 286 @Override 287 public void run() { 288 resetToPreviewIfPossible(); 289 } 290 }); 291 } 292 clearMosaicFrameProcessorIfNeeded(); 293 break; 294 case MSG_END_DIALOG_RESET_TO_PREVIEW: 295 onBackgroundThreadFinished(); 296 resetToPreviewIfPossible(); 297 clearMosaicFrameProcessorIfNeeded(); 298 break; 299 case MSG_CLEAR_SCREEN_DELAY: 300 mActivity.getWindow().clearFlags(WindowManager.LayoutParams. 301 FLAG_KEEP_SCREEN_ON); 302 break; 303 case MSG_RESET_TO_PREVIEW: 304 resetToPreviewIfPossible(); 305 break; 306 } 307 } 308 }; 309 } 310 311 @Override 312 public void onPreviewFocusChanged(boolean previewFocused) { 313 mPreviewFocused = previewFocused; 314 mUI.onPreviewFocusChanged(previewFocused); 315 } 316 317 @Override 318 public boolean arePreviewControlsVisible() { 319 return mUI.arePreviewControlsVisible(); 320 } 321 322 /** 323 * Opens camera and sets the parameters. 324 * 325 * @return Whether the camera was opened successfully. 326 */ 327 private boolean setupCamera() { 328 if (!openCamera()) { 329 return false; 330 } 331 Parameters parameters = mCameraDevice.getParameters(); 332 setupCaptureParams(parameters); 333 configureCamera(parameters); 334 return true; 335 } 336 337 private void releaseCamera() { 338 if (mCameraDevice != null) { 339 CameraHolder.instance().release(); 340 mCameraDevice = null; 341 mCameraState = PREVIEW_STOPPED; 342 } 343 } 344 345 /** 346 * Opens the camera device. The back camera has priority over the front 347 * one. 348 * 349 * @return Whether the camera was opened successfully. 350 */ 351 private boolean openCamera() { 352 int cameraId = CameraHolder.instance().getBackCameraId(); 353 // If there is no back camera, use the first camera. Camera id starts 354 // from 0. Currently if a camera is not back facing, it is front facing. 355 // This is also forward compatible if we have a new facing other than 356 // back or front in the future. 357 if (cameraId == -1) cameraId = 0; 358 mCameraDevice = CameraUtil.openCamera(mActivity, cameraId, 359 mMainHandler, mActivity.getCameraOpenErrorCallback()); 360 if (mCameraDevice == null) { 361 return false; 362 } 363 mCameraOrientation = CameraUtil.getCameraOrientation(cameraId); 364 if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true; 365 return true; 366 } 367 368 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 369 boolean needSmaller) { 370 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 371 boolean hasFound = false; 372 for (Size size : supportedSizes) { 373 int h = size.height; 374 int w = size.width; 375 // we only want 4:3 format. 376 int d = DEFAULT_CAPTURE_PIXELS - h * w; 377 if (needSmaller && d < 0) { // no bigger preview than 960x720. 378 continue; 379 } 380 if (need4To3 && (h * 4 != w * 3)) { 381 continue; 382 } 383 d = Math.abs(d); 384 if (d < pixelsDiff) { 385 mCameraPreviewWidth = w; 386 mCameraPreviewHeight = h; 387 pixelsDiff = d; 388 hasFound = true; 389 } 390 } 391 return hasFound; 392 } 393 394 private void setupCaptureParams(Parameters parameters) { 395 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 396 if (!findBestPreviewSize(supportedSizes, true, true)) { 397 Log.w(TAG, "No 4:3 ratio preview size supported."); 398 if (!findBestPreviewSize(supportedSizes, false, true)) { 399 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 400 findBestPreviewSize(supportedSizes, false, false); 401 } 402 } 403 Log.d(TAG, "camera preview h = " 404 + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth); 405 parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight); 406 407 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 408 int last = frameRates.size() - 1; 409 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 410 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 411 parameters.setPreviewFpsRange(minFps, maxFps); 412 Log.d(TAG, "preview fps: " + minFps + ", " + maxFps); 413 414 List<String> supportedFocusModes = parameters.getSupportedFocusModes(); 415 if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) { 416 parameters.setFocusMode(mTargetFocusMode); 417 } else { 418 // Use the default focus mode and log a message 419 Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode + 420 " becuase the mode is not supported."); 421 } 422 423 parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE); 424 425 mHorizontalViewAngle = parameters.getHorizontalViewAngle(); 426 mVerticalViewAngle = parameters.getVerticalViewAngle(); 427 } 428 429 public int getPreviewBufSize() { 430 PixelFormat pixelInfo = new PixelFormat(); 431 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 432 // TODO: remove this extra 32 byte after the driver bug is fixed. 433 return (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 434 } 435 436 private void configureCamera(Parameters parameters) { 437 mCameraDevice.setParameters(parameters); 438 } 439 440 /** 441 * Configures the preview renderer according to the dimension defined by 442 * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}. 443 * Will stop the camera preview first. 444 */ 445 private void configMosaicPreview() { 446 if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0 447 || mUI.getSurfaceTexture() == null) { 448 return; 449 } 450 451 stopCameraPreview(); 452 synchronized (mRendererLock) { 453 if (mMosaicPreviewRenderer != null) { 454 mMosaicPreviewRenderer.release(); 455 } 456 mMosaicPreviewRenderer = null; 457 } 458 final boolean isLandscape = 459 (mActivity.getResources().getConfiguration().orientation == 460 Configuration.ORIENTATION_LANDSCAPE); 461 mUI.flipPreviewIfNeeded(); 462 MosaicPreviewRenderer renderer = new MosaicPreviewRenderer( 463 mUI.getSurfaceTexture(), 464 mPreviewUIWidth, mPreviewUIHeight, isLandscape); 465 synchronized (mRendererLock) { 466 mMosaicPreviewRenderer = renderer; 467 mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture(); 468 469 if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) { 470 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 471 } 472 mRendererLock.notifyAll(); 473 } 474 mMosaicPreviewConfigured = true; 475 resetToPreviewIfPossible(); 476 } 477 478 /** 479 * Receives the layout change event from the preview area. So we can 480 * initialize the mosaic preview renderer. 481 */ 482 @Override 483 public void onPreviewUILayoutChange(int l, int t, int r, int b) { 484 Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t)); 485 mPreviewUIWidth = r - l; 486 mPreviewUIHeight = b - t; 487 configMosaicPreview(); 488 } 489 490 @Override 491 public void onFrameAvailable(SurfaceTexture surface) { 492 /* This function may be called by some random thread, 493 * so let's be safe and jump back to ui thread. 494 * No OpenGL calls can be done here. */ 495 mActivity.runOnUiThread(mOnFrameAvailableRunnable); 496 } 497 498 public void startCapture() { 499 // Reset values so we can do this again. 500 mCancelComputation = false; 501 mTimeTaken = System.currentTimeMillis(); 502 mActivity.setSwipingEnabled(false); 503 mCaptureState = CAPTURE_STATE_MOSAIC; 504 mUI.onStartCapture(); 505 506 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 507 @Override 508 public void onProgress(boolean isFinished, float panningRateX, float panningRateY, 509 float progressX, float progressY) { 510 float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle; 511 float accumulatedVerticalAngle = progressY * mVerticalViewAngle; 512 if (isFinished 513 || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE) 514 || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) { 515 stopCapture(false); 516 } else { 517 float panningRateXInDegree = panningRateX * mHorizontalViewAngle; 518 float panningRateYInDegree = panningRateY * mVerticalViewAngle; 519 mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree, 520 accumulatedHorizontalAngle, accumulatedVerticalAngle, 521 PANNING_SPEED_THRESHOLD); 522 } 523 } 524 }); 525 526 mUI.resetCaptureProgress(); 527 // TODO: calculate the indicator width according to different devices to reflect the actual 528 // angle of view of the camera device. 529 mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE); 530 mUI.showCaptureProgress(); 531 mDeviceOrientationAtCapture = mDeviceOrientation; 532 keepScreenOn(); 533 // TODO: mActivity.getOrientationManager().lockOrientation(); 534 mOrientationManager.lockOrientation(); 535 int degrees = CameraUtil.getDisplayRotation(mActivity); 536 int cameraId = CameraHolder.instance().getBackCameraId(); 537 int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId); 538 mUI.setProgressOrientation(orientation); 539 } 540 541 private void stopCapture(boolean aborted) { 542 mCaptureState = CAPTURE_STATE_VIEWFINDER; 543 mUI.onStopCapture(); 544 545 mMosaicFrameProcessor.setProgressListener(null); 546 stopCameraPreview(); 547 548 mCameraTexture.setOnFrameAvailableListener(null); 549 550 if (!aborted && !mThreadRunning) { 551 mUI.showWaitingDialog(mPreparePreviewString); 552 // Hide shutter button, shutter icon, etc when waiting for 553 // panorama to stitch 554 mUI.hideUI(); 555 runBackgroundThread(new Thread() { 556 @Override 557 public void run() { 558 MosaicJpeg jpeg = generateFinalMosaic(false); 559 560 if (jpeg != null && jpeg.isValid) { 561 Bitmap bitmap = null; 562 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 563 mMainHandler.sendMessage(mMainHandler.obtainMessage( 564 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 565 } else { 566 mMainHandler.sendMessage(mMainHandler.obtainMessage( 567 MSG_END_DIALOG_RESET_TO_PREVIEW)); 568 } 569 } 570 }); 571 } 572 keepScreenOnAwhile(); 573 } 574 575 @Override 576 public void onShutterButtonClick() { 577 // If mCameraTexture == null then GL setup is not finished yet. 578 // No buttons can be pressed. 579 if (mPaused || mThreadRunning || mCameraTexture == null) { 580 return; 581 } 582 // Since this button will stay on the screen when capturing, we need to check the state 583 // right now. 584 switch (mCaptureState) { 585 case CAPTURE_STATE_VIEWFINDER: 586 final long storageSpaceBytes = mActivity.getStorageSpaceBytes(); 587 if(storageSpaceBytes <= Storage.LOW_STORAGE_THRESHOLD_BYTES) { 588 Log.w(TAG, "Low storage warning: " + storageSpaceBytes); 589 return; 590 } 591 mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); 592 startCapture(); 593 break; 594 case CAPTURE_STATE_MOSAIC: 595 mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); 596 stopCapture(false); 597 break; 598 default: 599 Log.w(TAG, "Unknown capture state: " + mCaptureState); 600 break; 601 } 602 } 603 604 public void reportProgress() { 605 mUI.resetSavingProgress(); 606 Thread t = new Thread() { 607 @Override 608 public void run() { 609 while (mThreadRunning) { 610 final int progress = mMosaicFrameProcessor.reportProgress( 611 true, mCancelComputation); 612 613 try { 614 synchronized (mWaitObject) { 615 mWaitObject.wait(50); 616 } 617 } catch (InterruptedException e) { 618 throw new RuntimeException("Panorama reportProgress failed", e); 619 } 620 // Update the progress bar 621 mActivity.runOnUiThread(new Runnable() { 622 @Override 623 public void run() { 624 mUI.updateSavingProgress(progress); 625 } 626 }); 627 } 628 } 629 }; 630 t.start(); 631 } 632 633 private int getCaptureOrientation() { 634 // The panorama image returned from the library is oriented based on the 635 // natural orientation of a camera. We need to set an orientation for the image 636 // in its EXIF header, so the image can be displayed correctly. 637 // The orientation is calculated from compensating the 638 // device orientation at capture and the camera orientation respective to 639 // the natural orientation of the device. 640 int orientation; 641 if (mUsingFrontCamera) { 642 // mCameraOrientation is negative with respect to the front facing camera. 643 // See document of android.hardware.Camera.Parameters.setRotation. 644 orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360; 645 } else { 646 orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360; 647 } 648 return orientation; 649 } 650 651 /** The orientation of the camera image. The value is the angle that the camera 652 * image needs to be rotated clockwise so it shows correctly on the display 653 * in its natural orientation. It should be 0, 90, 180, or 270.*/ 654 public int getCameraOrientation() { 655 return mCameraOrientation; 656 } 657 658 public void saveHighResMosaic() { 659 runBackgroundThread(new Thread() { 660 @Override 661 public void run() { 662 mPartialWakeLock.acquire(); 663 MosaicJpeg jpeg; 664 try { 665 jpeg = generateFinalMosaic(true); 666 } finally { 667 mPartialWakeLock.release(); 668 } 669 670 if (jpeg == null) { // Cancelled by user. 671 mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW); 672 } else if (!jpeg.isValid) { // Error when generating mosaic. 673 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 674 } else { 675 int orientation = getCaptureOrientation(); 676 final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); 677 if (uri != null) { 678 mActivity.runOnUiThread(new Runnable() { 679 @Override 680 public void run() { 681 mActivity.notifyNewMedia(uri); 682 } 683 }); 684 } 685 mMainHandler.sendMessage( 686 mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW)); 687 } 688 } 689 }); 690 reportProgress(); 691 } 692 693 private void runBackgroundThread(Thread thread) { 694 mThreadRunning = true; 695 thread.start(); 696 } 697 698 private void onBackgroundThreadFinished() { 699 mThreadRunning = false; 700 mUI.dismissAllDialogs(); 701 } 702 703 private void cancelHighResComputation() { 704 mCancelComputation = true; 705 synchronized (mWaitObject) { 706 mWaitObject.notify(); 707 } 708 } 709 710 // This function will be called upon the first camera frame is available. 711 private void reset() { 712 mCaptureState = CAPTURE_STATE_VIEWFINDER; 713 714 mOrientationManager.unlockOrientation(); 715 mUI.reset(); 716 mActivity.setSwipingEnabled(true); 717 // Orientation change will trigger onLayoutChange->configMosaicPreview-> 718 // resetToPreview. Do not show the capture UI in film strip. 719 if (mPreviewFocused) { 720 mUI.showPreviewUI(); 721 } 722 mMosaicFrameProcessor.reset(); 723 } 724 725 private void resetToPreviewIfPossible() { 726 reset(); 727 if (!mMosaicFrameProcessorInitialized 728 || mUI.getSurfaceTexture() == null 729 || !mMosaicPreviewConfigured) { 730 return; 731 } 732 if (!mPaused) { 733 startCameraPreview(); 734 } 735 } 736 737 private void showFinalMosaic(Bitmap bitmap) { 738 mUI.showFinalMosaic(bitmap, getCaptureOrientation()); 739 } 740 741 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 742 if (jpegData != null) { 743 String filename = PanoUtil.createName( 744 mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); 745 String filepath = Storage.generateFilepath(filename); 746 747 UsageStatistics.onEvent(UsageStatistics.COMPONENT_PANORAMA, 748 UsageStatistics.ACTION_CAPTURE_DONE, null, 0, 749 UsageStatistics.hashFileName(filename + ".jpg")); 750 751 Location loc = mLocationManager.getCurrentLocation(); 752 ExifInterface exif = new ExifInterface(); 753 try { 754 exif.readExif(jpegData); 755 exif.addGpsDateTimeStampTag(mTimeTaken); 756 exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken, 757 TimeZone.getDefault()); 758 exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, 759 ExifInterface.getOrientationValueForRotation(orientation))); 760 writeLocation(loc, exif); 761 exif.writeExif(jpegData, filepath); 762 } catch (IOException e) { 763 Log.e(TAG, "Cannot set exif for " + filepath, e); 764 Storage.writeFile(filepath, jpegData); 765 } 766 int jpegLength = (int) (new File(filepath).length()); 767 return Storage.addImage(mContentResolver, filename, mTimeTaken, loc, orientation, 768 jpegLength, filepath, width, height, LocalData.MIME_TYPE_JPEG); 769 } 770 return null; 771 } 772 773 private static void writeLocation(Location location, ExifInterface exif) { 774 if (location == null) { 775 return; 776 } 777 exif.addGpsTags(location.getLatitude(), location.getLongitude()); 778 exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider())); 779 } 780 781 private void clearMosaicFrameProcessorIfNeeded() { 782 if (!mPaused || mThreadRunning) return; 783 // Only clear the processor if it is initialized by this activity 784 // instance. Other activity instances may be using it. 785 if (mMosaicFrameProcessorInitialized) { 786 mMosaicFrameProcessor.clear(); 787 mMosaicFrameProcessorInitialized = false; 788 } 789 } 790 791 private void initMosaicFrameProcessorIfNeeded() { 792 if (mPaused || mThreadRunning) { 793 return; 794 } 795 796 mMosaicFrameProcessor.initialize( 797 mCameraPreviewWidth, mCameraPreviewHeight, getPreviewBufSize()); 798 mMosaicFrameProcessorInitialized = true; 799 } 800 801 @Override 802 public void onPauseBeforeSuper() { 803 mPaused = true; 804 if (mLocationManager != null) mLocationManager.recordLocation(false); 805 mOrientationManager.pause(); 806 } 807 808 @Override 809 public void onPauseAfterSuper() { 810 mOrientationEventListener.disable(); 811 if (mCameraDevice == null) { 812 // Camera open failed. Nothing should be done here. 813 return; 814 } 815 // Stop the capturing first. 816 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 817 stopCapture(true); 818 reset(); 819 } 820 mUI.showPreviewCover(); 821 releaseCamera(); 822 synchronized (mRendererLock) { 823 mCameraTexture = null; 824 825 // The preview renderer might not have a chance to be initialized 826 // before onPause(). 827 if (mMosaicPreviewRenderer != null) { 828 mMosaicPreviewRenderer.release(); 829 mMosaicPreviewRenderer = null; 830 } 831 } 832 833 clearMosaicFrameProcessorIfNeeded(); 834 if (mWaitProcessorTask != null) { 835 mWaitProcessorTask.cancel(true); 836 mWaitProcessorTask = null; 837 } 838 resetScreenOn(); 839 mUI.removeDisplayChangeListener(); 840 if (mSoundPlayer != null) { 841 mSoundPlayer.release(); 842 mSoundPlayer = null; 843 } 844 System.gc(); 845 } 846 847 @Override 848 public void onConfigurationChanged(Configuration newConfig) { 849 mUI.onConfigurationChanged(newConfig, mThreadRunning); 850 } 851 852 @Override 853 public void onOrientationChanged(int orientation) { 854 } 855 856 @Override 857 public void onResumeBeforeSuper() { 858 mPaused = false; 859 } 860 861 @Override 862 public void onResumeAfterSuper() { 863 mOrientationEventListener.enable(); 864 865 mCaptureState = CAPTURE_STATE_VIEWFINDER; 866 867 if (!setupCamera()) { 868 Log.e(TAG, "Failed to open camera, aborting"); 869 return; 870 } 871 872 // Set up sound playback for shutter button 873 mSoundPlayer = SoundClips.getPlayer(mActivity); 874 875 // Check if another panorama instance is using the mosaic frame processor. 876 mUI.dismissAllDialogs(); 877 if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { 878 mUI.showWaitingDialog(mDialogWaitingPreviousString); 879 // If stitching is still going on, make sure switcher and shutter button 880 // are not showing 881 mUI.hideUI(); 882 mWaitProcessorTask = new WaitProcessorTask().execute(); 883 } else { 884 // Camera must be initialized before MosaicFrameProcessor is 885 // initialized. The preview size has to be decided by camera device. 886 initMosaicFrameProcessorIfNeeded(); 887 Point size = mUI.getPreviewAreaSize(); 888 mPreviewUIWidth = size.x; 889 mPreviewUIHeight = size.y; 890 configMosaicPreview(); 891 mActivity.updateStorageSpaceAndHint(); 892 } 893 keepScreenOnAwhile(); 894 895 mOrientationManager.resume(); 896 // Initialize location service. 897 boolean recordLocation = RecordLocationPreference.get(mPreferences, 898 mContentResolver); 899 mLocationManager.recordLocation(recordLocation); 900 mUI.initDisplayChangeListener(); 901 UsageStatistics.onContentViewChanged( 902 UsageStatistics.COMPONENT_CAMERA, "PanoramaModule"); 903 } 904 905 /** 906 * Generate the final mosaic image. 907 * 908 * @param highRes flag to indicate whether we want to get a high-res version. 909 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation 910 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there 911 * is an error in generating the final mosaic. 912 */ 913 public MosaicJpeg generateFinalMosaic(boolean highRes) { 914 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes); 915 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) { 916 return null; 917 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) { 918 return new MosaicJpeg(); 919 } 920 921 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 922 if (imageData == null) { 923 Log.e(TAG, "getFinalMosaicNV21() returned null."); 924 return new MosaicJpeg(); 925 } 926 927 int len = imageData.length - 8; 928 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 929 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 930 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 931 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 932 Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 933 934 if (width <= 0 || height <= 0) { 935 // TODO: pop up an error message indicating that the final result is not generated. 936 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 937 height); 938 return new MosaicJpeg(); 939 } 940 941 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 942 ByteArrayOutputStream out = new ByteArrayOutputStream(); 943 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 944 try { 945 out.close(); 946 } catch (Exception e) { 947 Log.e(TAG, "Exception in storing final mosaic", e); 948 return new MosaicJpeg(); 949 } 950 return new MosaicJpeg(out.toByteArray(), width, height); 951 } 952 953 private void startCameraPreview() { 954 if (mCameraDevice == null) { 955 // Camera open failed. Return. 956 return; 957 } 958 959 if (mUI.getSurfaceTexture() == null) { 960 // UI is not ready. 961 return; 962 } 963 964 // This works around a driver issue. startPreview may fail if 965 // stopPreview/setPreviewTexture/startPreview are called several times 966 // in a row. mCameraTexture can be null after pressing home during 967 // mosaic generation and coming back. Preview will be started later in 968 // onLayoutChange->configMosaicPreview. This also reduces the latency. 969 synchronized (mRendererLock) { 970 if (mCameraTexture == null) return; 971 972 // If we're previewing already, stop the preview first (this will 973 // blank the screen). 974 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 975 976 // Set the display orientation to 0, so that the underlying mosaic 977 // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight 978 // image data from SurfaceTexture. 979 mCameraDevice.setDisplayOrientation(0); 980 981 mCameraTexture.setOnFrameAvailableListener(this); 982 mCameraDevice.setPreviewTexture(mCameraTexture); 983 } 984 mCameraDevice.startPreview(); 985 mCameraState = PREVIEW_ACTIVE; 986 } 987 988 private void stopCameraPreview() { 989 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 990 mCameraDevice.stopPreview(); 991 } 992 mCameraState = PREVIEW_STOPPED; 993 } 994 995 @Override 996 public void onUserInteraction() { 997 if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); 998 } 999 1000 @Override 1001 public boolean onBackPressed() { 1002 // If panorama is generating low res or high res mosaic, ignore back 1003 // key. So the activity will not be destroyed. 1004 if (mThreadRunning) return true; 1005 return false; 1006 } 1007 1008 private void resetScreenOn() { 1009 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1010 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1011 } 1012 1013 private void keepScreenOnAwhile() { 1014 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1015 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1016 mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1017 } 1018 1019 private void keepScreenOn() { 1020 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1021 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1022 } 1023 1024 private class WaitProcessorTask extends AsyncTask<Void, Void, Void> { 1025 @Override 1026 protected Void doInBackground(Void... params) { 1027 synchronized (mMosaicFrameProcessor) { 1028 while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { 1029 try { 1030 mMosaicFrameProcessor.wait(); 1031 } catch (Exception e) { 1032 // ignore 1033 } 1034 } 1035 } 1036 mActivity.updateStorageSpace(); 1037 return null; 1038 } 1039 1040 @Override 1041 protected void onPostExecute(Void result) { 1042 mWaitProcessorTask = null; 1043 mUI.dismissAllDialogs(); 1044 // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE); 1045 initMosaicFrameProcessorIfNeeded(); 1046 Point size = mUI.getPreviewAreaSize(); 1047 mPreviewUIWidth = size.x; 1048 mPreviewUIHeight = size.y; 1049 configMosaicPreview(); 1050 resetToPreviewIfPossible(); 1051 mActivity.updateStorageHint(mActivity.getStorageSpaceBytes()); 1052 } 1053 } 1054 1055 @Override 1056 public void cancelHighResStitching() { 1057 if (mPaused || mCameraTexture == null) return; 1058 cancelHighResComputation(); 1059 } 1060 1061 @Override 1062 public void onStop() { 1063 } 1064 1065 @Override 1066 public void installIntentFilter() { 1067 } 1068 1069 @Override 1070 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1071 } 1072 1073 1074 @Override 1075 public boolean onKeyDown(int keyCode, KeyEvent event) { 1076 return false; 1077 } 1078 1079 @Override 1080 public boolean onKeyUp(int keyCode, KeyEvent event) { 1081 return false; 1082 } 1083 1084 @Override 1085 public void onSingleTapUp(View view, int x, int y) { 1086 } 1087 1088 @Override 1089 public void onPreviewTextureCopied() { 1090 } 1091 1092 @Override 1093 public void onCaptureTextureCopied() { 1094 } 1095 1096 @Override 1097 public boolean updateStorageHintOnResume() { 1098 return false; 1099 } 1100 1101 @Override 1102 public void onShowSwitcherPopup() { 1103 } 1104 1105 @Override 1106 public void onMediaSaveServiceConnected(MediaSaveService s) { 1107 // do nothing. 1108 } 1109 } 1110