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