/* * 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.mediaframeworktest.integration; import android.graphics.ImageFormat; import android.graphics.SurfaceTexture; import android.hardware.ICameraService; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.CaptureResultExtras; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.utils.SubmitInfo; import android.media.Image; import android.media.ImageReader; import android.os.Handler; import android.os.HandlerThread; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.os.SystemClock; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import android.view.Surface; import static android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW; import com.android.mediaframeworktest.MediaFrameworkIntegrationTestRunner; import org.mockito.ArgumentCaptor; import org.mockito.compat.ArgumentMatcher; import static org.mockito.Mockito.*; public class CameraDeviceBinderTest extends AndroidTestCase { private static String TAG = "CameraDeviceBinderTest"; // Number of streaming callbacks need to check. private static int NUM_CALLBACKS_CHECKED = 10; // Wait for capture result timeout value: 1500ms private final static int WAIT_FOR_COMPLETE_TIMEOUT_MS = 1500; // Wait for flush timeout value: 1000ms private final static int WAIT_FOR_FLUSH_TIMEOUT_MS = 1000; // Wait for idle timeout value: 2000ms private final static int WAIT_FOR_IDLE_TIMEOUT_MS = 2000; // Wait while camera device starts working on requests private final static int WAIT_FOR_WORK_MS = 300; // Default size is VGA, which is mandatory camera supported image size by CDD. private static final int DEFAULT_IMAGE_WIDTH = 640; private static final int DEFAULT_IMAGE_HEIGHT = 480; private static final int MAX_NUM_IMAGES = 5; private String mCameraId; private ICameraDeviceUser mCameraUser; private CameraBinderTestUtils mUtils; private ICameraDeviceCallbacks.Stub mMockCb; private Surface mSurface; private OutputConfiguration mOutputConfiguration; private HandlerThread mHandlerThread; private Handler mHandler; ImageReader mImageReader; public CameraDeviceBinderTest() { } private class ImageDropperListener implements ImageReader.OnImageAvailableListener { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireNextImage(); if (image != null) image.close(); } } public class DummyCameraDeviceCallbacks extends ICameraDeviceCallbacks.Stub { /* * (non-Javadoc) * @see * android.hardware.camera2.ICameraDeviceCallbacks#onDeviceError(int, * android.hardware.camera2.CaptureResultExtras) */ public void onDeviceError(int errorCode, CaptureResultExtras resultExtras) throws RemoteException { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see android.hardware.camera2.ICameraDeviceCallbacks#onDeviceIdle() */ public void onDeviceIdle() throws RemoteException { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see * android.hardware.camera2.ICameraDeviceCallbacks#onCaptureStarted( * android.hardware.camera2.CaptureResultExtras, long) */ public void onCaptureStarted(CaptureResultExtras resultExtras, long timestamp) throws RemoteException { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see * android.hardware.camera2.ICameraDeviceCallbacks#onResultReceived( * android.hardware.camera2.impl.CameraMetadataNative, * android.hardware.camera2.CaptureResultExtras) */ public void onResultReceived(CameraMetadataNative result, CaptureResultExtras resultExtras) throws RemoteException { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see android.hardware.camera2.ICameraDeviceCallbacks#onPrepared() */ @Override public void onPrepared(int streamId) throws RemoteException { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see android.hardware.camera2.ICameraDeviceCallbacks#onRequestQueueEmpty() */ @Override public void onRequestQueueEmpty() throws RemoteException { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see android.hardware.camera2.ICameraDeviceCallbacks#onRepeatingRequestError() */ @Override public void onRepeatingRequestError(long lastFrameNumber) { // TODO Auto-generated method stub } } class IsMetadataNotEmpty extends ArgumentMatcher { @Override public boolean matchesObject(Object obj) { return !((CameraMetadataNative) obj).isEmpty(); } } private void createDefaultSurface() { mImageReader = ImageReader.newInstance(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, ImageFormat.YUV_420_888, MAX_NUM_IMAGES); mImageReader.setOnImageAvailableListener(new ImageDropperListener(), mHandler); mSurface = mImageReader.getSurface(); mOutputConfiguration = new OutputConfiguration(mSurface); } private CaptureRequest.Builder createDefaultBuilder(boolean needStream) throws Exception { CameraMetadataNative metadata = null; assertTrue(metadata.isEmpty()); metadata = mCameraUser.createDefaultRequest(TEMPLATE_PREVIEW); assertFalse(metadata.isEmpty()); CaptureRequest.Builder request = new CaptureRequest.Builder(metadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE); assertFalse(request.isEmpty()); assertFalse(metadata.isEmpty()); if (needStream) { int streamId = mCameraUser.createStream(mOutputConfiguration); assertEquals(0, streamId); request.addTarget(mSurface); } return request; } private SubmitInfo submitCameraRequest(CaptureRequest request, boolean streaming) throws Exception { SubmitInfo requestInfo = mCameraUser.submitRequest(request, streaming); assertTrue( "Request IDs should be non-negative (expected: >= 0, actual: " + requestInfo.getRequestId() + ")", requestInfo.getRequestId() >= 0); return requestInfo; } @Override protected void setUp() throws Exception { super.setUp(); /** * Workaround for mockito and JB-MR2 incompatibility * * Avoid java.lang.IllegalArgumentException: dexcache == null * https://code.google.com/p/dexmaker/issues/detail?id=2 */ System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString()); mUtils = new CameraBinderTestUtils(getContext()); // This cannot be set in the constructor, since the onCreate isn't // called yet mCameraId = MediaFrameworkIntegrationTestRunner.mCameraId; ICameraDeviceCallbacks.Stub dummyCallbacks = new DummyCameraDeviceCallbacks(); String clientPackageName = getContext().getPackageName(); mMockCb = spy(dummyCallbacks); mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId, clientPackageName, ICameraService.USE_CALLING_UID); assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser); mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); createDefaultSurface(); Log.v(TAG, String.format("Camera %s connected", mCameraId)); } @Override protected void tearDown() throws Exception { mCameraUser.disconnect(); mCameraUser = null; mSurface.release(); mImageReader.close(); mHandlerThread.quitSafely(); } @SmallTest public void testCreateDefaultRequest() throws Exception { CameraMetadataNative metadata = null; assertTrue(metadata.isEmpty()); metadata = mCameraUser.createDefaultRequest(TEMPLATE_PREVIEW); assertFalse(metadata.isEmpty()); } @SmallTest public void testCreateStream() throws Exception { int streamId = mCameraUser.createStream(mOutputConfiguration); assertEquals(0, streamId); try { mCameraUser.createStream(mOutputConfiguration); fail("Creating same stream twice"); } catch (ServiceSpecificException e) { assertEquals("Creating same stream twice", e.errorCode, ICameraService.ERROR_ALREADY_EXISTS); } mCameraUser.deleteStream(streamId); } @SmallTest public void testDeleteInvalidStream() throws Exception { int[] badStreams = { -1, 0, 1, 0xC0FFEE }; for (int badStream : badStreams) { try { mCameraUser.deleteStream(badStream); fail("Allowed bad stream delete"); } catch (ServiceSpecificException e) { assertEquals(e.errorCode, ICameraService.ERROR_ILLEGAL_ARGUMENT); } } } @SmallTest public void testCreateStreamTwo() throws Exception { // Create first stream int streamId = mCameraUser.createStream(mOutputConfiguration); assertEquals(0, streamId); try { mCameraUser.createStream(mOutputConfiguration); fail("Created same stream twice"); } catch (ServiceSpecificException e) { assertEquals("Created same stream twice", ICameraService.ERROR_ALREADY_EXISTS, e.errorCode); } // Create second stream with a different surface. SurfaceTexture surfaceTexture = new SurfaceTexture(/* ignored */0); surfaceTexture.setDefaultBufferSize(640, 480); Surface surface2 = new Surface(surfaceTexture); OutputConfiguration output2 = new OutputConfiguration(surface2); int streamId2 = mCameraUser.createStream(output2); assertEquals(1, streamId2); // Clean up streams mCameraUser.deleteStream(streamId); mCameraUser.deleteStream(streamId2); } @SmallTest public void testSubmitBadRequest() throws Exception { CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */false); CaptureRequest request1 = builder.build(); try { SubmitInfo requestInfo = mCameraUser.submitRequest(request1, /* streaming */false); fail("Exception expected"); } catch(ServiceSpecificException e) { assertEquals("Expected submitRequest to throw ServiceSpecificException with BAD_VALUE " + "since we had 0 surface targets set.", ICameraService.ERROR_ILLEGAL_ARGUMENT, e.errorCode); } builder.addTarget(mSurface); CaptureRequest request2 = builder.build(); try { SubmitInfo requestInfo = mCameraUser.submitRequest(request2, /* streaming */false); fail("Exception expected"); } catch(ServiceSpecificException e) { assertEquals("Expected submitRequest to throw ILLEGAL_ARGUMENT " + "ServiceSpecificException since the target wasn't registered with createStream.", ICameraService.ERROR_ILLEGAL_ARGUMENT, e.errorCode); } } @SmallTest public void testSubmitGoodRequest() throws Exception { CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true); CaptureRequest request = builder.build(); // Submit valid request twice. SubmitInfo requestInfo1 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo2 = submitCameraRequest(request, /* streaming */false); assertNotSame("Request IDs should be unique for multiple requests", requestInfo1.getRequestId(), requestInfo2.getRequestId()); } @SmallTest public void testSubmitStreamingRequest() throws Exception { CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true); CaptureRequest request = builder.build(); // Submit valid request once (non-streaming), and another time // (streaming) SubmitInfo requestInfo1 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfoStreaming = submitCameraRequest(request, /* streaming */true); assertNotSame("Request IDs should be unique for multiple requests", requestInfo1.getRequestId(), requestInfoStreaming.getRequestId()); try { long lastFrameNumber = mCameraUser.cancelRequest(-1); fail("Expected exception"); } catch (ServiceSpecificException e) { assertEquals("Invalid request IDs should not be cancellable", ICameraService.ERROR_ILLEGAL_ARGUMENT, e.errorCode); } try { long lastFrameNumber = mCameraUser.cancelRequest(requestInfo1.getRequestId()); fail("Expected exception"); } catch (ServiceSpecificException e) { assertEquals("Non-streaming request IDs should not be cancellable", ICameraService.ERROR_ILLEGAL_ARGUMENT, e.errorCode); } long lastFrameNumber = mCameraUser.cancelRequest(requestInfoStreaming.getRequestId()); } @SmallTest public void testCameraInfo() throws RemoteException { CameraMetadataNative info = mCameraUser.getCameraInfo(); assertFalse(info.isEmpty()); assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS)); } @SmallTest public void testCameraCharacteristics() throws RemoteException { CameraMetadataNative info = mUtils.getCameraService().getCameraCharacteristics(mCameraId); assertFalse(info.isEmpty()); assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS)); } @SmallTest public void testWaitUntilIdle() throws Exception { CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true); SubmitInfo requestInfoStreaming = submitCameraRequest(builder.build(), /* streaming */true); // Test Bad case first: waitUntilIdle when there is active repeating request try { mCameraUser.waitUntilIdle(); } catch (ServiceSpecificException e) { assertEquals("waitUntilIdle is invalid operation when there is active repeating request", ICameraService.ERROR_INVALID_OPERATION, e.errorCode); } // Test good case, waitUntilIdle when there is no active repeating request long lastFrameNumber = mCameraUser.cancelRequest(requestInfoStreaming.getRequestId()); mCameraUser.waitUntilIdle(); } @SmallTest public void testCaptureResultCallbacks() throws Exception { IsMetadataNotEmpty matcher = new IsMetadataNotEmpty(); CaptureRequest request = createDefaultBuilder(/* needStream */true).build(); // Test both single request and streaming request. verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).times(1)).onResultReceived( argThat(matcher), any(CaptureResultExtras.class)); verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).atLeast(NUM_CALLBACKS_CHECKED)) .onResultReceived( argThat(matcher), any(CaptureResultExtras.class)); } @SmallTest public void testCaptureStartedCallbacks() throws Exception { CaptureRequest request = createDefaultBuilder(/* needStream */true).build(); ArgumentCaptor timestamps = ArgumentCaptor.forClass(Long.class); // Test both single request and streaming request. SubmitInfo requestInfo1 = submitCameraRequest(request, /* streaming */false); verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).times(1)).onCaptureStarted( any(CaptureResultExtras.class), anyLong()); SubmitInfo streamingInfo = submitCameraRequest(request, /* streaming */true); verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).atLeast(NUM_CALLBACKS_CHECKED)) .onCaptureStarted( any(CaptureResultExtras.class), timestamps.capture()); long timestamp = 0; // All timestamps should be larger than 0. for (Long nextTimestamp : timestamps.getAllValues()) { Log.v(TAG, "next t: " + nextTimestamp + " current t: " + timestamp); assertTrue("Captures are out of order", timestamp < nextTimestamp); timestamp = nextTimestamp; } } @SmallTest public void testIdleCallback() throws Exception { int status; CaptureRequest request = createDefaultBuilder(/* needStream */true).build(); // Try streaming SubmitInfo streamingInfo = submitCameraRequest(request, /* streaming */true); // Wait a bit to fill up the queue SystemClock.sleep(WAIT_FOR_WORK_MS); // Cancel and make sure we eventually quiesce long lastFrameNumber = mCameraUser.cancelRequest(streamingInfo.getRequestId()); verify(mMockCb, timeout(WAIT_FOR_IDLE_TIMEOUT_MS).times(1)).onDeviceIdle(); // Submit a few capture requests SubmitInfo requestInfo1 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo2 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo3 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo4 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo5 = submitCameraRequest(request, /* streaming */false); // And wait for more idle verify(mMockCb, timeout(WAIT_FOR_IDLE_TIMEOUT_MS).times(2)).onDeviceIdle(); } @SmallTest public void testFlush() throws Exception { int status; // Initial flush should work long lastFrameNumber = mCameraUser.flush(); // Then set up a stream CaptureRequest request = createDefaultBuilder(/* needStream */true).build(); // Flush should still be a no-op, really lastFrameNumber = mCameraUser.flush(); // Submit a few capture requests SubmitInfo requestInfo1 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo2 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo3 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo4 = submitCameraRequest(request, /* streaming */false); SubmitInfo requestInfo5 = submitCameraRequest(request, /* streaming */false); // Then flush and wait for idle lastFrameNumber = mCameraUser.flush(); verify(mMockCb, timeout(WAIT_FOR_FLUSH_TIMEOUT_MS).times(1)).onDeviceIdle(); // Now a streaming request SubmitInfo streamingInfo = submitCameraRequest(request, /* streaming */true); // Wait a bit to fill up the queue SystemClock.sleep(WAIT_FOR_WORK_MS); // Then flush and wait for the idle callback lastFrameNumber = mCameraUser.flush(); verify(mMockCb, timeout(WAIT_FOR_FLUSH_TIMEOUT_MS).times(2)).onDeviceIdle(); // TODO: When errors are hooked up, count that errors + successful // requests equal to 5. } }