/* * Copyright (C) 2013 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.fov; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.hardware.Camera.PictureCallback; import android.hardware.Camera.ShutterCallback; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.os.Bundle; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.View.OnClickListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.android.cts.verifier.R; import com.android.cts.verifier.TestResult; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * An activity for showing the camera preview and taking a picture. */ public class PhotoCaptureActivity extends Activity implements PictureCallback, TextureView.SurfaceTextureListener { private static final String TAG = PhotoCaptureActivity.class.getSimpleName(); private static final int FOV_REQUEST_CODE = 1006; private static final String PICTURE_FILENAME = "photo.jpg"; private static float mReportedFovDegrees = 0; private float mReportedFovPrePictureTaken = -1; private TextureView mPreviewView; private SurfaceTexture mPreviewTexture; private int mPreviewTexWidth; private int mPreviewTexHeight; private Spinner mResolutionSpinner; private List mSupportedResolutions; private ArrayAdapter mAdapter; private SelectableResolution mSelectedResolution; private Camera mCamera; private boolean mCameraInitialized = false; private boolean mPreviewActive = false; private boolean mTakingPicture = false; private int mResolutionSpinnerIndex = -1; private WakeLock mWakeLock; private long shutterStartTime; private int mPreviewOrientation; private int mJpegOrientation; private ArrayList mPreviewSizeCamerasToProcess = new ArrayList(); private Dialog mActiveDialog; /** * Selected preview size per camera. If null, preview size should be * automatically detected. */ private Size[] mPreviewSizes = null; public static File getPictureFile(Context context) { return new File(context.getExternalCacheDir(), PICTURE_FILENAME); } public static float getReportedFovDegrees() { return mReportedFovDegrees; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.camera_fov_calibration_photo_capture); int cameraToBeTested = 0; for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); ++cameraId) { if (!isExternalCamera(cameraId)) { cameraToBeTested++; } } mPreviewView = (TextureView) findViewById(R.id.camera_fov_camera_preview); mPreviewView.setSurfaceTextureListener(this); TextView textView = (TextView) findViewById(R.id.camera_fov_tap_to_take_photo); textView.setTextColor(Color.WHITE); Button setupButton = (Button) findViewById(R.id.camera_fov_settings_button); setupButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent( PhotoCaptureActivity.this, CalibrationPreferenceActivity.class)); } }); Button changePreviewSizeButton = (Button) findViewById( R.id.camera_fov_change_preview_size_button); changePreviewSizeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Stop camera until preview sizes have been obtained. if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } mPreviewSizeCamerasToProcess.clear(); mPreviewSizes = new Size[Camera.getNumberOfCameras()]; for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); ++cameraId) { if (!isExternalCamera(cameraId)) { mPreviewSizeCamerasToProcess.add(cameraId); } } showNextDialogToChoosePreviewSize(); } }); View previewView = findViewById(R.id.camera_fov_preview_overlay); previewView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mPreviewActive && !mTakingPicture) { mTakingPicture = true; shutterStartTime = System.currentTimeMillis(); mCamera.takePicture(new ShutterCallback() { @Override public void onShutter() { long dT = System.currentTimeMillis() - shutterStartTime; Log.d("CTS", "Shutter Lag: " + dT); } }, null, PhotoCaptureActivity.this); } } }); mResolutionSpinner = (Spinner) findViewById(R.id.camera_fov_resolution_selector); mResolutionSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected( AdapterView parent, View view, int position, long id) { if (mSupportedResolutions != null) { SelectableResolution resolution = mSupportedResolutions.get(position); switchToCamera(resolution, false); // It should be guaranteed that the FOV is correctly updated after // setParameters(). mReportedFovPrePictureTaken = getCameraFov(resolution.cameraId); mResolutionSpinnerIndex = position; startPreview(); } } @Override public void onNothingSelected(AdapterView arg0) {} }); if (cameraToBeTested == 0) { Log.i(TAG, "No cameras needs to be tested. Setting test pass."); Toast.makeText(this, "No cameras needs to be tested. Test pass.", Toast.LENGTH_LONG).show(); TestResult.setPassedResult(this, getClass().getName(), "All cameras are external, test skipped!"); finish(); } } @Override protected void onResume() { super.onResume(); // Keep the device from going to sleep. PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); mWakeLock.acquire(); if (mSupportedResolutions == null) { mSupportedResolutions = new ArrayList(); int numCameras = Camera.getNumberOfCameras(); for (int cameraId = 0; cameraId < numCameras; ++cameraId) { if (isExternalCamera(cameraId)) { continue; } Camera camera = Camera.open(cameraId); // Get the supported picture sizes and fill the spinner. List supportedSizes = camera.getParameters().getSupportedPictureSizes(); for (Camera.Size size : supportedSizes) { mSupportedResolutions.add( new SelectableResolution(cameraId, size.width, size.height)); } camera.release(); } } // Find the first untested entry. for (mResolutionSpinnerIndex = 0; mResolutionSpinnerIndex < mSupportedResolutions.size(); mResolutionSpinnerIndex++) { if (!mSupportedResolutions.get(mResolutionSpinnerIndex).tested) { break; } } mAdapter = new ArrayAdapter( this, android.R.layout.simple_spinner_dropdown_item, mSupportedResolutions); mResolutionSpinner.setAdapter(mAdapter); mResolutionSpinner.setSelection(mResolutionSpinnerIndex); setResult(RESULT_CANCELED); } @Override public void onPause() { if (mCamera != null) { if (mPreviewActive) { mCamera.stopPreview(); } mCamera.release(); mCamera = null; } mPreviewActive = false; mWakeLock.release(); super.onPause(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); this.recreate(); } @Override public void onPictureTaken(byte[] data, Camera camera) { File pictureFile = getPictureFile(this); mReportedFovDegrees = getCameraFov(mSelectedResolution.cameraId); // Show error if FOV does not match the value reported before takePicture(). if (mReportedFovPrePictureTaken != mReportedFovDegrees) { mSupportedResolutions.get(mResolutionSpinnerIndex).tested = true; mSupportedResolutions.get(mResolutionSpinnerIndex).passed = false; AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); dialogBuilder.setTitle(R.string.camera_fov_reported_fov_problem); dialogBuilder.setNeutralButton( android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (mActiveDialog != null) { mActiveDialog.dismiss(); mActiveDialog = null; initializeCamera(); } } }); String message = getResources().getString(R.string.camera_fov_reported_fov_problem_message); dialogBuilder.setMessage(String.format(message, mReportedFovPrePictureTaken, mReportedFovDegrees)); mActiveDialog = dialogBuilder.show(); mTakingPicture = false; return; } try { FileOutputStream fos = new FileOutputStream(pictureFile); fos.write(data); fos.close(); Log.d(TAG, "File saved to " + pictureFile.getAbsolutePath()); // Start activity which will use the taken picture to determine the // FOV. startActivityForResult(new Intent(this, DetermineFovActivity.class), FOV_REQUEST_CODE + mResolutionSpinnerIndex, null); } catch (IOException e) { Log.e(TAG, "Could not save picture file.", e); Toast.makeText(this, "Could not save picture file: " + e.getMessage(), Toast.LENGTH_LONG).show(); } mTakingPicture = false; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != RESULT_OK) { return; } int testIndex = requestCode - FOV_REQUEST_CODE; SelectableResolution res = mSupportedResolutions.get(testIndex); res.tested = true; float reportedFOV = CtsTestHelper.getReportedFOV(data); float measuredFOV = CtsTestHelper.getMeasuredFOV(data); res.measuredFOV = measuredFOV; if (CtsTestHelper.isResultPassed(reportedFOV, measuredFOV)) { res.passed = true; } boolean allTested = true; for (int i = 0; i < mSupportedResolutions.size(); i++) { if (!mSupportedResolutions.get(i).tested) { allTested = false; break; } } if (!allTested) { mAdapter.notifyDataSetChanged(); return; } boolean allPassed = true; for (int i = 0; i < mSupportedResolutions.size(); i++) { if (!mSupportedResolutions.get(i).passed) { allPassed = false; break; } } if (allPassed) { TestResult.setPassedResult(this, getClass().getName(), CtsTestHelper.getTestDetails(mSupportedResolutions)); } else { TestResult.setFailedResult(this, getClass().getName(), CtsTestHelper.getTestDetails(mSupportedResolutions)); } finish(); } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mPreviewTexture = surface; mPreviewTexWidth = width; mPreviewTexHeight = height; initializeCamera(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // Ignored, Camera does all the work for us } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { // Invoked every time there's a new Camera preview frame } private void showNextDialogToChoosePreviewSize() { final int cameraId = mPreviewSizeCamerasToProcess.remove(0); Camera camera = Camera.open(cameraId); final List sizes = camera.getParameters() .getSupportedPreviewSizes(); String[] choices = new String[sizes.size()]; for (int i = 0; i < sizes.size(); ++i) { Camera.Size size = sizes.get(i); choices[i] = size.width + " x " + size.height; } final AlertDialog.Builder builder = new AlertDialog.Builder(this); String dialogTitle = String.format( getResources().getString(R.string.camera_fov_choose_preview_size_for_camera), cameraId); builder.setTitle( dialogTitle). setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface arg0) { // User cancelled preview size selection. mPreviewSizes = null; switchToCamera(mSelectedResolution, true); } }). setSingleChoiceItems(choices, 0, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Camera.Size size = sizes.get(which); mPreviewSizes[cameraId] = new Size( size.width, size.height); dialog.dismiss(); if (mPreviewSizeCamerasToProcess.isEmpty()) { // We're done, re-initialize camera. switchToCamera(mSelectedResolution, true); } else { // Process other cameras. showNextDialogToChoosePreviewSize(); } } }).create().show(); camera.release(); } private void initializeCamera() { initializeCamera(true); } private void setPreviewTransform(Size previewSize) { int sensorRotation = mPreviewOrientation; float selectedPreviewAspectRatio; if (sensorRotation == 0 || sensorRotation == 180) { selectedPreviewAspectRatio = (float) previewSize.width / (float) previewSize.height; } else { selectedPreviewAspectRatio = (float) previewSize.height / (float) previewSize.width; } Matrix transform = new Matrix(); float viewAspectRatio = (float) mPreviewView.getMeasuredWidth() / (float) mPreviewView.getMeasuredHeight(); float scaleX = 1.0f, scaleY = 1.0f; float translateX = 0, translateY = 0; if (selectedPreviewAspectRatio > viewAspectRatio) { scaleY = viewAspectRatio / selectedPreviewAspectRatio; translateY = (float) mPreviewView.getMeasuredHeight() / 2 - (float) mPreviewView.getMeasuredHeight() * scaleY / 2; } else { scaleX = selectedPreviewAspectRatio / viewAspectRatio; translateX = (float) mPreviewView.getMeasuredWidth() / 2 - (float) mPreviewView.getMeasuredWidth() * scaleX / 2; } transform.postScale(scaleX, scaleY); transform.postTranslate(translateX, translateY); mPreviewView.setTransform(transform); } private void initializeCamera(boolean startPreviewAfterInit) { if (mCamera == null || mPreviewTexture == null) { return; } try { mCamera.setPreviewTexture(mPreviewTexture); } catch (Throwable t) { Log.e(TAG, "Could not set preview texture", t); Toast.makeText(this, t.getMessage(), Toast.LENGTH_LONG).show(); return; } calculateOrientations(this, mSelectedResolution.cameraId, mCamera); Camera.Parameters params = setCameraParams(mCamera); // Either use chosen preview size for current camera or automatically // choose preview size based on view dimensions. Size selectedPreviewSize = null; if (mPreviewSizes != null) { selectedPreviewSize = mPreviewSizes[mSelectedResolution.cameraId]; } else { if (mPreviewOrientation == 0 || mPreviewOrientation == 180) { selectedPreviewSize = getBestPreviewSize( mPreviewTexWidth, mPreviewTexHeight, params); } else { selectedPreviewSize = getBestPreviewSize( mPreviewTexHeight, mPreviewTexWidth, params); } } if (selectedPreviewSize != null) { params.setPreviewSize(selectedPreviewSize.width, selectedPreviewSize.height); mCamera.setParameters(params); setPreviewTransform(selectedPreviewSize); mCameraInitialized = true; } if (startPreviewAfterInit) { if (selectedPreviewSize == null) { Log.w(TAG, "Preview started without setting preview size"); } startPreview(); } } private void startPreview() { if (mCameraInitialized && mCamera != null) { mCamera.setDisplayOrientation(mPreviewOrientation); mCamera.startPreview(); mPreviewActive = true; } } private void switchToCamera(SelectableResolution resolution, boolean startPreview) { if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); } mSelectedResolution = resolution; mCamera = Camera.open(mSelectedResolution.cameraId); initializeCamera(startPreview); } /** * Get the best supported focus mode. * * @param camera - Android camera object. * @return the best supported focus mode. */ private static String getFocusMode(Camera camera) { List modes = camera.getParameters().getSupportedFocusModes(); if (modes != null) { if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) { Log.v(TAG, "Using Focus mode infinity"); return Camera.Parameters.FOCUS_MODE_INFINITY; } if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) { Log.v(TAG, "Using Focus mode fixed"); return Camera.Parameters.FOCUS_MODE_FIXED; } } Log.v(TAG, "Using Focus mode auto."); return Camera.Parameters.FOCUS_MODE_AUTO; } /** * Set the common camera parameters on the given camera and returns the * parameter object for further modification, if needed. */ private Camera.Parameters setCameraParams(Camera camera) { // The picture size is taken and set from the spinner selection // callback. Camera.Parameters params = camera.getParameters(); params.setJpegThumbnailSize(0, 0); params.setJpegQuality(100); params.setRotation(mJpegOrientation); params.setFocusMode(getFocusMode(camera)); params.setZoom(0); params.setPictureSize(mSelectedResolution.width, mSelectedResolution.height); return params; } private Size getBestPreviewSize( int width, int height, Camera.Parameters parameters) { Size result = null; for (Camera.Size size : parameters.getSupportedPreviewSizes()) { if (size.width <= width && size.height <= height) { if (result == null) { result = new Size(size.width, size.height); } else { int resultArea = result.width * result.height; int newArea = size.width * size.height; if (newArea > resultArea) { result = new Size(size.width, size.height); } } } } return result; } private int getDisplayRotation() { int displayRotation = getDisplay().getRotation(); int displayRotationDegrees = 0; switch (displayRotation) { case Surface.ROTATION_0: displayRotationDegrees = 0; break; case Surface.ROTATION_90: displayRotationDegrees = 90; break; case Surface.ROTATION_180: displayRotationDegrees = 180; break; case Surface.ROTATION_270: displayRotationDegrees = 270; break; } return displayRotationDegrees; } private void calculateOrientations(Activity activity, int cameraId, android.hardware.Camera camera) { android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); android.hardware.Camera.getCameraInfo(cameraId, info); int degrees = getDisplayRotation(); if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { mJpegOrientation = (info.orientation + degrees) % 360; mPreviewOrientation = (360 - mJpegOrientation) % 360; // compensate the mirror } else { // back-facing mJpegOrientation = (info.orientation - degrees + 360) % 360; mPreviewOrientation = mJpegOrientation; } } private boolean isExternalCamera(int cameraId) { CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); try { String cameraIdStr = manager.getCameraIdList()[cameraId]; CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraIdStr); if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) { // External camera doesn't support FOV informations return true; } } catch (CameraAccessException e) { Toast.makeText(this, "Could not access camera " + cameraId + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); } return false; } private float getCameraFov(int cameraId) { if (mPreviewOrientation == 0 || mPreviewOrientation == 180) { return mCamera.getParameters().getHorizontalViewAngle(); } else { return mCamera.getParameters().getVerticalViewAngle(); } } }