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