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