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