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