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