1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.android.cts.verifier.camera.orientation; 15 16 import android.content.Intent; 17 import android.graphics.Bitmap; 18 import android.graphics.BitmapFactory; 19 import android.graphics.ImageFormat; 20 import android.graphics.Matrix; 21 import android.hardware.Camera; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.util.Log; 25 import android.view.SurfaceHolder; 26 import android.view.SurfaceView; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.widget.Button; 30 import android.widget.ImageButton; 31 import android.widget.ImageView; 32 import android.widget.LinearLayout.LayoutParams; 33 import android.widget.TextView; 34 35 import com.android.cts.verifier.PassFailButtons; 36 import com.android.cts.verifier.R; 37 import com.android.cts.verifier.TestResult; 38 39 import java.io.IOException; 40 import java.util.ArrayList; 41 import java.util.Comparator; 42 import java.util.List; 43 import java.util.TreeSet; 44 45 /** 46 * Tests for manual verification of the CDD-required camera output formats 47 * for preview callbacks 48 */ 49 public class CameraOrientationActivity extends PassFailButtons.Activity 50 implements OnClickListener, SurfaceHolder.Callback { 51 52 private static final String TAG = "CameraOrientation"; 53 private static final int STATE_OFF = 0; 54 private static final int STATE_PREVIEW = 1; 55 private static final int STATE_CAPTURE = 2; 56 private static final int NUM_ORIENTATIONS = 4; 57 private static final String STAGE_INDEX_EXTRA = "stageIndex"; 58 59 private ImageButton mPassButton; 60 private ImageButton mFailButton; 61 private Button mTakePictureButton; 62 63 private SurfaceView mCameraView; 64 private ImageView mFormatView; 65 private SurfaceHolder mSurfaceHolder; 66 private Camera mCamera; 67 private List<Camera.Size> mPreviewSizes; 68 private Camera.Size mOptimalSize; 69 private List<Integer> mPreviewOrientations; 70 private int mNextPreviewOrientation; 71 private int mNumCameras; 72 private int mCurrentCameraId = -1; 73 private int mState = STATE_OFF; 74 private boolean mSizeAdjusted; 75 76 private StringBuilder mReportBuilder = new StringBuilder(); 77 private final TreeSet<String> mTestedCombinations = new TreeSet<String>(); 78 private final TreeSet<String> mUntestedCombinations = new TreeSet<String>(); 79 80 @Override onCreate(Bundle savedInstanceState)81 public void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 84 setContentView(R.layout.co_main); 85 setPassFailButtonClickListeners(); 86 setInfoResources(R.string.camera_orientation, R.string.co_info, -1); 87 mNumCameras = Camera.getNumberOfCameras(); 88 89 mPassButton = (ImageButton) findViewById(R.id.pass_button); 90 mFailButton = (ImageButton) findViewById(R.id.fail_button); 91 mTakePictureButton = (Button) findViewById(R.id.take_picture_button); 92 mFormatView = (ImageView) findViewById(R.id.format_view); 93 mCameraView = (SurfaceView) findViewById(R.id.camera_view); 94 95 mFormatView.setOnClickListener(this); 96 mCameraView.setOnClickListener(this); 97 mTakePictureButton.setOnClickListener(this); 98 99 mSurfaceHolder = mCameraView.getHolder(); 100 mSurfaceHolder.addCallback(this); 101 102 mPreviewOrientations = new ArrayList<Integer>(); 103 mPreviewOrientations.add(0); 104 mPreviewOrientations.add(90); 105 mPreviewOrientations.add(180); 106 mPreviewOrientations.add(270); 107 108 // This activity is reused multiple times 109 // to test each camera/orientation combination 110 final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0); 111 Settings settings = getSettings(stageIndex); 112 113 // Hitting the pass button goes to the next test activity. 114 // Only the last one uses the PassFailButtons click callback function, 115 // which gracefully terminates the activity. 116 if (stageIndex + 1 < mNumCameras * NUM_ORIENTATIONS) { 117 setPassButtonGoesToNextStage(stageIndex); 118 } 119 120 String[] availableOrientations = new String[NUM_ORIENTATIONS]; 121 for (int i=0; i<availableOrientations.length; i++) { 122 // append degree symbol 123 availableOrientations[i] = Integer.toString(i * 90) + "\u00b0"; 124 } 125 126 resetButtons(); 127 128 // Set initial values 129 mSizeAdjusted = false; 130 mCurrentCameraId = settings.mCameraId; 131 TextView cameraLabel = (TextView) findViewById(R.id.camera_text); 132 cameraLabel.setText( 133 getString(R.string.co_camera_label) 134 + " " + (mCurrentCameraId+1) + " of " + mNumCameras); 135 136 mNextPreviewOrientation = settings.mOrientation; 137 TextView orientationLabel = 138 (TextView) findViewById(R.id.orientation_text); 139 orientationLabel.setText( 140 getString(R.string.co_orientation_label) 141 + " " 142 + Integer.toString(mNextPreviewOrientation+1) 143 + " of " 144 + Integer.toString(NUM_ORIENTATIONS) 145 + ": " 146 + mPreviewOrientations.get(mNextPreviewOrientation) + "\u00b0" 147 + " " 148 + getString(R.string.co_orientation_direction_label) 149 ); 150 151 TextView instructionLabel = 152 (TextView) findViewById(R.id.instruction_text); 153 instructionLabel.setText(R.string.co_instruction_text_photo_label); 154 155 mTakePictureButton.setEnabled(false); 156 setUpCamera(mCurrentCameraId); 157 } 158 159 @Override onResume()160 public void onResume() { 161 super.onResume(); 162 setUpCamera(mCurrentCameraId); 163 } 164 165 @Override onPause()166 public void onPause() { 167 super.onPause(); 168 shutdownCamera(); 169 } 170 171 @Override getTestDetails()172 public String getTestDetails() { 173 return mReportBuilder.toString(); 174 } 175 setUpCamera(int id)176 private void setUpCamera(int id) { 177 shutdownCamera(); 178 179 Log.v(TAG, "Setting up Camera " + id); 180 mCurrentCameraId = id; 181 182 try { 183 mCamera = Camera.open(id); 184 } catch (Exception e) { 185 Log.e(TAG, "Error opening camera"); 186 } 187 188 Camera.Parameters p = mCamera.getParameters(); 189 190 // Get preview resolutions 191 List<Camera.Size> unsortedSizes = p.getSupportedPreviewSizes(); 192 class SizeCompare implements Comparator<Camera.Size> { 193 @Override 194 public int compare(Camera.Size lhs, Camera.Size rhs) { 195 if (lhs.width < rhs.width) return -1; 196 if (lhs.width > rhs.width) return 1; 197 if (lhs.height < rhs.height) return -1; 198 if (lhs.height > rhs.height) return 1; 199 return 0; 200 } 201 } 202 SizeCompare s = new SizeCompare(); 203 TreeSet<Camera.Size> sortedResolutions = new TreeSet<Camera.Size>(s); 204 sortedResolutions.addAll(unsortedSizes); 205 mPreviewSizes = new ArrayList<Camera.Size>(sortedResolutions); 206 207 startPreview(); 208 } 209 shutdownCamera()210 private void shutdownCamera() { 211 if (mCamera != null) { 212 mCamera.setPreviewCallback(null); 213 mCamera.stopPreview(); 214 mCamera.release(); 215 mCamera = null; 216 mState = STATE_OFF; 217 } 218 } 219 startPreview()220 private void startPreview() { 221 if (mState != STATE_OFF) { 222 // Stop for a while to drain callbacks 223 mCamera.setPreviewCallback(null); 224 mCamera.stopPreview(); 225 mState = STATE_OFF; 226 Handler h = new Handler(); 227 Runnable mDelayedPreview = new Runnable() { 228 @Override 229 public void run() { 230 startPreview(); 231 } 232 }; 233 h.postDelayed(mDelayedPreview, 300); 234 return; 235 } 236 237 mCamera.setPreviewCallback(mPreviewCallback); 238 239 try { 240 mCamera.setPreviewDisplay(mCameraView.getHolder()); 241 } catch (IOException ioe) { 242 Log.e(TAG, "Unable to connect camera to display"); 243 } 244 245 Camera.Parameters p = mCamera.getParameters(); 246 Log.v(TAG, "Initializing picture format"); 247 p.setPictureFormat(ImageFormat.JPEG); 248 mOptimalSize = getOptimalPreviewSize(mPreviewSizes, 640, 480); 249 Log.v(TAG, "Initializing picture size to " 250 + mOptimalSize.width + "x" + mOptimalSize.height); 251 p.setPictureSize(mOptimalSize.width, mOptimalSize.height); 252 Log.v(TAG, "Initializing preview size to " 253 + mOptimalSize.width + "x" + mOptimalSize.height); 254 p.setPreviewSize(mOptimalSize.width, mOptimalSize.height); 255 256 Log.v(TAG, "Setting camera parameters"); 257 mCamera.setParameters(p); 258 Log.v(TAG, "Setting color filter"); 259 mFormatView.setColorFilter(null); 260 Log.v(TAG, "Starting preview"); 261 try { 262 mCamera.startPreview(); 263 } catch (Exception e) { 264 Log.d(TAG, "Cannot start preview", e); 265 } 266 267 // set preview orientation 268 int degrees = mPreviewOrientations.get(mNextPreviewOrientation); 269 mCamera.setDisplayOrientation(degrees); 270 271 android.hardware.Camera.CameraInfo info = 272 new android.hardware.Camera.CameraInfo(); 273 android.hardware.Camera.getCameraInfo(mCurrentCameraId, info); 274 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 275 TextView cameraExtraLabel = 276 (TextView) findViewById(R.id.instruction_extra_text); 277 cameraExtraLabel.setText( 278 getString(R.string.co_instruction_text_extra_label)); 279 } 280 281 mState = STATE_PREVIEW; 282 } 283 284 @Override onClick(View view)285 public void onClick(View view) { 286 Log.v(TAG, "Click detected"); 287 288 if (view == mFormatView || view == mTakePictureButton) { 289 if(mState == STATE_PREVIEW) { 290 mTakePictureButton.setEnabled(false); 291 Log.v(TAG, "Taking picture"); 292 mCamera.takePicture(null, null, null, mCameraCallback); 293 mState = STATE_CAPTURE; 294 } 295 } 296 297 if(view == mPassButton || view == mFailButton) { 298 final int stageIndex = 299 getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0); 300 String[] cameraNames = new String[mNumCameras]; 301 int counter = 0; 302 for (int i = 0; i < mNumCameras; i++) { 303 cameraNames[i] = "Camera " + i; 304 305 for(int j = 0; j < mPreviewOrientations.size(); j++) { 306 String combination = cameraNames[i] + ", " 307 + mPreviewOrientations.get(j) 308 + "\u00b0" 309 + "\n"; 310 311 if(counter < stageIndex) { 312 // test already passed, or else wouldn't have made 313 // it to current stageIndex 314 mTestedCombinations.add(combination); 315 } 316 317 if(counter == stageIndex) { 318 // current test configuration 319 if(view == mPassButton) { 320 mTestedCombinations.add(combination); 321 } 322 else if(view == mFailButton) { 323 mUntestedCombinations.add(combination); 324 } 325 } 326 327 if(counter > stageIndex) { 328 // test not passed yet, since haven't made it to 329 // stageIndex 330 mUntestedCombinations.add(combination); 331 } 332 333 counter++; 334 } 335 } 336 337 mReportBuilder = new StringBuilder(); 338 mReportBuilder.append("Passed combinations:\n"); 339 for (String combination : mTestedCombinations) { 340 mReportBuilder.append(combination); 341 } 342 mReportBuilder.append("Failed/untested combinations:\n"); 343 for (String combination : mUntestedCombinations) { 344 mReportBuilder.append(combination); 345 } 346 347 if(view == mPassButton) { 348 TestResult.setPassedResult(this, "CameraOrientationActivity", 349 getTestDetails()); 350 } 351 if(view == mFailButton) { 352 TestResult.setFailedResult(this, "CameraOrientationActivity", 353 getTestDetails()); 354 } 355 356 // restart activity to test next orientation 357 Intent intent = new Intent(CameraOrientationActivity.this, 358 CameraOrientationActivity.class); 359 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP 360 | Intent.FLAG_ACTIVITY_FORWARD_RESULT); 361 intent.putExtra(STAGE_INDEX_EXTRA, stageIndex + 1); 362 startActivity(intent); 363 } 364 } 365 resetButtons()366 private void resetButtons() { 367 enablePassFailButtons(false); 368 } 369 enablePassFailButtons(boolean enable)370 private void enablePassFailButtons(boolean enable) { 371 mPassButton.setEnabled(enable); 372 mFailButton.setEnabled(enable); 373 } 374 375 // find a supported size with ratio less than tolerance threshold, and 376 // which is closest to height and width of given dimensions without 377 // being larger than either of given dimensions getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h)378 private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, 379 int h) { 380 final double ASPECT_TOLERANCE = 0.1; 381 double targetRatio = (double) 640 / (double) 480; 382 if (sizes == null) return null; 383 384 Camera.Size optimalSize = null; 385 int minDiff = Integer.MAX_VALUE; 386 int curDiff; 387 388 int targetHeight = h; 389 int targetWidth = w; 390 391 boolean aspectRatio = true; 392 boolean maintainCeiling = true; 393 while(true) { 394 for (Camera.Size size : sizes) { 395 if(aspectRatio) { 396 double ratio = (double) size.width / size.height; 397 if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) { 398 continue; 399 } 400 } 401 curDiff = Math.abs(size.height - targetHeight) + 402 Math.abs(size.width - targetWidth); 403 if (maintainCeiling && curDiff < minDiff 404 && size.height <= targetHeight 405 && size.width <= targetWidth) { 406 optimalSize = size; 407 minDiff = curDiff; 408 } else if (maintainCeiling == false 409 && curDiff < minDiff) { 410 //try to get as close as possible 411 optimalSize = size; 412 minDiff = curDiff; 413 } 414 } 415 if (optimalSize == null && aspectRatio == true) { 416 // Cannot find a match, so repeat search and 417 // ignore aspect ratio requirement 418 aspectRatio = false; 419 } else if (maintainCeiling == true) { 420 //Camera resolutions are greater than ceiling provided 421 //lets try to get as close as we can 422 maintainCeiling = false; 423 } else { 424 break; 425 } 426 } 427 428 return optimalSize; 429 } 430 431 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)432 public void surfaceChanged(SurfaceHolder holder, int format, int width, 433 int height) { 434 startPreview(); 435 } 436 setTestedConfiguration(int cameraId, int orientation)437 private void setTestedConfiguration(int cameraId, int orientation) { 438 String combination = "Camera " + cameraId + ", " 439 + orientation 440 + "\u00b0" 441 + "\n"; 442 if (!mTestedCombinations.contains(combination)) { 443 mTestedCombinations.add(combination); 444 mUntestedCombinations.remove(combination); 445 } 446 } 447 448 @Override surfaceCreated(SurfaceHolder holder)449 public void surfaceCreated(SurfaceHolder holder) { 450 // Auto-generated method stub 451 } 452 453 @Override surfaceDestroyed(SurfaceHolder holder)454 public void surfaceDestroyed(SurfaceHolder holder) { 455 // Auto-generated method stub 456 } 457 458 private final Camera.PreviewCallback mPreviewCallback = 459 new Camera.PreviewCallback() { 460 @Override 461 public void onPreviewFrame(byte[] data, Camera camera) { 462 // adjust camera preview to match output image's aspect ratio 463 if(!mSizeAdjusted && mState == STATE_PREVIEW) { 464 int viewWidth = mFormatView.getWidth(); 465 int viewHeight = mFormatView.getHeight(); 466 int newWidth, newHeight; 467 468 if (mPreviewOrientations.get(mNextPreviewOrientation) == 0 469 || mPreviewOrientations.get(mNextPreviewOrientation) == 180) { 470 // make preview width same as output image width, 471 // then calculate height using output image's height/width ratio 472 newWidth = viewWidth; 473 newHeight = (int) (viewWidth * ((double) mOptimalSize.height / 474 (double) mOptimalSize.width)); 475 } 476 else { 477 newHeight = viewHeight; 478 newWidth = (int) (viewHeight * ((double) mOptimalSize.height / 479 (double) mOptimalSize.width)); 480 } 481 482 LayoutParams layoutParams = new LayoutParams(newWidth, newHeight); 483 mCameraView.setLayoutParams(layoutParams); 484 mSizeAdjusted = true; 485 mTakePictureButton.setEnabled(true); 486 } 487 } 488 }; 489 490 private final Camera.PictureCallback mCameraCallback = 491 new Camera.PictureCallback() { 492 @Override 493 public void onPictureTaken(byte[] data, Camera mCamera) { 494 if (data != null) { 495 Bitmap inputImage; 496 inputImage = BitmapFactory.decodeByteArray(data, 0, data.length); 497 498 int degrees = mPreviewOrientations.get(mNextPreviewOrientation); 499 android.hardware.Camera.CameraInfo info = 500 new android.hardware.Camera.CameraInfo(); 501 android.hardware.Camera.getCameraInfo(mCurrentCameraId, info); 502 float mirrorX[]; 503 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 504 // mirror the image along vertical axis 505 mirrorX = new float[] {-1, 0, 0, 0, 1, 1, 0, 0, 1}; 506 degrees = (360 - degrees) % 360; // compensate the mirror 507 } else { 508 // leave image the same via identity matrix 509 mirrorX = new float[] {1, 0, 0, 0, 1, 0, 0, 0, 1}; 510 } 511 512 // use matrix to transform the image 513 Matrix matrixMirrorX = new Matrix(); 514 matrixMirrorX.setValues(mirrorX); 515 Matrix mat = new Matrix(); 516 mat.postRotate(degrees); 517 mat.postConcat(matrixMirrorX); 518 519 Bitmap inputImageAdjusted = Bitmap.createBitmap(inputImage, 520 0, 521 0, 522 inputImage.getWidth(), 523 inputImage.getHeight(), 524 mat, 525 true); 526 mFormatView.setImageBitmap(inputImageAdjusted); 527 528 Log.v(TAG, "Output image set"); 529 enablePassFailButtons(true); 530 531 TextView instructionLabel = 532 (TextView) findViewById(R.id.instruction_text); 533 instructionLabel.setText( 534 R.string.co_instruction_text_passfail_label); 535 } 536 537 startPreview(); 538 } 539 }; 540 setPassButtonGoesToNextStage(final int stageIndex)541 private void setPassButtonGoesToNextStage(final int stageIndex) { 542 findViewById(R.id.pass_button).setOnClickListener(this); 543 } 544 getSettings(int stageIndex)545 private Settings getSettings(int stageIndex) { 546 int curCameraId = stageIndex / NUM_ORIENTATIONS; 547 int curOrientation = stageIndex % NUM_ORIENTATIONS; 548 return new Settings(stageIndex, curCameraId, curOrientation); 549 } 550 551 // Bundle of settings for testing a particular 552 // camera/orientation combination 553 class Settings { 554 int mCameraId; 555 int mOrientation; 556 Settings(int stageIndex, int cameraId, int orientation)557 Settings(int stageIndex, int cameraId, int orientation) { 558 mCameraId = cameraId; 559 mOrientation = orientation; 560 } 561 } 562 } 563