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