1 /* 2 * Copyright (C) 2013 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 17 package com.android.cts.verifier.camera.fov; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.Intent; 25 import android.graphics.Color; 26 import android.hardware.Camera; 27 import android.hardware.Camera.PictureCallback; 28 import android.hardware.Camera.ShutterCallback; 29 import android.os.Bundle; 30 import android.os.PowerManager; 31 import android.os.PowerManager.WakeLock; 32 import android.util.Log; 33 import android.view.Surface; 34 import android.view.SurfaceHolder; 35 import android.view.SurfaceView; 36 import android.view.View; 37 import android.view.View.OnClickListener; 38 import android.widget.AdapterView; 39 import android.widget.AdapterView.OnItemSelectedListener; 40 import android.widget.ArrayAdapter; 41 import android.widget.Button; 42 import android.widget.Spinner; 43 import android.widget.TextView; 44 import android.widget.Toast; 45 46 import com.android.cts.verifier.R; 47 import com.android.cts.verifier.TestResult; 48 49 import java.io.File; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.util.ArrayList; 53 import java.util.List; 54 55 /** 56 * An activity for showing the camera preview and taking a picture. 57 */ 58 public class PhotoCaptureActivity extends Activity 59 implements PictureCallback, SurfaceHolder.Callback { 60 private static final String TAG = PhotoCaptureActivity.class.getSimpleName(); 61 private static final int FOV_REQUEST_CODE = 1006; 62 private static final String PICTURE_FILENAME = "photo.jpg"; 63 private static float mReportedFovDegrees = 0; 64 private float mReportedFovPrePictureTaken = -1; 65 66 private SurfaceView mPreview; 67 private SurfaceHolder mSurfaceHolder; 68 private Spinner mResolutionSpinner; 69 private List<SelectableResolution> mSupportedResolutions; 70 private ArrayAdapter<SelectableResolution> mAdapter; 71 72 private SelectableResolution mSelectedResolution; 73 private Camera mCamera; 74 private Size mSurfaceSize; 75 private boolean mCameraInitialized = false; 76 private boolean mPreviewActive = false; 77 private int mResolutionSpinnerIndex = -1; 78 private WakeLock mWakeLock; 79 private long shutterStartTime; 80 81 private ArrayList<Integer> mPreviewSizeCamerasToProcess = new ArrayList<Integer>(); 82 83 private Dialog mActiveDialog; 84 85 /** 86 * Selected preview size per camera. If null, preview size should be 87 * automatically detected. 88 */ 89 private Size[] mPreviewSizes = null; 90 91 public static File getPictureFile(Context context) { 92 return new File(context.getExternalCacheDir(), PICTURE_FILENAME); 93 } 94 95 public static float getReportedFovDegrees() { 96 return mReportedFovDegrees; 97 } 98 99 @Override 100 protected void onCreate(Bundle savedInstanceState) { 101 super.onCreate(savedInstanceState); 102 setContentView(R.layout.camera_fov_calibration_photo_capture); 103 104 mPreview = (SurfaceView) findViewById(R.id.camera_fov_camera_preview); 105 mSurfaceHolder = mPreview.getHolder(); 106 mSurfaceHolder.addCallback(this); 107 108 // This is required for older versions of Android hardware. 109 mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 110 111 TextView textView = (TextView) findViewById(R.id.camera_fov_tap_to_take_photo); 112 textView.setTextColor(Color.WHITE); 113 114 Button setupButton = (Button) findViewById(R.id.camera_fov_settings_button); 115 setupButton.setOnClickListener(new OnClickListener() { 116 117 @Override 118 public void onClick(View v) { 119 startActivity(new Intent( 120 PhotoCaptureActivity.this, CalibrationPreferenceActivity.class)); 121 } 122 }); 123 124 Button changePreviewSizeButton = (Button) findViewById( 125 R.id.camera_fov_change_preview_size_button); 126 changePreviewSizeButton.setOnClickListener(new OnClickListener() { 127 @Override 128 public void onClick(View v) { 129 // Stop camera until preview sizes have been obtained. 130 if (mCamera != null) { 131 mCamera.stopPreview(); 132 mCamera.release(); 133 mCamera = null; 134 } 135 136 mPreviewSizeCamerasToProcess.clear(); 137 mPreviewSizes = new Size[Camera.getNumberOfCameras()]; 138 for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); ++cameraId) { 139 mPreviewSizeCamerasToProcess.add(cameraId); 140 } 141 showNextDialogToChoosePreviewSize(); 142 } 143 }); 144 145 View previewView = findViewById(R.id.camera_fov_preview_overlay); 146 previewView.setOnClickListener(new OnClickListener() { 147 @Override 148 public void onClick(View v) { 149 shutterStartTime = System.currentTimeMillis(); 150 151 mCamera.takePicture(new ShutterCallback() { 152 @Override 153 public void onShutter() { 154 long dT = System.currentTimeMillis() - shutterStartTime; 155 Log.d("CTS", "Shutter Lag: " + dT); 156 } 157 }, null, PhotoCaptureActivity.this); 158 } 159 }); 160 161 mResolutionSpinner = (Spinner) findViewById(R.id.camera_fov_resolution_selector); 162 mResolutionSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 163 @Override 164 public void onItemSelected( 165 AdapterView<?> parent, View view, int position, long id) { 166 if (mSupportedResolutions != null) { 167 SelectableResolution resolution = mSupportedResolutions.get(position); 168 switchToCamera(resolution, false); 169 170 // It should be guaranteed that the FOV is correctly updated after setParameters(). 171 mReportedFovPrePictureTaken = mCamera.getParameters().getHorizontalViewAngle(); 172 173 mResolutionSpinnerIndex = position; 174 startPreview(); 175 } 176 } 177 178 @Override 179 public void onNothingSelected(AdapterView<?> arg0) {} 180 }); 181 } 182 183 @Override 184 protected void onResume() { 185 super.onResume(); 186 // Keep the device from going to sleep. 187 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 188 mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); 189 mWakeLock.acquire(); 190 191 if (mSupportedResolutions == null) { 192 mSupportedResolutions = new ArrayList<SelectableResolution>(); 193 int numCameras = Camera.getNumberOfCameras(); 194 for (int cameraId = 0; cameraId < numCameras; ++cameraId) { 195 Camera camera = Camera.open(cameraId); 196 197 // Get the supported picture sizes and fill the spinner. 198 List<Camera.Size> supportedSizes = 199 camera.getParameters().getSupportedPictureSizes(); 200 for (Camera.Size size : supportedSizes) { 201 mSupportedResolutions.add( 202 new SelectableResolution(cameraId, size.width, size.height)); 203 } 204 camera.release(); 205 } 206 } 207 208 // Find the first untested entry. 209 for (mResolutionSpinnerIndex = 0; 210 mResolutionSpinnerIndex < mSupportedResolutions.size(); 211 mResolutionSpinnerIndex++) { 212 if (!mSupportedResolutions.get(mResolutionSpinnerIndex).tested) { 213 break; 214 } 215 } 216 217 mAdapter = new ArrayAdapter<SelectableResolution>( 218 this, android.R.layout.simple_spinner_dropdown_item, 219 mSupportedResolutions); 220 mResolutionSpinner.setAdapter(mAdapter); 221 222 mResolutionSpinner.setSelection(mResolutionSpinnerIndex); 223 setResult(RESULT_CANCELED); 224 } 225 226 @Override 227 public void onPause() { 228 if (mCamera != null) { 229 if (mPreviewActive) { 230 mCamera.stopPreview(); 231 } 232 233 mCamera.release(); 234 mCamera = null; 235 } 236 mPreviewActive = false; 237 mWakeLock.release(); 238 super.onPause(); 239 } 240 241 @Override 242 public void onPictureTaken(byte[] data, Camera camera) { 243 File pictureFile = getPictureFile(this); 244 Camera.Parameters params = mCamera.getParameters(); 245 mReportedFovDegrees = params.getHorizontalViewAngle(); 246 247 // Show error if FOV does not match the value reported before takePicture(). 248 if (mReportedFovPrePictureTaken != mReportedFovDegrees) { 249 mSupportedResolutions.get(mResolutionSpinnerIndex).tested = true; 250 mSupportedResolutions.get(mResolutionSpinnerIndex).passed = false; 251 252 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); 253 dialogBuilder.setTitle(R.string.camera_fov_reported_fov_problem); 254 dialogBuilder.setNeutralButton( 255 android.R.string.ok, new DialogInterface.OnClickListener() { 256 @Override 257 public void onClick(DialogInterface dialog, int which) { 258 if (mActiveDialog != null) { 259 mActiveDialog.dismiss(); 260 mActiveDialog = null; 261 initializeCamera(); 262 } 263 } 264 }); 265 266 String message = getResources().getString(R.string.camera_fov_reported_fov_problem_message); 267 dialogBuilder.setMessage(String.format(message, mReportedFovPrePictureTaken, mReportedFovDegrees)); 268 mActiveDialog = dialogBuilder.show(); 269 return; 270 } 271 272 try { 273 FileOutputStream fos = new FileOutputStream(pictureFile); 274 fos.write(data); 275 fos.close(); 276 Log.d(TAG, "File saved to " + pictureFile.getAbsolutePath()); 277 278 // Start activity which will use the taken picture to determine the 279 // FOV. 280 startActivityForResult(new Intent(this, DetermineFovActivity.class), 281 FOV_REQUEST_CODE + mResolutionSpinnerIndex, null); 282 } catch (IOException e) { 283 Log.e(TAG, "Could not save picture file.", e); 284 Toast.makeText(this, "Could not save picture file: " + e.getMessage(), 285 Toast.LENGTH_LONG).show(); 286 return; 287 } 288 } 289 290 @Override 291 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 292 if (resultCode != RESULT_OK) { 293 return; 294 } 295 int testIndex = requestCode - FOV_REQUEST_CODE; 296 SelectableResolution res = mSupportedResolutions.get(testIndex); 297 res.tested = true; 298 float reportedFOV = CtsTestHelper.getReportedFOV(data); 299 float measuredFOV = CtsTestHelper.getMeasuredFOV(data); 300 res.measuredFOV = measuredFOV; 301 if (CtsTestHelper.isResultPassed(reportedFOV, measuredFOV)) { 302 res.passed = true; 303 } 304 305 boolean allTested = true; 306 for (int i = 0; i < mSupportedResolutions.size(); i++) { 307 if (!mSupportedResolutions.get(i).tested) { 308 allTested = false; 309 break; 310 } 311 } 312 if (!allTested) { 313 mAdapter.notifyDataSetChanged(); 314 return; 315 } 316 317 boolean allPassed = true; 318 for (int i = 0; i < mSupportedResolutions.size(); i++) { 319 if (!mSupportedResolutions.get(i).passed) { 320 allPassed = false; 321 break; 322 } 323 } 324 if (allPassed) { 325 TestResult.setPassedResult(this, getClass().getName(), 326 CtsTestHelper.getTestDetails(mSupportedResolutions)); 327 } else { 328 TestResult.setFailedResult(this, getClass().getName(), 329 CtsTestHelper.getTestDetails(mSupportedResolutions)); 330 } 331 finish(); 332 } 333 334 @Override 335 public void surfaceChanged( 336 SurfaceHolder holder, int format, int width, int height) { 337 mSurfaceSize = new Size(width, height); 338 initializeCamera(); 339 } 340 341 @Override 342 public void surfaceCreated(SurfaceHolder holder) { 343 // Nothing to do. 344 } 345 346 @Override 347 public void surfaceDestroyed(SurfaceHolder holder) { 348 // Nothing to do. 349 } 350 351 private void showNextDialogToChoosePreviewSize() { 352 final int cameraId = mPreviewSizeCamerasToProcess.remove(0); 353 354 Camera camera = Camera.open(cameraId); 355 final List<Camera.Size> sizes = camera.getParameters() 356 .getSupportedPreviewSizes(); 357 String[] choices = new String[sizes.size()]; 358 for (int i = 0; i < sizes.size(); ++i) { 359 Camera.Size size = sizes.get(i); 360 choices[i] = size.width + " x " + size.height; 361 } 362 363 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 364 String dialogTitle = String.format( 365 getResources().getString(R.string.camera_fov_choose_preview_size_for_camera), 366 cameraId); 367 builder.setTitle( 368 dialogTitle). 369 setOnCancelListener(new DialogInterface.OnCancelListener() { 370 @Override 371 public void onCancel(DialogInterface arg0) { 372 // User cancelled preview size selection. 373 mPreviewSizes = null; 374 switchToCamera(mSelectedResolution, true); 375 } 376 }). 377 setSingleChoiceItems(choices, 0, new DialogInterface.OnClickListener() { 378 @Override 379 public void onClick(DialogInterface dialog, int which) { 380 Camera.Size size = sizes.get(which); 381 mPreviewSizes[cameraId] = new Size( 382 size.width, size.height); 383 dialog.dismiss(); 384 385 if (mPreviewSizeCamerasToProcess.isEmpty()) { 386 // We're done, re-initialize camera. 387 switchToCamera(mSelectedResolution, true); 388 } else { 389 // Process other cameras. 390 showNextDialogToChoosePreviewSize(); 391 } 392 } 393 }).create().show(); 394 camera.release(); 395 } 396 397 private void initializeCamera() { 398 initializeCamera(true); 399 } 400 401 private void initializeCamera(boolean startPreviewAfterInit) { 402 if (mCamera == null || mSurfaceHolder.getSurface() == null) { 403 return; 404 } 405 406 try { 407 mCamera.setPreviewDisplay(mSurfaceHolder); 408 } catch (Throwable t) { 409 Log.e("TAG", "Could not set preview display", t); 410 Toast.makeText(this, t.getMessage(), Toast.LENGTH_LONG).show(); 411 return; 412 } 413 Camera.Parameters params = setCameraParams(mCamera); 414 415 // Either use chosen preview size for current camera or automatically 416 // choose preview size based on view dimensions. 417 Size selectedPreviewSize = (mPreviewSizes != null) ? mPreviewSizes[mSelectedResolution.cameraId] : 418 getBestPreviewSize(mSurfaceSize.width, mSurfaceSize.height, params); 419 if (selectedPreviewSize != null) { 420 params.setPreviewSize(selectedPreviewSize.width, selectedPreviewSize.height); 421 mCamera.setParameters(params); 422 mCameraInitialized = true; 423 } 424 425 if (startPreviewAfterInit) { 426 startPreview(); 427 } 428 } 429 430 private void startPreview() { 431 if (mCameraInitialized && mCamera != null) { 432 setCameraDisplayOrientation(this, mSelectedResolution.cameraId, mCamera); 433 mCamera.startPreview(); 434 mPreviewActive = true; 435 } 436 } 437 438 private void switchToCamera(SelectableResolution resolution, boolean startPreview) { 439 if (mCamera != null) { 440 mCamera.stopPreview(); 441 mCamera.release(); 442 } 443 444 mSelectedResolution = resolution; 445 mCamera = Camera.open(mSelectedResolution.cameraId); 446 447 initializeCamera(startPreview); 448 } 449 450 /** 451 * Get the best supported focus mode. 452 * 453 * @param camera - Android camera object. 454 * @return the best supported focus mode. 455 */ 456 private static String getFocusMode(Camera camera) { 457 List<String> modes = camera.getParameters().getSupportedFocusModes(); 458 if (modes != null) { 459 if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) { 460 Log.v(TAG, "Using Focus mode infinity"); 461 return Camera.Parameters.FOCUS_MODE_INFINITY; 462 } 463 if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) { 464 Log.v(TAG, "Using Focus mode fixed"); 465 return Camera.Parameters.FOCUS_MODE_FIXED; 466 } 467 } 468 Log.v(TAG, "Using Focus mode auto."); 469 return Camera.Parameters.FOCUS_MODE_AUTO; 470 } 471 472 /** 473 * Set the common camera parameters on the given camera and returns the 474 * parameter object for further modification, if needed. 475 */ 476 private Camera.Parameters setCameraParams(Camera camera) { 477 // The picture size is taken and set from the spinner selection 478 // callback. 479 Camera.Parameters params = camera.getParameters(); 480 params.setJpegThumbnailSize(0, 0); 481 params.setJpegQuality(100); 482 params.setFocusMode(getFocusMode(camera)); 483 params.setZoom(0); 484 params.setPictureSize(mSelectedResolution.width, mSelectedResolution.height); 485 return params; 486 } 487 488 private Size getBestPreviewSize( 489 int width, int height, Camera.Parameters parameters) { 490 Size result = null; 491 492 for (Camera.Size size : parameters.getSupportedPreviewSizes()) { 493 if (size.width <= width && size.height <= height) { 494 if (result == null) { 495 result = new Size(size.width, size.height); 496 } else { 497 int resultArea = result.width * result.height; 498 int newArea = size.width * size.height; 499 500 if (newArea > resultArea) { 501 result = new Size(size.width, size.height); 502 } 503 } 504 } 505 } 506 return result; 507 } 508 509 public static void setCameraDisplayOrientation(Activity activity, 510 int cameraId, android.hardware.Camera camera) { 511 android.hardware.Camera.CameraInfo info = 512 new android.hardware.Camera.CameraInfo(); 513 android.hardware.Camera.getCameraInfo(cameraId, info); 514 int rotation = activity.getWindowManager().getDefaultDisplay() 515 .getRotation(); 516 int degrees = 0; 517 switch (rotation) { 518 case Surface.ROTATION_0: degrees = 0; break; 519 case Surface.ROTATION_90: degrees = 90; break; 520 case Surface.ROTATION_180: degrees = 180; break; 521 case Surface.ROTATION_270: degrees = 270; break; 522 } 523 524 int result; 525 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 526 result = (info.orientation + degrees) % 360; 527 result = (360 - result) % 360; // compensate the mirror 528 } else { // back-facing 529 result = (info.orientation - degrees + 360) % 360; 530 } 531 camera.setDisplayOrientation(result); 532 } 533 } 534