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