blob: b90f213226728f260d5c4a428548df4f65b65c91 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.cts.verifier.camera.orientation;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import com.android.cts.verifier.TestResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
/**
* Tests for manual verification of the CDD-required camera output formats
* for preview callbacks
*/
public class CameraOrientationActivity extends PassFailButtons.Activity
implements OnClickListener, SurfaceHolder.Callback {
private static final String TAG = "CameraOrientation";
private static final int STATE_OFF = 0;
private static final int STATE_PREVIEW = 1;
private static final int NUM_ORIENTATIONS = 4;
private static final String STAGE_INDEX_EXTRA = "stageIndex";
private Button mPassButton;
private Button mFailButton;
private Button mTakePictureButton;
private SurfaceView mCameraView;
private ImageView mFormatView;
private SurfaceHolder mSurfaceHolder;
private Camera mCamera;
private List<Camera.Size> mPreviewSizes;
private Camera.Size mOptimalSize;
private List<Integer> mPreviewOrientations;
private int mNextPreviewOrientation;
private int mNumCameras;
private int mCurrentCameraId = -1;
private int mState = STATE_OFF;
private StringBuilder mReportBuilder = new StringBuilder();
private final TreeSet<String> mTestedCombinations = new TreeSet<String>();
private final TreeSet<String> mUntestedCombinations = new TreeSet<String>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.co_main);
setPassFailButtonClickListeners();
setInfoResources(R.string.camera_orientation, R.string.co_info, -1);
mNumCameras = Camera.getNumberOfCameras();
mPassButton = (Button) findViewById(R.id.pass_button);
mFailButton = (Button) findViewById(R.id.fail_button);
mTakePictureButton = (Button) findViewById(R.id.take_picture_button);
mFormatView = (ImageView) findViewById(R.id.format_view);
mCameraView = (SurfaceView) findViewById(R.id.camera_view);
mFormatView.setOnClickListener(this);
mCameraView.setOnClickListener(this);
mTakePictureButton.setOnClickListener(this);
mSurfaceHolder = mCameraView.getHolder();
mSurfaceHolder.addCallback(this);
mPreviewOrientations = new ArrayList<Integer>();
mPreviewOrientations.add(0);
mPreviewOrientations.add(90);
mPreviewOrientations.add(180);
mPreviewOrientations.add(270);
// This activity is reused multiple times
// to test each camera/orientation combination
final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0);
Settings settings = getSettings(stageIndex);
// Hitting the pass button goes to the next test activity.
// Only the last one uses the PassFailButtons click callback function,
// which gracefully terminates the activity.
if (stageIndex + 1 < mNumCameras * NUM_ORIENTATIONS) {
setPassButtonGoesToNextStage(stageIndex);
}
String[] availableOrientations = new String[NUM_ORIENTATIONS];
for (int i=0; i<availableOrientations.length; i++) {
// append degree symbol
availableOrientations[i] = Integer.toString(i * 90) + "\u00b0";
}
resetButtons();
// Set initial values
mCurrentCameraId = settings.mCameraId;
TextView cameraLabel = (TextView) findViewById(R.id.camera_text);
cameraLabel.setText(
getString(R.string.co_camera_label)
+ " " + (mCurrentCameraId+1) + " of " + mNumCameras);
mNextPreviewOrientation = settings.mOrientation;
TextView orientationLabel =
(TextView) findViewById(R.id.orientation_text);
orientationLabel.setText(
getString(R.string.co_orientation_label)
+ " "
+ Integer.toString(mNextPreviewOrientation+1)
+ " of "
+ Integer.toString(NUM_ORIENTATIONS)
+ ": "
+ mPreviewOrientations.get(mNextPreviewOrientation) + "\u00b0"
+ " "
+ getString(R.string.co_orientation_direction_label)
);
TextView instructionLabel =
(TextView) findViewById(R.id.instruction_text);
instructionLabel.setText(R.string.co_instruction_text_photo_label);
mTakePictureButton.setEnabled(true);
setUpCamera(mCurrentCameraId);
}
@Override
public void onResume() {
super.onResume();
setUpCamera(mCurrentCameraId);
}
@Override
public void onPause() {
super.onPause();
shutdownCamera();
}
@Override
public String getTestDetails() {
return mReportBuilder.toString();
}
private void setUpCamera(int id) {
shutdownCamera();
Log.v(TAG, "Setting up Camera " + id);
mCurrentCameraId = id;
try {
mCamera = Camera.open(id);
} catch (Exception e) {
Log.e(TAG, "Error opening camera");
}
Camera.Parameters p = mCamera.getParameters();
// Get preview resolutions
List<Camera.Size> unsortedSizes = p.getSupportedPreviewSizes();
class SizeCompare implements Comparator<Camera.Size> {
@Override
public int compare(Camera.Size lhs, Camera.Size rhs) {
if (lhs.width < rhs.width) return -1;
if (lhs.width > rhs.width) return 1;
if (lhs.height < rhs.height) return -1;
if (lhs.height > rhs.height) return 1;
return 0;
}
}
SizeCompare s = new SizeCompare();
TreeSet<Camera.Size> sortedResolutions = new TreeSet<Camera.Size>(s);
sortedResolutions.addAll(unsortedSizes);
mPreviewSizes = new ArrayList<Camera.Size>(sortedResolutions);
startPreview();
}
private void shutdownCamera() {
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
mState = STATE_OFF;
}
}
private void startPreview() {
if (mState != STATE_OFF) {
// Stop for a while to drain callbacks
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mState = STATE_OFF;
Handler h = new Handler();
Runnable mDelayedPreview = new Runnable() {
@Override
public void run() {
startPreview();
}
};
h.postDelayed(mDelayedPreview, 300);
return;
}
mState = STATE_PREVIEW;
mCamera.setPreviewCallback(mPreviewCallback);
try {
mCamera.setPreviewDisplay(mCameraView.getHolder());
} catch (IOException ioe) {
Log.e(TAG, "Unable to connect camera to display");
}
Camera.Parameters p = mCamera.getParameters();
Log.v(TAG, "Initializing picture format");
p.setPictureFormat(ImageFormat.JPEG);
mOptimalSize = getOptimalPreviewSize(mPreviewSizes, 640, 480);
Log.v(TAG, "Initializing picture size to "
+ mOptimalSize.width + "x" + mOptimalSize.height);
p.setPictureSize(mOptimalSize.width, mOptimalSize.height);
Log.v(TAG, "Initializing preview size to "
+ mOptimalSize.width + "x" + mOptimalSize.height);
p.setPreviewSize(mOptimalSize.width, mOptimalSize.height);
Log.v(TAG, "Setting camera parameters");
mCamera.setParameters(p);
Log.v(TAG, "Setting color filter");
mFormatView.setColorFilter(null);
Log.v(TAG, "Starting preview");
try {
mCamera.startPreview();
} catch (Exception e) {
Log.d(TAG, "Cannot start preview", e);
}
// set preview orientation
int degrees = mPreviewOrientations.get(mNextPreviewOrientation);
mCamera.setDisplayOrientation(degrees);
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(mCurrentCameraId, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
TextView cameraExtraLabel =
(TextView) findViewById(R.id.instruction_extra_text);
cameraExtraLabel.setText(
getString(R.string.co_instruction_text_extra_label));
}
}
@Override
public void onClick(View view) {
Log.v(TAG, "Click detected");
if (view == mFormatView || view == mTakePictureButton) {
Log.v(TAG, "Taking picture");
mCamera.takePicture(null, null, null, mCameraCallback);
}
if(view == mPassButton || view == mFailButton) {
final int stageIndex =
getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0);
String[] cameraNames = new String[mNumCameras];
int counter = 0;
for (int i = 0; i < mNumCameras; i++) {
cameraNames[i] = "Camera " + i;
for(int j = 0; j < mPreviewOrientations.size(); j++) {
String combination = cameraNames[i] + ", "
+ mPreviewOrientations.get(j)
+ "\u00b0"
+ "\n";
if(counter < stageIndex) {
// test already passed, or else wouldn't have made
// it to current stageIndex
mTestedCombinations.add(combination);
}
if(counter == stageIndex) {
// current test configuration
if(view == mPassButton) {
mTestedCombinations.add(combination);
}
else if(view == mFailButton) {
mUntestedCombinations.add(combination);
}
}
if(counter > stageIndex) {
// test not passed yet, since haven't made it to
// stageIndex
mUntestedCombinations.add(combination);
}
counter++;
}
}
mReportBuilder = new StringBuilder();
mReportBuilder.append("Passed combinations:\n");
for (String combination : mTestedCombinations) {
mReportBuilder.append(combination);
}
mReportBuilder.append("Failed/untested combinations:\n");
for (String combination : mUntestedCombinations) {
mReportBuilder.append(combination);
}
if(view == mPassButton) {
TestResult.setPassedResult(this, "CameraOrientationActivity",
getTestDetails());
}
if(view == mFailButton) {
TestResult.setFailedResult(this, "CameraOrientationActivity",
getTestDetails());
}
// restart activity to test next orientation
Intent intent = new Intent(CameraOrientationActivity.this,
CameraOrientationActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_FORWARD_RESULT);
intent.putExtra(STAGE_INDEX_EXTRA, stageIndex + 1);
startActivity(intent);
}
}
private void resetButtons() {
enablePassFailButtons(false);
}
private void enablePassFailButtons(boolean enable) {
mPassButton.setEnabled(enable);
mFailButton.setEnabled(enable);
}
// find a supported size with ratio less than tolerance threshold, and
// which is closest to height and width of given dimensions without
// being larger than either of given dimensions
private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w,
int h) {
final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) 640 / (double) 480;
if (sizes == null) return null;
Camera.Size optimalSize = null;
int minDiff = Integer.MAX_VALUE;
int curDiff;
int targetHeight = h;
int targetWidth = w;
boolean aspectRatio = true;
while(true) {
for (Camera.Size size : sizes) {
if(aspectRatio) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) {
continue;
}
}
curDiff = Math.abs(size.height - targetHeight) +
Math.abs(size.width - targetWidth);
if (curDiff < minDiff
&& size.height <= targetHeight
&& size.width <= targetWidth) {
optimalSize = size;
minDiff = curDiff;
}
}
if (optimalSize == null) {
// Cannot find a match, so repeat search and
// ignore aspect ratio requirement
aspectRatio = false;
continue;
}
else {
break;
}
}
return optimalSize;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
startPreview();
}
private void setTestedConfiguration(int cameraId, int orientation) {
String combination = "Camera " + cameraId + ", "
+ orientation
+ "\u00b0"
+ "\n";
if (!mTestedCombinations.contains(combination)) {
mTestedCombinations.add(combination);
mUntestedCombinations.remove(combination);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// Auto-generated method stub
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Auto-generated method stub
}
private final Camera.PreviewCallback mPreviewCallback =
new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
// adjust camera preview to match output image's aspect ratio
int viewWidth = mFormatView.getWidth();
int viewHeight = mFormatView.getHeight();
int newWidth, newHeight;
if (mPreviewOrientations.get(mNextPreviewOrientation) == 0
|| mPreviewOrientations.get(mNextPreviewOrientation) == 180) {
// make preview width same as output image width,
// then calculate height using output image's height/width ratio
newWidth = viewWidth;
newHeight = (int) (viewWidth * ((double) mOptimalSize.height /
(double) mOptimalSize.width));
}
else {
newHeight = viewHeight;
newWidth = (int) (viewHeight * ((double) mOptimalSize.height /
(double) mOptimalSize.width));
}
LayoutParams layoutParams = new LayoutParams(newWidth, newHeight);
mCameraView.setLayoutParams(layoutParams);
}
};
private final Camera.PictureCallback mCameraCallback =
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera mCamera) {
if (data != null) {
Bitmap inputImage;
inputImage = BitmapFactory.decodeByteArray(data, 0, data.length);
int degrees = mPreviewOrientations.get(mNextPreviewOrientation);
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(mCurrentCameraId, info);
float mirrorX[];
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
// mirror the image along vertical axis
mirrorX = new float[] {-1, 0, 0, 0, 1, 1, 0, 0, 1};
degrees = (360 - degrees) % 360; // compensate the mirror
} else {
// leave image the same via identity matrix
mirrorX = new float[] {1, 0, 0, 0, 1, 0, 0, 0, 1};
}
// use matrix to transform the image
Matrix matrixMirrorX = new Matrix();
matrixMirrorX.setValues(mirrorX);
Matrix mat = new Matrix();
mat.postRotate(degrees);
mat.postConcat(matrixMirrorX);
Bitmap inputImageAdjusted = Bitmap.createBitmap(inputImage,
0,
0,
inputImage.getWidth(),
inputImage.getHeight(),
mat,
true);
mFormatView.setImageBitmap(inputImageAdjusted);
Log.v(TAG, "Output image set");
enablePassFailButtons(true);
TextView instructionLabel =
(TextView) findViewById(R.id.instruction_text);
instructionLabel.setText(
R.string.co_instruction_text_passfail_label);
}
startPreview();
}
};
private void setPassButtonGoesToNextStage(final int stageIndex) {
findViewById(R.id.pass_button).setOnClickListener(this);
}
private Settings getSettings(int stageIndex) {
int curCameraId = stageIndex / NUM_ORIENTATIONS;
int curOrientation = stageIndex % NUM_ORIENTATIONS;
return new Settings(stageIndex, curCameraId, curOrientation);
}
// Bundle of settings for testing a particular
// camera/orientation combination
class Settings {
int mCameraId;
int mOrientation;
Settings(int stageIndex, int cameraId, int orientation) {
mCameraId = cameraId;
mOrientation = orientation;
}
}
}