| /* |
| * Copyright (C) 2014 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 android.hardware.camera2.cts.testcases; |
| |
| import static android.hardware.camera2.cts.CameraTestUtils.*; |
| import static com.android.ex.camera2.blocking.BlockingSessionCallback.*; |
| import static com.android.ex.camera2.blocking.BlockingStateCallback.*; |
| |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.graphics.Matrix; |
| import android.graphics.RectF; |
| import android.graphics.SurfaceTexture; |
| import android.hardware.camera2.CameraCaptureSession; |
| import android.hardware.camera2.CameraCaptureSession.CaptureCallback; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.camera2.CaptureRequest; |
| import android.hardware.camera2.cts.Camera2MultiViewCtsActivity; |
| import android.hardware.camera2.cts.helpers.StaticMetadata; |
| import android.hardware.camera2.cts.helpers.StaticMetadata.CheckLevel; |
| import android.os.ConditionVariable; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.SystemClock; |
| import android.test.ActivityInstrumentationTestCase2; |
| import android.util.Log; |
| import android.util.Size; |
| import android.view.Surface; |
| import android.view.TextureView; |
| import android.view.WindowManager; |
| |
| import com.android.ex.camera2.blocking.BlockingCameraManager; |
| import com.android.ex.camera2.blocking.BlockingSessionCallback; |
| import com.android.ex.camera2.blocking.BlockingStateCallback; |
| |
| import junit.framework.Assert; |
| |
| import java.util.List; |
| import java.util.HashMap; |
| |
| /** |
| * Camera2 test case base class by using mixed SurfaceView and TextureView as rendering target. |
| */ |
| public class Camera2MultiViewTestCase extends |
| ActivityInstrumentationTestCase2<Camera2MultiViewCtsActivity> { |
| private static final String TAG = "MultiViewTestCase"; |
| private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); |
| |
| |
| private static final long SHORT_SLEEP_WAIT_TIME_MS = 100; |
| |
| protected TextureView[] mTextureView = new TextureView[2]; |
| protected String[] mCameraIds; |
| protected Handler mHandler; |
| |
| private CameraManager mCameraManager; |
| private BlockingStateCallback mCameraListener; |
| private HandlerThread mHandlerThread; |
| private Context mContext; |
| |
| private CameraHolder[] mCameraHolders; |
| private HashMap<String, Integer> mCameraIdMap; |
| |
| protected WindowManager mWindowManager; |
| |
| public Camera2MultiViewTestCase() { |
| super(Camera2MultiViewCtsActivity.class); |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| mContext = getActivity(); |
| assertNotNull("Unable to get activity", mContext); |
| mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); |
| assertNotNull("Unable to get CameraManager", mCameraManager); |
| mCameraIds = mCameraManager.getCameraIdList(); |
| assertNotNull("Unable to get camera ids", mCameraIds); |
| mHandlerThread = new HandlerThread(TAG); |
| mHandlerThread.start(); |
| mHandler = new Handler(mHandlerThread.getLooper()); |
| mCameraListener = new BlockingStateCallback(); |
| Camera2MultiViewCtsActivity activity = (Camera2MultiViewCtsActivity) mContext; |
| mTextureView[0] = activity.getTextureView(0); |
| mTextureView[1] = activity.getTextureView(1); |
| assertNotNull("Unable to get texture view", mTextureView); |
| mCameraIdMap = new HashMap<String, Integer>(); |
| int numCameras = mCameraIds.length; |
| mCameraHolders = new CameraHolder[numCameras]; |
| for (int i = 0; i < numCameras; i++) { |
| mCameraHolders[i] = new CameraHolder(mCameraIds[i]); |
| mCameraIdMap.put(mCameraIds[i], i); |
| } |
| mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| mHandlerThread.quitSafely(); |
| mHandler = null; |
| mCameraListener = null; |
| for (CameraHolder camera : mCameraHolders) { |
| if (camera.isOpenned()) { |
| camera.close(); |
| camera = null; |
| } |
| } |
| super.tearDown(); |
| } |
| |
| /** |
| * Update preview TextureView rotation to accommodate discrepancy between preview |
| * buffer and the view window orientation. |
| * |
| * Assumptions: |
| * - Aspect ratio for the sensor buffers is in landscape orientation, |
| * - Dimensions of buffers received are rotated to the natural device orientation. |
| * - The contents of each buffer are rotated by the inverse of the display rotation. |
| * - Surface scales the buffer to fit the current view bounds. |
| * TODO: Make this method works for all orientations |
| * |
| */ |
| protected void updatePreviewDisplayRotation(Size previewSize, TextureView textureView) { |
| int rotationDegrees = 0; |
| Camera2MultiViewCtsActivity activity = (Camera2MultiViewCtsActivity) mContext; |
| int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); |
| Configuration config = activity.getResources().getConfiguration(); |
| |
| // Get UI display rotation |
| switch (displayRotation) { |
| case Surface.ROTATION_0: |
| rotationDegrees = 0; |
| break; |
| case Surface.ROTATION_90: |
| rotationDegrees = 90; |
| break; |
| case Surface.ROTATION_180: |
| rotationDegrees = 180; |
| break; |
| case Surface.ROTATION_270: |
| rotationDegrees = 270; |
| break; |
| } |
| |
| // Get device natural orientation |
| int deviceOrientation = Configuration.ORIENTATION_PORTRAIT; |
| if ((rotationDegrees % 180 == 0 && |
| config.orientation == Configuration.ORIENTATION_LANDSCAPE) || |
| ((rotationDegrees % 180 != 0 && |
| config.orientation == Configuration.ORIENTATION_PORTRAIT))) { |
| deviceOrientation = Configuration.ORIENTATION_LANDSCAPE; |
| } |
| |
| // Rotate the buffer dimensions if device orientation is portrait. |
| int effectiveWidth = previewSize.getWidth(); |
| int effectiveHeight = previewSize.getHeight(); |
| if (deviceOrientation == Configuration.ORIENTATION_PORTRAIT) { |
| effectiveWidth = previewSize.getHeight(); |
| effectiveHeight = previewSize.getWidth(); |
| } |
| |
| // Find and center view rect and buffer rect |
| Matrix transformMatrix = textureView.getTransform(null); |
| int viewWidth = textureView.getWidth(); |
| int viewHeight = textureView.getHeight(); |
| RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); |
| RectF bufRect = new RectF(0, 0, effectiveWidth, effectiveHeight); |
| float centerX = viewRect.centerX(); |
| float centerY = viewRect.centerY(); |
| bufRect.offset(centerX - bufRect.centerX(), centerY - bufRect.centerY()); |
| |
| // Undo ScaleToFit.FILL done by the surface |
| transformMatrix.setRectToRect(viewRect, bufRect, Matrix.ScaleToFit.FILL); |
| |
| // Rotate buffer contents to proper orientation |
| transformMatrix.postRotate((360 - rotationDegrees) % 360, centerX, centerY); |
| if ((rotationDegrees % 180) == 90) { |
| int temp = effectiveWidth; |
| effectiveWidth = effectiveHeight; |
| effectiveHeight = temp; |
| } |
| |
| // Scale to fit view, cropping the longest dimension |
| float scale = |
| Math.max(viewWidth / (float) effectiveWidth, viewHeight / (float) effectiveHeight); |
| transformMatrix.postScale(scale, scale, centerX, centerY); |
| |
| Handler handler = new Handler(Looper.getMainLooper()); |
| class TransformUpdater implements Runnable { |
| TextureView mView; |
| Matrix mTransformMatrix; |
| TransformUpdater(TextureView view, Matrix matrix) { |
| mView = view; |
| mTransformMatrix = matrix; |
| } |
| |
| @Override |
| public void run() { |
| mView.setTransform(mTransformMatrix); |
| } |
| } |
| handler.post(new TransformUpdater(textureView, transformMatrix)); |
| } |
| |
| protected void openCamera(String cameraId) throws Exception { |
| CameraHolder camera = getCameraHolder(cameraId); |
| assertFalse("Camera has already opened", camera.isOpenned()); |
| camera.open(); |
| return; |
| } |
| |
| protected void closeCamera(String cameraId) throws Exception { |
| CameraHolder camera = getCameraHolder(cameraId); |
| camera.close(); |
| } |
| |
| protected void startPreview( |
| String cameraId, List<Surface> outputSurfaces, CaptureCallback listener) |
| throws Exception { |
| CameraHolder camera = getCameraHolder(cameraId); |
| assertTrue("Camera " + cameraId + " is not openned", camera.isOpenned()); |
| camera.startPreview(outputSurfaces, listener); |
| } |
| |
| protected void stopPreview(String cameraId) throws Exception { |
| CameraHolder camera = getCameraHolder(cameraId); |
| assertTrue("Camera " + cameraId + " preview is not running", camera.isPreviewStarted()); |
| camera.stopPreview(); |
| } |
| |
| protected StaticMetadata getStaticInfo(String cameraId) { |
| CameraHolder camera = getCameraHolder(cameraId); |
| assertTrue("Camera is not openned", camera.isOpenned()); |
| return camera.getStaticInfo(); |
| } |
| |
| protected List<Size> getOrderedPreviewSizes(String cameraId) { |
| CameraHolder camera = getCameraHolder(cameraId); |
| assertTrue("Camera is not openned", camera.isOpenned()); |
| return camera.getOrderedPreviewSizes(); |
| } |
| |
| /** |
| * Wait until the SurfaceTexture available from the TextureView, then return it. |
| * Return null if the wait times out. |
| * |
| * @param timeOutMs The timeout value for the wait |
| * @return The available SurfaceTexture, return null if the wait times out. |
| */ |
| protected SurfaceTexture getAvailableSurfaceTexture(long timeOutMs, TextureView view) { |
| long waitTime = timeOutMs; |
| |
| while (!view.isAvailable() && waitTime > 0) { |
| long startTimeMs = SystemClock.elapsedRealtime(); |
| SystemClock.sleep(SHORT_SLEEP_WAIT_TIME_MS); |
| waitTime -= (SystemClock.elapsedRealtime() - startTimeMs); |
| } |
| |
| if (view.isAvailable()) { |
| return view.getSurfaceTexture(); |
| } else { |
| Log.w(TAG, "Wait for SurfaceTexture available timed out after " + timeOutMs + "ms"); |
| return null; |
| } |
| } |
| |
| public static class CameraPreviewListener implements TextureView.SurfaceTextureListener { |
| private boolean mFirstPreviewAvailable = false; |
| private final ConditionVariable mPreviewDone = new ConditionVariable(); |
| |
| @Override |
| public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { |
| // Ignored. The SurfaceTexture is polled by getAvailableSurfaceTexture. |
| } |
| |
| @Override |
| public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { |
| // Ignored. The CameraDevice should already know the changed size. |
| } |
| |
| @Override |
| public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { |
| /** |
| * Return true, assume that client detaches the surface before it is |
| * destroyed. For example, CameraDevice should detach this surface when |
| * stopping preview. No need to release the SurfaceTexture here as it |
| * is released by TextureView after onSurfaceTextureDestroyed is called. |
| */ |
| Log.i(TAG, "onSurfaceTextureDestroyed called."); |
| return true; |
| } |
| |
| @Override |
| public void onSurfaceTextureUpdated(SurfaceTexture surface) { |
| // Invoked every time there's a new Camera preview frame |
| if (!mFirstPreviewAvailable) { |
| mFirstPreviewAvailable = true; |
| mPreviewDone.open(); |
| } |
| } |
| |
| /** Waits until the camera preview is up running */ |
| public boolean waitForPreviewDone(long timeOutMs) { |
| if (!mPreviewDone.block(timeOutMs)) { |
| // timeout could be expected or unexpected. The caller will decide. |
| Log.w(TAG, "waitForPreviewDone timed out after " + timeOutMs + "ms"); |
| return false; |
| } |
| mPreviewDone.close(); |
| return true; |
| } |
| } |
| |
| private CameraHolder getCameraHolder(String cameraId) { |
| Integer cameraIdx = mCameraIdMap.get(cameraId); |
| if (cameraIdx == null) { |
| Assert.fail("Unknown camera Id"); |
| } |
| return mCameraHolders[cameraIdx]; |
| } |
| |
| // Per device fields |
| private class CameraHolder { |
| private String mCameraId; |
| private CameraCaptureSession mSession; |
| private CameraDevice mCamera; |
| private StaticMetadata mStaticInfo; |
| private List<Size> mOrderedPreviewSizes; |
| private BlockingSessionCallback mSessionListener; |
| |
| public CameraHolder(String id){ |
| mCameraId = id; |
| } |
| |
| public StaticMetadata getStaticInfo() { |
| return mStaticInfo; |
| } |
| |
| public List<Size> getOrderedPreviewSizes() { |
| return mOrderedPreviewSizes; |
| } |
| |
| public void open() throws Exception { |
| assertNull("Camera is already opened", mCamera); |
| mCamera = (new BlockingCameraManager(mCameraManager)).openCamera( |
| mCameraId, mCameraListener, mHandler); |
| mStaticInfo = new StaticMetadata(mCameraManager.getCameraCharacteristics(mCameraId), |
| CheckLevel.ASSERT, /*collector*/null); |
| mOrderedPreviewSizes = getSupportedPreviewSizes( |
| mCameraId, mCameraManager, |
| getPreviewSizeBound(mWindowManager, PREVIEW_SIZE_BOUND)); |
| assertNotNull(String.format("Failed to open camera device ID: %s", mCameraId), mCamera); |
| } |
| |
| public boolean isOpenned() { |
| return (mCamera != null); |
| } |
| |
| public void close() throws Exception { |
| if (!isOpenned()) { |
| return; |
| } |
| mCamera.close(); |
| mCameraListener.waitForState(STATE_CLOSED, CAMERA_CLOSE_TIMEOUT_MS); |
| mCamera = null; |
| mSession = null; |
| mStaticInfo = null; |
| mOrderedPreviewSizes = null; |
| } |
| |
| public void startPreview(List<Surface> outputSurfaces, CaptureCallback listener) |
| throws Exception { |
| mSessionListener = new BlockingSessionCallback(); |
| mSession = configureCameraSession(mCamera, outputSurfaces, mSessionListener, mHandler); |
| |
| // TODO: vary the different settings like crop region to cover more cases. |
| CaptureRequest.Builder captureBuilder = |
| mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); |
| |
| for (Surface surface : outputSurfaces) { |
| captureBuilder.addTarget(surface); |
| } |
| mSession.setRepeatingRequest(captureBuilder.build(), listener, mHandler); |
| } |
| |
| public boolean isPreviewStarted() { |
| return (mSession != null); |
| } |
| |
| public void stopPreview() throws Exception { |
| if (VERBOSE) Log.v(TAG, |
| "Stopping camera " + mCameraId +" preview and waiting for idle"); |
| // Stop repeat, wait for captures to complete, and disconnect from surfaces |
| mSession.close(); |
| mSessionListener.getStateWaiter().waitForState( |
| SESSION_CLOSED, SESSION_CLOSE_TIMEOUT_MS); |
| mSessionListener = null; |
| } |
| } |
| } |
| |