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