PanoramaActivity.java revision eeb94d4de94bfd4e01f3a716803f77a530f5b92c
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.ImageFormat; 33import android.graphics.Matrix; 34import android.graphics.PixelFormat; 35import android.graphics.Rect; 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.net.Uri; 45import android.os.Bundle; 46import android.os.Handler; 47import android.os.Message; 48import android.util.Log; 49import android.view.SurfaceHolder; 50import android.view.SurfaceView; 51import android.view.View; 52import android.view.WindowManager; 53import android.widget.Button; 54import android.widget.ImageView; 55 56import java.io.ByteArrayOutputStream; 57import java.util.List; 58 59/** 60 * Activity to handle panorama capturing. 61 */ 62public class PanoramaActivity extends Activity implements 63 ModePicker.OnModeChangeListener, SurfaceHolder.Callback { 64 public static final int DEFAULT_SWEEP_ANGLE = 60; 65 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 66 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 67 68 private static final int MSG_FINAL_MOSAIC_READY = 1; 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 74 // Ratio of nanosecond to second 75 private static final float NS2S = 1.0f / 1000000000.0f; 76 private View mPanoControlLayout; 77 private View mCaptureLayout; 78 private View mReviewLayout; 79 private SurfaceView mPreview; 80 private ImageView mReview; 81 private CaptureView mCaptureView; 82 private MosaicRendererSurfaceView mRealTimeMosaicView; 83 84 private ShutterButton mShutterButton; 85 private Button mStopButton; 86 private int mPreviewWidth; 87 private int mPreviewHeight; 88 private Camera mCameraDevice; 89 private int mCameraState; 90 private SensorManager mSensorManager; 91 private Sensor mSensor; 92 private ModePicker mModePicker; 93 private MosaicFrameProcessor mMosaicFrameProcessor; 94 private String mCurrentImagePath = null; 95 private long mTimeTaken; 96 private Handler mMainHandler; 97 private SurfaceHolder mSurfaceHolder; 98 99 @Override 100 public void onCreate(Bundle icicle) { 101 super.onCreate(icicle); 102 103 getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 104 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 105 106 createContentView(); 107 108 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 109 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); 110 if (mSensor == null) { 111 mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); 112 } 113 114 mMainHandler = new Handler() { 115 @Override 116 public void handleMessage(Message msg) { 117 switch (msg.what) { 118 case MSG_FINAL_MOSAIC_READY: 119 Uri uri = (Uri) msg.obj; 120 showFinalMosaic(uri); 121 } 122 } 123 }; 124 } 125 126 private void setupCamera() { 127 openCamera(); 128 Parameters parameters = mCameraDevice.getParameters(); 129 setupCaptureParams(parameters); 130 configureCamera(parameters); 131 } 132 133 private void releaseCamera() { 134 if (mCameraDevice != null) { 135 CameraHolder.instance().release(); 136 mCameraDevice = null; 137 mCameraState = PREVIEW_STOPPED; 138 } 139 } 140 141 private void openCamera() { 142 try { 143 mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId()); 144 } catch (CameraHardwareException e) { 145 Util.showErrorAndFinish(this, R.string.cannot_connect_camera); 146 return; 147 } catch (CameraDisabledException e) { 148 Util.showErrorAndFinish(this, R.string.camera_disabled); 149 return; 150 } 151 } 152 153 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 154 boolean needSmaller) { 155 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 156 boolean hasFound = false; 157 for (Size size : supportedSizes) { 158 int h = size.height; 159 int w = size.width; 160 // we only want 4:3 format. 161 int d = DEFAULT_CAPTURE_PIXELS - h * w; 162 if (needSmaller && d < 0) { // no bigger preview than 960x720. 163 continue; 164 } 165 if (need4To3 && (h * 4 != w * 3)) { 166 continue; 167 } 168 d = Math.abs(d); 169 if (d < pixelsDiff) { 170 mPreviewWidth = w; 171 mPreviewHeight = h; 172 pixelsDiff = d; 173 hasFound = true; 174 } 175 } 176 return hasFound; 177 } 178 179 private void setupCaptureParams(Parameters parameters) { 180 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 181 if (!findBestPreviewSize(supportedSizes, true, true)) { 182 Log.w(TAG, "No 4:3 ratio preview size supported."); 183 if (!findBestPreviewSize(supportedSizes, false, true)) { 184 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 185 findBestPreviewSize(supportedSizes, false, false); 186 } 187 } 188 Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); 189 parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 190 191 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 192 int last = frameRates.size() - 1; 193 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 194 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 195 parameters.setPreviewFpsRange(minFps, maxFps); 196 Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); 197 198 // TODO: use camera API after it is published. 199 parameters.set("recording-hint", "false"); 200 } 201 202 public int getPreviewBufSize() { 203 PixelFormat pixelInfo = new PixelFormat(); 204 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 205 // TODO: remove this extra 32 byte after the driver bug is fixed. 206 return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 207 } 208 209 private void configureCamera(Parameters parameters) { 210 mCameraDevice.setParameters(parameters); 211 212 int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this), 213 CameraHolder.instance().getBackCameraId()); 214 mCameraDevice.setDisplayOrientation(orientation); 215 216 int bufSize = getPreviewBufSize(); 217 Log.v(TAG, "BufSize = " + bufSize); 218 for (int i = 0; i < 10; i++) { 219 try { 220 mCameraDevice.addCallbackBuffer(new byte[bufSize]); 221 } catch (OutOfMemoryError e) { 222 Log.v(TAG, "Buffer allocation failed: buffer " + i); 223 break; 224 } 225 } 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 startCapture() { 246 // Reset values so we can do this again. 247 mTimeTaken = System.currentTimeMillis(); 248 249 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 250 @Override 251 public void onProgress(boolean isFinished, float translationRate, int traversedAngleX, 252 int traversedAngleY, Bitmap lowResBitmapAlpha, Matrix transformaMatrix) { 253 if (isFinished) { 254 stopCapture(); 255 } else { 256 updateProgress(translationRate, traversedAngleX, traversedAngleY, 257 lowResBitmapAlpha, transformaMatrix); 258 } 259 } 260 }); 261 262 // Preview callback used whenever new viewfinder frame is available 263 mCameraDevice.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() { 264 @Override 265 public void onPreviewFrame(final byte[] data, Camera camera) { 266 mMosaicFrameProcessor.processFrame(data, mPreviewWidth, mPreviewHeight); 267 // The returned buffer needs be added back to callback buffer 268 // again. 269 camera.addCallbackBuffer(data); 270 } 271 }); 272 273 mCaptureLayout.setVisibility(View.VISIBLE); 274 mPanoControlLayout.setVisibility(View.GONE); 275 } 276 277 private void stopCapture() { 278 mMosaicFrameProcessor.setProgressListener(null); 279 mCameraDevice.stopPreview(); 280 mCameraDevice.setPreviewCallbackWithBuffer(null); 281 // TODO: show some dialog for long computation. 282 Thread t = new Thread() { 283 @Override 284 public void run() { 285 generateAndStoreFinalMosaic(false); 286 } 287 }; 288 t.start(); 289 } 290 291 private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY, 292 Bitmap lowResBitmapAlpha, Matrix transformationMatrix) { 293 294 mRealTimeMosaicView.setReady(); 295 mRealTimeMosaicView.requestRender(); 296 297 if (translationRate > 150) { 298 // TODO: remove the text and draw implications according to the UI 299 // spec. 300 mCaptureView.setStatusText("S L O W D O W N"); 301 mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1); 302 mCaptureView.invalidate(); 303 } else { 304 mCaptureView.setStatusText(""); 305 mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1); 306 mCaptureView.invalidate(); 307 } 308 } 309 310 private void createContentView() { 311 setContentView(R.layout.panorama); 312 313 mCaptureLayout = (View) findViewById(R.id.pano_capture_layout); 314 mReviewLayout = (View) findViewById(R.id.pano_review_layout); 315 316 mPreview = (SurfaceView) findViewById(R.id.pano_preview_view); 317 mPreview.getHolder().addCallback(this); 318 mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view); 319 mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2); 320 mReview = (ImageView) findViewById(R.id.pano_reviewarea); 321 322 mRealTimeMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer); 323 324 mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button); 325 mShutterButton.setOnClickListener(new View.OnClickListener() { 326 @Override 327 public void onClick(View v) { 328 startCapture(); 329 } 330 }); 331 mStopButton = (Button) findViewById(R.id.pano_capture_stop_button); 332 mStopButton.setOnClickListener(new View.OnClickListener() { 333 @Override 334 public void onClick(View v) { 335 stopCapture(); 336 } 337 }); 338 339 mPanoControlLayout = (View) findViewById(R.id.pano_control_layout); 340 341 mModePicker = (ModePicker) findViewById(R.id.mode_picker); 342 mModePicker.setVisibility(View.VISIBLE); 343 mModePicker.setOnModeChangeListener(this); 344 mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA); 345 } 346 347 private void showFinalMosaic(Uri uri) { 348 mReview.setImageURI(uri); 349 mCaptureLayout.setVisibility(View.INVISIBLE); 350 mPreview.setVisibility(View.INVISIBLE); 351 mReviewLayout.setVisibility(View.VISIBLE); 352 mCaptureView.setStatusText(""); 353 mCaptureView.setSweepAngle(0); 354 } 355 356 @Override 357 protected void onPause() { 358 super.onPause(); 359 releaseCamera(); 360 mMosaicFrameProcessor.onPause(); 361 mCaptureView.onPause(); 362 mRealTimeMosaicView.onPause(); 363 mSensorManager.unregisterListener(mListener); 364 System.gc(); 365 } 366 367 @Override 368 protected void onResume() { 369 super.onResume(); 370 371 /* 372 * It is not necessary to get accelerometer events at a very high rate, 373 * by using a slower rate (SENSOR_DELAY_UI), we get an automatic 374 * low-pass filter, which "extracts" the gravity component of the 375 * acceleration. As an added benefit, we use less power and CPU 376 * resources. 377 */ 378 mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI); 379 380 setupCamera(); 381 startPreview(); 382 383 if (mMosaicFrameProcessor == null) { 384 // Start the activity for the first time. 385 mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5, 386 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 387 mMosaicFrameProcessor.onResume(); 388 } else { 389 mMosaicFrameProcessor.onResume(); 390 } 391 mCaptureView.onResume(); 392 mRealTimeMosaicView.onResume(); 393 } 394 395 private final SensorEventListener mListener = new SensorEventListener() { 396 private float mCompassCurrX; // degrees 397 private float mCompassCurrY; // degrees 398 private float mTimestamp; 399 400 public void onSensorChanged(SensorEvent event) { 401 if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { 402 if (mTimestamp != 0) { 403 final float dT = (event.timestamp - mTimestamp) * NS2S; 404 mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI; 405 mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI; 406 } 407 mTimestamp = event.timestamp; 408 409 } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { 410 mCompassCurrX = event.values[0]; 411 mCompassCurrY = event.values[1]; 412 } 413 414 if (mMosaicFrameProcessor != null) { 415 mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY); 416 } 417 } 418 419 @Override 420 public void onAccuracyChanged(Sensor sensor, int accuracy) { 421 } 422 }; 423 424 public void generateAndStoreFinalMosaic(boolean highRes) { 425 mMosaicFrameProcessor.createMosaic(highRes); 426 427 mCurrentImagePath = PanoUtil.createName( 428 getResources().getString(R.string.pano_file_name_format), mTimeTaken); 429 430 if (highRes) { 431 mCurrentImagePath += "_HR"; 432 } else { 433 mCurrentImagePath += "_LR"; 434 } 435 436 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 437 int len = imageData.length - 8; 438 439 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 440 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 441 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 442 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 443 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 444 445 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 446 ByteArrayOutputStream out = new ByteArrayOutputStream(); 447 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 448 try { 449 out.close(); 450 } catch (Exception e) { 451 Log.e(TAG, "Exception in storing final mosaic", e); 452 return; 453 } 454 Uri uri = Storage.addImage( 455 getContentResolver(), mCurrentImagePath, mTimeTaken, null, 0, 456 out.toByteArray()); 457 458 mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_FINAL_MOSAIC_READY, uri)); 459 // Now's a good time to run the GC. Since we won't do any explicit 460 // allocation during the test, the GC should stay dormant and not 461 // influence our results. 462 System.runFinalization(); 463 System.gc(); 464 } 465 466 @Override 467 public void surfaceCreated(SurfaceHolder holder) { 468 } 469 470 @Override 471 public void surfaceDestroyed(SurfaceHolder holder) { 472 } 473 474 @Override 475 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { 476 mSurfaceHolder = holder; 477 478 if (mCameraDevice == null) return; 479 480 // Set preview display if the surface is being created. Preview was 481 // already started. Also restart the preview if display rotation has 482 // changed. Sometimes this happens when the device is held in portrait 483 // and camera app is opened. Rotation animation takes some time and 484 // display rotation in onCreate may not be what we want. 485 if (holder.isCreating()) { 486 // Set preview display if the surface is being created and preview 487 // was already started. That means preview display was set to null 488 // and we need to set it now. 489 setPreviewDisplay(holder); 490 } else { 491 // 1. Restart the preview if the size of surface was changed. The 492 // framework may not support changing preview display on the fly. 493 // 2. Start the preview now if surface was destroyed and preview 494 // stopped. 495 startPreview(); 496 } 497 } 498 499 private void setPreviewDisplay(SurfaceHolder holder) { 500 try { 501 mCameraDevice.setPreviewDisplay(holder); 502 } catch (Throwable ex) { 503 releaseCamera(); 504 throw new RuntimeException("setPreviewDisplay failed", ex); 505 } 506 } 507 508 private void startPreview() { 509 // If we're previewing already, stop the preview first (this will blank 510 // the screen). 511 if (mCameraState != PREVIEW_STOPPED) stopPreview(); 512 513 setPreviewDisplay(mSurfaceHolder); 514 515 try { 516 Log.v(TAG, "startPreview"); 517 mCameraDevice.startPreview(); 518 } catch (Throwable ex) { 519 releaseCamera(); 520 throw new RuntimeException("startPreview failed", ex); 521 } 522 mCameraState = PREVIEW_ACTIVE; 523 } 524 525 private void stopPreview() { 526 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 527 Log.v(TAG, "stopPreview"); 528 mCameraDevice.stopPreview(); 529 } 530 mCameraState = PREVIEW_STOPPED; 531 } 532} 533