PanoramaActivity.java revision 41a2e9735136f372de95652d0828600282c8e967
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.camera.panorama; 18 19import com.android.camera.CameraDisabledException; 20import com.android.camera.CameraHardwareException; 21import com.android.camera.CameraHolder; 22import com.android.camera.MenuHelper; 23import com.android.camera.ModePicker; 24import com.android.camera.R; 25import com.android.camera.ShutterButton; 26import com.android.camera.Storage; 27import com.android.camera.Util; 28 29import android.app.Activity; 30import android.content.Context; 31import android.graphics.Bitmap; 32import android.graphics.BitmapFactory; 33import android.graphics.ImageFormat; 34import android.graphics.PixelFormat; 35import android.graphics.Rect; 36import android.graphics.SurfaceTexture; 37import android.graphics.YuvImage; 38import android.hardware.Camera; 39import android.hardware.Sensor; 40import android.hardware.SensorEvent; 41import android.hardware.SensorEventListener; 42import android.hardware.SensorManager; 43import android.hardware.Camera.Parameters; 44import android.hardware.Camera.Size; 45import android.os.Bundle; 46import android.os.Handler; 47import android.os.Message; 48import android.util.Log; 49import android.view.SurfaceView; 50import android.view.View; 51import android.view.WindowManager; 52import android.widget.ImageView; 53 54import java.io.ByteArrayOutputStream; 55import java.util.List; 56 57/** 58 * Activity to handle panorama capturing. 59 */ 60public class PanoramaActivity extends Activity implements 61 ModePicker.OnModeChangeListener, 62 SurfaceTexture.OnFrameAvailableListener { 63 public static final int DEFAULT_SWEEP_ANGLE = 60; 64 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 65 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 66 67 private static final int MSG_FINAL_MOSAIC_READY = 1; 68 private static final int MSG_RESET_TO_PREVIEW = 2; 69 70 private static final String TAG = "PanoramaActivity"; 71 private static final int PREVIEW_STOPPED = 0; 72 private static final int PREVIEW_ACTIVE = 1; 73 private static final int CAPTURE_VIEWFINDER = 0; 74 private static final int CAPTURE_MOSAIC = 1; 75 76 // Ratio of nanosecond to second 77 private static final float NS2S = 1.0f / 1000000000.0f; 78 79 private boolean mPausing; 80 81 private View mPanoControlLayout; 82 private View mCaptureLayout; 83 private View mReviewLayout; 84 private SurfaceView mPreview; 85 private ImageView mReview; 86 private CaptureView mCaptureView; 87 private MosaicRendererSurfaceView mRealTimeMosaicView; 88 private ShutterButton mShutterButton; 89 90 private byte[] mFinalJpegData; 91 92 private int mPreviewWidth; 93 private int mPreviewHeight; 94 private Camera mCameraDevice; 95 private int mCameraState; 96 private int mCaptureState; 97 private SensorManager mSensorManager; 98 private Sensor mSensor; 99 private ModePicker mModePicker; 100 private MosaicFrameProcessor mMosaicFrameProcessor; 101 private String mCurrentImagePath = null; 102 private long mTimeTaken; 103 private Handler mMainHandler; 104 private SurfaceTexture mSurface; 105 private boolean mUseSurfaceTexture = true; 106 107 private boolean mThreadRunning; 108 109 @Override 110 public void onCreate(Bundle icicle) { 111 super.onCreate(icicle); 112 113 getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 114 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 115 116 createContentView(); 117 118 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 119 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 120 if (mSensor == null) { 121 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 122 } 123 124 mMainHandler = new Handler() { 125 @Override 126 public void handleMessage(Message msg) { 127 switch (msg.what) { 128 case MSG_FINAL_MOSAIC_READY: 129 mThreadRunning = false; 130 showFinalMosaic((Bitmap) msg.obj); 131 break; 132 case MSG_RESET_TO_PREVIEW: 133 mThreadRunning = false; 134 resetToPreview(); 135 break; 136 } 137 clearMosaicFrameProcessorIfNeeded(); 138 } 139 }; 140 } 141 142 public void createSurfaceTextureAndStartPreview(int textureID) 143 { 144 /* 145 * Create the SurfaceTexture that will feed this textureID, and pass it to the camera 146 */ 147 mSurface = new SurfaceTexture(textureID); 148 mSurface.setOnFrameAvailableListener(this); 149 startPreview(); 150 Log.i(TAG, "Created Surface Texture"); 151 } 152 153 public SurfaceTexture getSurfaceTexture() 154 { 155 return mSurface; 156 } 157 158 private void setupCamera() { 159 openCamera(); 160 Parameters parameters = mCameraDevice.getParameters(); 161 setupCaptureParams(parameters); 162 configureCamera(parameters); 163 } 164 165 private void releaseCamera() { 166 if (mCameraDevice != null) { 167 mCameraDevice.setPreviewCallbackWithBuffer(null); 168 CameraHolder.instance().release(); 169 mCameraDevice = null; 170 mCameraState = PREVIEW_STOPPED; 171 } 172 } 173 174 private void openCamera() { 175 try { 176 mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId()); 177 } catch (CameraHardwareException e) { 178 Util.showErrorAndFinish(this, R.string.cannot_connect_camera); 179 return; 180 } catch (CameraDisabledException e) { 181 Util.showErrorAndFinish(this, R.string.camera_disabled); 182 return; 183 } 184 } 185 186 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 187 boolean needSmaller) { 188 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 189 boolean hasFound = false; 190 for (Size size : supportedSizes) { 191 int h = size.height; 192 int w = size.width; 193 // we only want 4:3 format. 194 int d = DEFAULT_CAPTURE_PIXELS - h * w; 195 if (needSmaller && d < 0) { // no bigger preview than 960x720. 196 continue; 197 } 198 if (need4To3 && (h * 4 != w * 3)) { 199 continue; 200 } 201 d = Math.abs(d); 202 if (d < pixelsDiff) { 203 mPreviewWidth = w; 204 mPreviewHeight = h; 205 pixelsDiff = d; 206 hasFound = true; 207 } 208 } 209 return hasFound; 210 } 211 212 private void setupCaptureParams(Parameters parameters) { 213 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 214 if (!findBestPreviewSize(supportedSizes, true, true)) { 215 Log.w(TAG, "No 4:3 ratio preview size supported."); 216 if (!findBestPreviewSize(supportedSizes, false, true)) { 217 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 218 findBestPreviewSize(supportedSizes, false, false); 219 } 220 } 221 Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); 222 parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 223 224 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 225 int last = frameRates.size() - 1; 226 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 227 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 228 parameters.setPreviewFpsRange(minFps, maxFps); 229 Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); 230 231 parameters.setRecordingHint(false); 232 } 233 234 public int getPreviewBufSize() { 235 PixelFormat pixelInfo = new PixelFormat(); 236 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 237 // TODO: remove this extra 32 byte after the driver bug is fixed. 238 return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 239 } 240 241 private void configureCamera(Parameters parameters) { 242 mCameraDevice.setParameters(parameters); 243 244 int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this), 245 CameraHolder.instance().getBackCameraId()); 246 mCameraDevice.setDisplayOrientation(orientation); 247 248 if(!mUseSurfaceTexture) { 249 int bufSize = getPreviewBufSize(); 250 Log.v(TAG, "BufSize = " + bufSize); 251 for (int i = 0; i < 10; i++) { 252 try { 253 mCameraDevice.addCallbackBuffer(new byte[bufSize]); 254 } catch (OutOfMemoryError e) { 255 Log.v(TAG, "Buffer allocation failed: buffer " + i); 256 break; 257 } 258 } 259 } 260 } 261 262 private boolean switchToOtherMode(int mode) { 263 if (isFinishing()) { 264 return false; 265 } 266 MenuHelper.gotoMode(mode, this); 267 finish(); 268 return true; 269 } 270 271 public boolean onModeChanged(int mode) { 272 if (mode != ModePicker.MODE_PANORAMA) { 273 return switchToOtherMode(mode); 274 } else { 275 return true; 276 } 277 } 278 279 public void runViewFinder() { 280 mRealTimeMosaicView.setWarping(false); 281 282 // First update the surface texture... 283 mRealTimeMosaicView.updateSurfaceTexture(); 284 // ...then call preprocess to render it to low-res and high-res RGB textures 285 mRealTimeMosaicView.preprocess(); 286 287 mRealTimeMosaicView.setReady(); 288 mRealTimeMosaicView.requestRender(); 289 } 290 291 public void runMosaicCapture() { 292 mRealTimeMosaicView.setWarping(true); 293 294 // Lock the condition variable 295 mRealTimeMosaicView.lockPreviewReadyFlag(); 296 // First update the surface texture... 297 mRealTimeMosaicView.updateSurfaceTexture(); 298 // ...then call preprocess to render it to low-res and high-res RGB textures 299 mRealTimeMosaicView.preprocess(); 300 // Now, transfer the textures from GPU to CPU memory for processing 301 mRealTimeMosaicView.transferGPUtoCPU(); 302 // Wait on the condition variable (will be opened when GPU->CPU transfer is done). 303 mRealTimeMosaicView.waitUntilPreviewReady(); 304 305 mMosaicFrameProcessor.processFrame(null); 306 } 307 308 synchronized public void onFrameAvailable(SurfaceTexture surface) { 309 /* For simplicity, SurfaceTexture calls here when it has new 310 * data available. Call may come in from some random thread, 311 * so let's be safe and use synchronize. No OpenGL calls can be done here. 312 */ 313 if (mCaptureState == CAPTURE_VIEWFINDER) { 314 runViewFinder(); 315 } else { 316 runMosaicCapture(); 317 } 318 } 319 320 public void startCapture() { 321 // Reset values so we can do this again. 322 mTimeTaken = System.currentTimeMillis(); 323 mCaptureState = CAPTURE_MOSAIC; 324 325 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 326 @Override 327 public void onProgress(boolean isFinished, float translationRate, int traversedAngleX, 328 int traversedAngleY) { 329 if (isFinished) { 330 stopCapture(); 331 } else { 332 updateProgress(translationRate, traversedAngleX, traversedAngleY); 333 } 334 } 335 }); 336 337 if (!mUseSurfaceTexture) { 338 // Preview callback used whenever new viewfinder frame is available 339 mCameraDevice.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() { 340 @Override 341 public void onPreviewFrame(final byte[] data, Camera camera) { 342 mMosaicFrameProcessor.processFrame(data); 343 // The returned buffer needs be added back to callback buffer 344 // again. 345 camera.addCallbackBuffer(data); 346 } 347 }); 348 } 349 350 mCaptureLayout.setVisibility(View.VISIBLE); 351 mPreview.setVisibility(View.INVISIBLE); // will be re-used, invisible is better than gone. 352 mRealTimeMosaicView.setVisibility(View.VISIBLE); 353 mPanoControlLayout.setVisibility(View.GONE); 354 355 } 356 357 private void stopCapture() { 358 mCaptureState = CAPTURE_VIEWFINDER; 359 360 mMosaicFrameProcessor.setProgressListener(null); 361 stopPreview(); 362 363 if (!mUseSurfaceTexture) { 364 mCameraDevice.setPreviewCallbackWithBuffer(null); 365 } 366 367 mSurface.setOnFrameAvailableListener(null); 368 369 // TODO: show some dialog for long computation. 370 if (!mThreadRunning) { 371 mThreadRunning = true; 372 Thread t = new Thread() { 373 @Override 374 public void run() { 375 generateAndStoreFinalMosaic(false); 376 } 377 }; 378 t.start(); 379 } 380 } 381 382 private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY) { 383 384 mRealTimeMosaicView.setReady(); 385 mRealTimeMosaicView.requestRender(); 386 387 if (translationRate > 150) { 388 // TODO: remove the text and draw implications according to the UI 389 // spec. 390 mCaptureView.setStatusText("S L O W D O W N"); 391 mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1); 392 mCaptureView.invalidate(); 393 } else { 394 mCaptureView.setStatusText(""); 395 mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1); 396 mCaptureView.invalidate(); 397 } 398 } 399 400 private void createContentView() { 401 setContentView(R.layout.panorama); 402 403 mCaptureState = CAPTURE_VIEWFINDER; 404 405 mCaptureLayout = (View) findViewById(R.id.pano_capture_layout); 406 mReviewLayout = (View) findViewById(R.id.pano_review_layout); 407 408 mPreview = (SurfaceView) findViewById(R.id.pano_preview_view); 409 410 mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view); 411 mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2); 412 mReview = (ImageView) findViewById(R.id.pano_reviewarea); 413 414 mRealTimeMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer); 415 mRealTimeMosaicView.setUIObject(this); 416 417 mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button); 418 mShutterButton.setOnClickListener(new View.OnClickListener() { 419 @Override 420 public void onClick(View v) { 421 if (mPausing || mThreadRunning) return; 422 startCapture(); 423 } 424 }); 425 426 mPanoControlLayout = (View) findViewById(R.id.pano_control_layout); 427 428 mModePicker = (ModePicker) findViewById(R.id.mode_picker); 429 mModePicker.setVisibility(View.VISIBLE); 430 mModePicker.setOnModeChangeListener(this); 431 mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA); 432 433 mRealTimeMosaicView.setVisibility(View.VISIBLE); 434 } 435 436 @OnClickAttr 437 public void onStopButtonClicked(View v) { 438 if (mPausing || mThreadRunning) return; 439 stopCapture(); 440 } 441 442 @OnClickAttr 443 public void onOkButtonClicked(View v) { 444 if (mPausing || mThreadRunning) return; 445 mThreadRunning = true; 446 Thread t = new Thread() { 447 @Override 448 public void run() { 449 saveFinalMosaic(); 450 } 451 }; 452 t.start(); 453 } 454 455 @OnClickAttr 456 public void onRetakeButtonClicked(View v) { 457 if (mPausing || mThreadRunning) return; 458 resetToPreview(); 459 } 460 461 private void resetToPreview() { 462 mCaptureState = CAPTURE_VIEWFINDER; 463 464 mReviewLayout.setVisibility(View.GONE); 465 mPreview.setVisibility(View.VISIBLE); 466 mPanoControlLayout.setVisibility(View.VISIBLE); 467 mCaptureLayout.setVisibility(View.GONE); 468 mMosaicFrameProcessor.reset(); 469 470 mSurface.setOnFrameAvailableListener(this); 471 472 if (!mPausing) startPreview(); 473 474 mRealTimeMosaicView.setVisibility(View.VISIBLE); 475 } 476 477 private void showFinalMosaic(Bitmap bitmap) { 478 if (bitmap != null) { 479 mReview.setImageBitmap(bitmap); 480 mCaptureLayout.setVisibility(View.GONE); 481 mPreview.setVisibility(View.INVISIBLE); 482 mReviewLayout.setVisibility(View.VISIBLE); 483 mCaptureView.setStatusText(""); 484 mCaptureView.setSweepAngle(0); 485 mCaptureView.invalidate(); 486 } 487 } 488 489 private void saveFinalMosaic() { 490 if (mFinalJpegData != null) { 491 Storage.addImage(getContentResolver(), mCurrentImagePath, mTimeTaken, null, 0, 492 mFinalJpegData); 493 mFinalJpegData = null; 494 } 495 mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW)); 496 } 497 498 private void clearMosaicFrameProcessorIfNeeded() { 499 if (!mPausing || mThreadRunning) return; 500 mMosaicFrameProcessor.clear(); 501 } 502 503 private void initMosaicFrameProcessorIfNeeded() { 504 if (mPausing || mThreadRunning) return; 505 if (mMosaicFrameProcessor == null) { 506 // Start the activity for the first time. 507 mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5, 508 mPreviewWidth, mPreviewHeight, getPreviewBufSize(), mUseSurfaceTexture); 509 } 510 mMosaicFrameProcessor.initialize(); 511 } 512 513 @Override 514 protected void onPause() { 515 super.onPause(); 516 mSurface.setOnFrameAvailableListener(null); 517 releaseCamera(); 518 mPausing = true; 519 520 mRealTimeMosaicView.onPause(); 521 mCaptureView.onPause(); 522 mSensorManager.unregisterListener(mListener); 523 clearMosaicFrameProcessorIfNeeded(); 524 System.gc(); 525 } 526 527 @Override 528 protected void onResume() { 529 super.onResume(); 530 531 mPausing = false; 532 /* 533 * It is not necessary to get accelerometer events at a very high rate, 534 * by using a slower rate (SENSOR_DELAY_UI), we get an automatic 535 * low-pass filter, which "extracts" the gravity component of the 536 * acceleration. As an added benefit, we use less power and CPU 537 * resources. 538 */ 539 mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI); 540 mCaptureState = CAPTURE_VIEWFINDER; 541 setupCamera(); 542 // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size 543 // has to be decided by camera device. 544 initMosaicFrameProcessorIfNeeded(); 545 mCaptureView.onResume(); 546 mRealTimeMosaicView.onResume(); 547 } 548 549 private final SensorEventListener mListener = new SensorEventListener() { 550 private float mCompassCurrX; // degrees 551 private float mCompassCurrY; // degrees 552 private float mTimestamp; 553 554 public void onSensorChanged(SensorEvent event) { 555 if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { 556 if (mTimestamp != 0) { 557 final float dT = (event.timestamp - mTimestamp) * NS2S; 558 mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI; 559 mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI; 560 } 561 mTimestamp = event.timestamp; 562 563 } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { 564 mCompassCurrX = event.values[0]; 565 mCompassCurrY = event.values[1]; 566 } 567 568 if (mMosaicFrameProcessor != null) { 569 mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY); 570 } 571 } 572 573 @Override 574 public void onAccuracyChanged(Sensor sensor, int accuracy) { 575 } 576 }; 577 578 public void generateAndStoreFinalMosaic(boolean highRes) { 579 mMosaicFrameProcessor.createMosaic(highRes); 580 581 mCurrentImagePath = PanoUtil.createName( 582 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 583 584 if (highRes) { 585 mCurrentImagePath += "_HR"; 586 } else { 587 mCurrentImagePath += "_LR"; 588 } 589 590 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 591 int len = imageData.length - 8; 592 593 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 594 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 595 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 596 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 597 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 598 599 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 600 ByteArrayOutputStream out = new ByteArrayOutputStream(); 601 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 602 try { 603 out.close(); 604 } catch (Exception e) { 605 Log.e(TAG, "Exception in storing final mosaic", e); 606 return; 607 } 608 609 mFinalJpegData = out.toByteArray(); 610 Bitmap bitmap = BitmapFactory.decodeByteArray(mFinalJpegData, 0, mFinalJpegData.length); 611 mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_FINAL_MOSAIC_READY, bitmap)); 612 // Now's a good time to run the GC. Since we won't do any explicit 613 // allocation during the test, the GC should stay dormant and not 614 // influence our results. 615 System.runFinalization(); 616 System.gc(); 617 } 618 619 private void setPreviewTexture(SurfaceTexture surface) { 620 try { 621 mCameraDevice.setPreviewTexture(surface); 622 } catch (Throwable ex) { 623 releaseCamera(); 624 throw new RuntimeException("setPreviewTexture failed", ex); 625 } 626 } 627 628 private void startPreview() { 629 // If we're previewing already, stop the preview first (this will blank 630 // the screen). 631 if (mCameraState != PREVIEW_STOPPED) stopPreview(); 632 633 setPreviewTexture(mSurface); 634 635 try { 636 Log.v(TAG, "startPreview"); 637 mCameraDevice.startPreview(); 638 } catch (Throwable ex) { 639 releaseCamera(); 640 throw new RuntimeException("startPreview failed", ex); 641 } 642 mCameraState = PREVIEW_ACTIVE; 643 } 644 645 private void stopPreview() { 646 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 647 Log.v(TAG, "stopPreview"); 648 mCameraDevice.stopPreview(); 649 } 650 mCameraState = PREVIEW_STOPPED; 651 } 652} 653