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