| /* |
| * Copyright 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 android.hardware.camera2.cts; |
| |
| import static junit.framework.Assert.*; |
| |
| import static org.mockito.Mockito.*; |
| |
| import android.app.Instrumentation; |
| import android.app.NotificationManager; |
| import android.app.UiAutomation; |
| import android.content.pm.PackageManager; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraCharacteristics; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraDevice.StateCallback; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.camera2.cts.Camera2ParameterizedTestCase; |
| import android.hardware.camera2.cts.CameraTestUtils.HandlerExecutor; |
| import android.hardware.camera2.cts.CameraTestUtils.MockStateCallback; |
| import android.hardware.camera2.cts.helpers.CameraErrorCollector; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.ParcelFileDescriptor; |
| import android.platform.test.annotations.AppModeFull; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import androidx.test.InstrumentationRegistry; |
| |
| import com.android.compatibility.common.util.PropertyUtil; |
| import com.android.ex.camera2.blocking.BlockingStateCallback; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.InOrder; |
| |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.LinkedBlockingQueue; |
| |
| /** |
| * <p>Basic test for CameraManager class.</p> |
| */ |
| |
| @RunWith(Parameterized.class) |
| public class CameraManagerTest extends Camera2ParameterizedTestCase { |
| private static final String TAG = "CameraManagerTest"; |
| private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); |
| private static final int NUM_CAMERA_REOPENS = 10; |
| private static final int AVAILABILITY_TIMEOUT_MS = 10; |
| |
| private PackageManager mPackageManager; |
| private NoopCameraListener mListener; |
| private HandlerThread mHandlerThread; |
| private Handler mHandler; |
| private BlockingStateCallback mCameraListener; |
| private CameraErrorCollector mCollector; |
| private Set<Set<String>> mConcurrentCameraIdCombinations; |
| |
| /** Load validation jni on initialization. */ |
| static { |
| System.loadLibrary("ctscamera2_jni"); |
| } |
| |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| mPackageManager = mContext.getPackageManager(); |
| assertNotNull("Can't get package manager", mPackageManager); |
| mListener = new NoopCameraListener(); |
| |
| /** |
| * 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", mContext.getCacheDir().toString()); |
| |
| mCameraListener = spy(new BlockingStateCallback()); |
| |
| mHandlerThread = new HandlerThread(TAG); |
| mHandlerThread.start(); |
| mHandler = new Handler(mHandlerThread.getLooper()); |
| mCollector = new CameraErrorCollector(); |
| mConcurrentCameraIdCombinations = |
| CameraTestUtils.getConcurrentCameraIds(mCameraManager, mAdoptShellPerm); |
| } |
| |
| @Override |
| public void tearDown() throws Exception { |
| mHandlerThread.quitSafely(); |
| mHandler = null; |
| |
| try { |
| mCollector.verify(); |
| } catch (Throwable e) { |
| // When new Exception(e) is used, exception info will be printed twice. |
| throw new Exception(e.getMessage()); |
| } finally { |
| super.tearDown(); |
| } |
| } |
| |
| /** |
| * Verifies that the reason is in the range of public-only codes. |
| */ |
| private static int checkCameraAccessExceptionReason(CameraAccessException e) { |
| int reason = e.getReason(); |
| |
| switch (reason) { |
| case CameraAccessException.CAMERA_DISABLED: |
| case CameraAccessException.CAMERA_DISCONNECTED: |
| case CameraAccessException.CAMERA_ERROR: |
| case CameraAccessException.CAMERA_IN_USE: |
| case CameraAccessException.MAX_CAMERAS_IN_USE: |
| return reason; |
| } |
| |
| fail("Invalid CameraAccessException code: " + reason); |
| |
| return -1; // unreachable |
| } |
| |
| @Test |
| public void testCameraManagerGetDeviceIdList() throws Exception { |
| String[] ids = mCameraIdsUnderTest; |
| if (VERBOSE) Log.v(TAG, "CameraManager ids: " + Arrays.toString(ids)); |
| |
| if (mAdoptShellPerm) { |
| Log.v(TAG, "Camera related features may not be accurate for system cameras, skipping"); |
| return; |
| } |
| |
| /** |
| * Test: that if there is at least one reported id, then the system must have |
| * the FEATURE_CAMERA_ANY feature. |
| */ |
| assertTrue("System camera feature and camera id list don't match", |
| ids.length == 0 || |
| mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)); |
| |
| /** |
| * Test: that if the device has front or rear facing cameras, then there |
| * must be matched system features. |
| */ |
| boolean externalCameraConnected = false; |
| String mainBackId = null, mainFrontId = null; |
| Map<String, Integer> lensFacingMap = new HashMap<String, Integer>(); |
| for (int i = 0; i < ids.length; i++) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(ids[i]); |
| assertNotNull("Can't get camera characteristics for camera " + ids[i], props); |
| Integer lensFacing = props.get(CameraCharacteristics.LENS_FACING); |
| lensFacingMap.put(ids[i], lensFacing); |
| assertNotNull("Can't get lens facing info", lensFacing); |
| if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { |
| assertTrue("System doesn't have front camera feature", |
| mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)); |
| if (mainFrontId == null) { |
| mainFrontId = ids[i]; |
| } |
| } else if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { |
| assertTrue("System doesn't have back camera feature", |
| mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)); |
| if (mainBackId == null) { |
| mainBackId = ids[i]; |
| } |
| } else if (lensFacing == CameraCharacteristics.LENS_FACING_EXTERNAL) { |
| externalCameraConnected = true; |
| assertTrue("System doesn't have external camera feature", |
| mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_EXTERNAL)); |
| } else { |
| fail("Unknown camera lens facing " + lensFacing.toString()); |
| } |
| } |
| |
| // Test an external camera is connected if FEATURE_CAMERA_EXTERNAL is advertised |
| if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_EXTERNAL)) { |
| assertTrue("External camera is not connected on device with FEATURE_CAMERA_EXTERNAL", |
| externalCameraConnected); |
| } |
| |
| /** |
| * Test: that if there is one camera device, then the system must have some |
| * specific features. |
| */ |
| assertTrue("Missing system feature: FEATURE_CAMERA_ANY", |
| ids.length == 0 |
| || mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)); |
| assertTrue("Missing system feature: FEATURE_CAMERA, FEATURE_CAMERA_FRONT or FEATURE_CAMERA_EXTERNAL", |
| ids.length == 0 |
| || mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) |
| || mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT) |
| || mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_EXTERNAL)); |
| |
| testConcurrentCameraFeature(mainFrontId, mainBackId); |
| } |
| |
| /** |
| * Returns true if mConcurrentCameraIdCombinations has at least one combination containing both |
| * mainFrontId and mainBackId. |
| * Returns false otherwise. |
| */ |
| private boolean containsMainFrontBackConcurrentCombination(String mainFrontId, |
| String mainBackId) { |
| if (mainFrontId == null || mainBackId == null) { |
| return false; |
| } |
| boolean combinationFound = false; |
| |
| // Go through all combinations and see that at least one combination has a main |
| // front + main back camera. |
| for (Set<String> cameraIdCombination : mConcurrentCameraIdCombinations) { |
| boolean frontFacingFound = false, backFacingFound = false; |
| for (String cameraId : cameraIdCombination) { |
| if (cameraId.equals(mainFrontId)) { |
| frontFacingFound = true; |
| } else if (cameraId.equals(mainBackId)) { |
| backFacingFound = true; |
| } |
| if (frontFacingFound && backFacingFound) { |
| combinationFound = true; |
| break; |
| } |
| } |
| if (combinationFound) { |
| break; |
| } |
| } |
| return combinationFound; |
| } |
| |
| /** |
| * Test the consistency of the statement: If FEATURE_CAMERA_CONCURRENT is advertised, |
| * CameraManager.getConcurrentCameraIds() |
| * returns a combination which contains the main front id and main back id, and vice versa. |
| */ |
| private void testConcurrentCameraFeature(String mainFrontId, String mainBackId) { |
| boolean frontBackFeatureAdvertised = |
| mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_CONCURRENT); |
| if (frontBackFeatureAdvertised) { |
| assertTrue("FEATURE_CAMERA_CONCURRENT advertised but main front id is null", |
| mainFrontId != null); |
| assertTrue("FEATURE_CAMERA_CONCURRENT advertised but main back id is null", |
| mainBackId != null); |
| } |
| |
| boolean concurrentMainFrontBackCombinationFound = |
| containsMainFrontBackConcurrentCombination(mainFrontId, mainBackId); |
| |
| if(mCameraIdsUnderTest.length > 0) { |
| assertTrue("System camera feature FEATURE_CAMERA_CONCURRENT = " |
| + frontBackFeatureAdvertised |
| + " and device actually having a main front back combination which can operate " |
| + "concurrently = " + concurrentMainFrontBackCombinationFound |
| + " do not match", |
| frontBackFeatureAdvertised == concurrentMainFrontBackCombinationFound); |
| } |
| } |
| |
| // Test: that properties can be queried from each device, without exceptions. |
| @Test |
| public void testCameraManagerGetCameraCharacteristics() throws Exception { |
| String[] ids = mCameraIdsUnderTest; |
| for (int i = 0; i < ids.length; i++) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(ids[i]); |
| assertNotNull( |
| String.format("Can't get camera characteristics from: ID %s", ids[i]), props); |
| } |
| } |
| |
| // Test: that properties queried between the Java SDK and the C++ NDK are equivalent. |
| @Test |
| public void testCameraCharacteristicsNdkFromSdk() throws Exception { |
| String[] ids = mCameraIdsUnderTest; |
| for (int i = 0; i < ids.length; i++) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(ids[i]); |
| Integer lensFacing = props.get(CameraCharacteristics.LENS_FACING); |
| assertNotNull("Can't get lens facing info", lensFacing); |
| |
| assertTrue(validateACameraMetadataFromCameraMetadataCriticalTagsNative( |
| props, lensFacing.intValue())); |
| } |
| } |
| |
| // Returns true if `props` has lens facing `lensFacing` when queried from the NDK via |
| // ACameraMetadata_fromCameraMetadata(). |
| private static native boolean validateACameraMetadataFromCameraMetadataCriticalTagsNative( |
| CameraCharacteristics props, int lensFacing); |
| |
| // Test: that an exception is thrown if an invalid device id is passed down. |
| @Test |
| public void testCameraManagerInvalidDevice() throws Exception { |
| String[] ids = mCameraIdsUnderTest; |
| // Create an invalid id by concatenating all the valid ids together. |
| StringBuilder invalidId = new StringBuilder(); |
| invalidId.append("INVALID"); |
| for (int i = 0; i < ids.length; i++) { |
| invalidId.append(ids[i]); |
| } |
| |
| try { |
| mCameraManager.getCameraCharacteristics( |
| invalidId.toString()); |
| fail(String.format("Accepted invalid camera ID: %s", invalidId.toString())); |
| } catch (IllegalArgumentException e) { |
| // This is the exception that should be thrown in this case. |
| } |
| } |
| |
| // Test: that each camera device can be opened one at a time, several times. |
| @Test |
| public void testCameraManagerOpenCamerasSerially() throws Exception { |
| testCameraManagerOpenCamerasSerially(/*useExecutor*/ false); |
| testCameraManagerOpenCamerasSerially(/*useExecutor*/ true); |
| } |
| |
| private void testCameraManagerOpenCamerasSerially(boolean useExecutor) throws Exception { |
| final Executor executor = useExecutor ? new HandlerExecutor(mHandler) : null; |
| String[] ids = mCameraIdsUnderTest; |
| for (int i = 0; i < ids.length; i++) { |
| for (int j = 0; j < NUM_CAMERA_REOPENS; j++) { |
| CameraDevice camera = null; |
| try { |
| MockStateCallback mockListener = MockStateCallback.mock(); |
| mCameraListener = new BlockingStateCallback(mockListener); |
| |
| if (useExecutor) { |
| mCameraManager.openCamera(ids[i], executor, mCameraListener); |
| } else { |
| mCameraManager.openCamera(ids[i], mCameraListener, mHandler); |
| } |
| |
| // Block until unConfigured |
| mCameraListener.waitForState(BlockingStateCallback.STATE_OPENED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| |
| // Ensure state transitions are in right order: |
| // -- 1) Opened |
| // Ensure no other state transitions have occurred: |
| camera = verifyCameraStateOpened(ids[i], mockListener); |
| } finally { |
| if (camera != null) { |
| camera.close(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Test: one or more camera devices can be open at the same time, or the right error state |
| * is set if this can't be done. |
| */ |
| @Test |
| public void testCameraManagerOpenAllCameras() throws Exception { |
| testCameraManagerOpenAllCameras(/*useExecutor*/ false); |
| testCameraManagerOpenAllCameras(/*useExecutor*/ true); |
| } |
| |
| private void testCameraManagerOpenAllCameras(boolean useExecutor) throws Exception { |
| String[] ids = mCameraIdsUnderTest; |
| assertNotNull("Camera ids shouldn't be null", ids); |
| |
| // Skip test if the device doesn't have multiple cameras. |
| if (ids.length <= 1) { |
| return; |
| } |
| |
| final Executor executor = useExecutor ? new HandlerExecutor(mHandler) : null; |
| List<CameraDevice> cameraList = new ArrayList<CameraDevice>(); |
| List<MockStateCallback> listenerList = new ArrayList<MockStateCallback>(); |
| List<BlockingStateCallback> blockingListenerList = new ArrayList<BlockingStateCallback>(); |
| try { |
| for (int i = 0; i < ids.length; i++) { |
| // Ignore state changes from other cameras |
| MockStateCallback mockListener = MockStateCallback.mock(); |
| mCameraListener = new BlockingStateCallback(mockListener); |
| |
| /** |
| * Track whether or not we got a synchronous error from openCamera. |
| * |
| * A synchronous error must also be accompanied by an asynchronous |
| * StateCallback#onError callback. |
| */ |
| boolean expectingError = false; |
| |
| String cameraId = ids[i]; |
| try { |
| if (useExecutor) { |
| mCameraManager.openCamera(cameraId, executor, mCameraListener); |
| } else { |
| mCameraManager.openCamera(cameraId, mCameraListener, mHandler); |
| } |
| } catch (CameraAccessException e) { |
| int reason = checkCameraAccessExceptionReason(e); |
| if (reason == CameraAccessException.CAMERA_DISCONNECTED || |
| reason == CameraAccessException.CAMERA_DISABLED) { |
| // TODO: We should handle a Disabled camera by passing here and elsewhere |
| fail("Camera must not be disconnected or disabled for this test" + ids[i]); |
| } else { |
| expectingError = true; |
| } |
| } |
| |
| List<Integer> expectedStates = new ArrayList<Integer>(); |
| expectedStates.add(BlockingStateCallback.STATE_OPENED); |
| expectedStates.add(BlockingStateCallback.STATE_ERROR); |
| int state = mCameraListener.waitForAnyOfStates( |
| expectedStates, CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| |
| // It's possible that we got an asynchronous error transition only. This is ok. |
| if (expectingError) { |
| assertEquals("Throwing a CAMERA_ERROR exception must be accompanied with a " + |
| "StateCallback#onError callback", |
| BlockingStateCallback.STATE_ERROR, state); |
| } |
| |
| /** |
| * Two situations are considered passing: |
| * 1) The camera opened successfully. |
| * => No error must be set. |
| * 2) The camera did not open because there were too many other cameras opened. |
| * => Only MAX_CAMERAS_IN_USE error must be set. |
| * |
| * Any other situation is considered a failure. |
| * |
| * For simplicity we treat disconnecting asynchronously as a failure, so |
| * camera devices should not be physically unplugged during this test. |
| */ |
| |
| CameraDevice camera; |
| if (state == BlockingStateCallback.STATE_ERROR) { |
| // Camera did not open because too many other cameras were opened |
| // => onError called exactly once with a non-null camera |
| assertTrue("At least one camera must be opened successfully", |
| cameraList.size() > 0); |
| |
| ArgumentCaptor<CameraDevice> argument = |
| ArgumentCaptor.forClass(CameraDevice.class); |
| |
| verify(mockListener) |
| .onError( |
| argument.capture(), |
| eq(CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE) |
| ); |
| verifyNoMoreInteractions(mockListener); |
| |
| camera = argument.getValue(); |
| assertNotNull("Expected a non-null camera for the error transition for ID: " |
| + ids[i], camera); |
| } else if (state == BlockingStateCallback.STATE_OPENED) { |
| // Camera opened successfully. |
| // => onOpened called exactly once |
| camera = verifyCameraStateOpened(cameraId, |
| mockListener); |
| } else { |
| fail("Unexpected state " + state); |
| camera = null; // unreachable. but need this for java compiler |
| } |
| |
| // Keep track of cameras so we can close it later |
| cameraList.add(camera); |
| listenerList.add(mockListener); |
| blockingListenerList.add(mCameraListener); |
| } |
| } finally { |
| for (int i = 0; i < cameraList.size(); i++) { |
| // With conflicting devices, opening of one camera could result in the other camera |
| // being disconnected. To handle such case, reset the mock before close. |
| reset(listenerList.get(i)); |
| cameraList.get(i).close(); |
| } |
| for (BlockingStateCallback blockingListener : blockingListenerList) { |
| blockingListener.waitForState( |
| BlockingStateCallback.STATE_CLOSED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| } |
| } |
| |
| /* |
| * Ensure that no state transitions have bled through from one camera to another |
| * after closing the cameras. |
| */ |
| int i = 0; |
| for (MockStateCallback listener : listenerList) { |
| CameraDevice camera = cameraList.get(i); |
| |
| verify(listener).onClosed(eq(camera)); |
| verifyNoMoreInteractions(listener); |
| i++; |
| // Only a #close can happen on the camera since we were done with it. |
| // Also nothing else should've happened between the close and the open. |
| } |
| } |
| |
| /** |
| * Verifies the camera in this listener was opened and then unconfigured exactly once. |
| * |
| * <p>This assumes that no other action to the camera has been done (e.g. |
| * it hasn't been configured, or closed, or disconnected). Verification is |
| * performed immediately without any timeouts.</p> |
| * |
| * <p>This checks that the state has previously changed first for opened and then unconfigured. |
| * Any other state transitions will fail. A test failure is thrown if verification fails.</p> |
| * |
| * @param cameraId Camera identifier |
| * @param listener Listener which was passed to {@link CameraManager#openCamera} |
| * |
| * @return The camera device (non-{@code null}). |
| */ |
| private static CameraDevice verifyCameraStateOpened(String cameraId, |
| MockStateCallback listener) { |
| ArgumentCaptor<CameraDevice> argument = |
| ArgumentCaptor.forClass(CameraDevice.class); |
| InOrder inOrder = inOrder(listener); |
| |
| /** |
| * State transitions (in that order): |
| * 1) onOpened |
| * |
| * No other transitions must occur for successful #openCamera |
| */ |
| inOrder.verify(listener) |
| .onOpened(argument.capture()); |
| |
| CameraDevice camera = argument.getValue(); |
| assertNotNull( |
| String.format("Failed to open camera device ID: %s", cameraId), |
| camera); |
| |
| // Do not use inOrder here since that would skip anything called before onOpened |
| verifyNoMoreInteractions(listener); |
| |
| return camera; |
| } |
| |
| /** |
| * Test: that opening the same device multiple times and make sure the right |
| * error state is set. |
| */ |
| @Test |
| public void testCameraManagerOpenCameraTwice() throws Exception { |
| testCameraManagerOpenCameraTwice(/*useExecutor*/ false); |
| testCameraManagerOpenCameraTwice(/*useExecutor*/ true); |
| } |
| |
| private void testCameraManagerOpenCameraTwice(boolean useExecutor) throws Exception { |
| String[] ids = mCameraIdsUnderTest; |
| final Executor executor = useExecutor ? new HandlerExecutor(mHandler) : null; |
| |
| // Test across every camera device. |
| for (int i = 0; i < ids.length; ++i) { |
| CameraDevice successCamera = null; |
| mCollector.setCameraId(ids[i]); |
| |
| try { |
| MockStateCallback mockSuccessListener = MockStateCallback.mock(); |
| MockStateCallback mockFailListener = MockStateCallback.mock(); |
| |
| BlockingStateCallback successListener = |
| new BlockingStateCallback(mockSuccessListener); |
| BlockingStateCallback failListener = |
| new BlockingStateCallback(mockFailListener); |
| |
| if (useExecutor) { |
| mCameraManager.openCamera(ids[i], executor, successListener); |
| mCameraManager.openCamera(ids[i], executor, failListener); |
| } else { |
| mCameraManager.openCamera(ids[i], successListener, mHandler); |
| mCameraManager.openCamera(ids[i], failListener, mHandler); |
| } |
| |
| successListener.waitForState(BlockingStateCallback.STATE_OPENED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| ArgumentCaptor<CameraDevice> argument = |
| ArgumentCaptor.forClass(CameraDevice.class); |
| verify(mockSuccessListener, atLeastOnce()).onOpened(argument.capture()); |
| verify(mockSuccessListener, atLeastOnce()).onDisconnected(argument.capture()); |
| |
| failListener.waitForState(BlockingStateCallback.STATE_OPENED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| verify(mockFailListener, atLeastOnce()).onOpened(argument.capture()); |
| |
| successCamera = verifyCameraStateOpened( |
| ids[i], mockFailListener); |
| |
| verifyNoMoreInteractions(mockFailListener); |
| } finally { |
| if (successCamera != null) { |
| successCamera.close(); |
| } |
| } |
| } |
| } |
| |
| private class NoopCameraListener extends CameraManager.AvailabilityCallback { |
| @Override |
| public void onCameraAvailable(String cameraId) { |
| // No-op |
| } |
| |
| @Override |
| public void onCameraUnavailable(String cameraId) { |
| // No-op |
| } |
| } |
| |
| /** |
| * Test: that the APIs to register and unregister a listener run successfully; |
| * doesn't test that the listener actually gets invoked at the right time. |
| * Registering a listener multiple times should have no effect, and unregistering |
| * a listener that isn't registered should have no effect. |
| */ |
| @Test |
| public void testCameraManagerListener() throws Exception { |
| mCameraManager.unregisterAvailabilityCallback(mListener); |
| // Test Handler API |
| mCameraManager.registerAvailabilityCallback(mListener, mHandler); |
| mCameraManager.registerAvailabilityCallback(mListener, mHandler); |
| mCameraManager.unregisterAvailabilityCallback(mListener); |
| mCameraManager.unregisterAvailabilityCallback(mListener); |
| // Test Executor API |
| Executor executor = new HandlerExecutor(mHandler); |
| mCameraManager.registerAvailabilityCallback(executor, mListener); |
| mCameraManager.registerAvailabilityCallback(executor, mListener); |
| mCameraManager.unregisterAvailabilityCallback(mListener); |
| mCameraManager.unregisterAvailabilityCallback(mListener); |
| } |
| |
| /** |
| * Test that the availability callbacks fire when expected |
| */ |
| @Test |
| public void testCameraManagerListenerCallbacks() throws Exception { |
| if (mOverrideCameraId != null) { |
| // Testing is done for individual camera. Skip. |
| return; |
| } |
| testCameraManagerListenerCallbacks(/*useExecutor*/ false); |
| testCameraManagerListenerCallbacks(/*useExecutor*/ true); |
| } |
| |
| private <T> void verifyAvailabilityCbsReceived(HashSet<T> expectedCameras, |
| LinkedBlockingQueue<T> queue, LinkedBlockingQueue<T> otherQueue, |
| boolean available) throws Exception { |
| while (expectedCameras.size() > 0) { |
| T id = queue.poll(AVAILABILITY_TIMEOUT_MS, |
| java.util.concurrent.TimeUnit.MILLISECONDS); |
| assertTrue("Did not receive initial " + (available ? "available" : "unavailable") |
| + " notices for some cameras", id != null); |
| expectedCameras.remove(id); |
| } |
| // Verify no unavailable/available cameras were reported |
| assertTrue("Some camera devices are initially " + (available ? "unavailable" : "available"), |
| otherQueue.size() == 0); |
| } |
| |
| private void verifySingleAvailabilityCbsReceived(LinkedBlockingQueue<String> expectedEventQueue, |
| LinkedBlockingQueue<String> unExpectedEventQueue, String expectedId, |
| String expectedStr, String unExpectedStr) throws Exception { |
| String candidateId = expectedEventQueue.poll(AVAILABILITY_TIMEOUT_MS, |
| java.util.concurrent.TimeUnit.MILLISECONDS); |
| assertNotNull("No " + expectedStr + " notice for expected ID " + expectedId, candidateId); |
| assertTrue("Received " + expectedStr + " notice for wrong ID, " + |
| "expected " + expectedId + ", got " + candidateId, expectedId.equals(candidateId)); |
| assertTrue("Received > 1 " + expectedStr + " callback for id " + expectedId, |
| expectedEventQueue.size() == 0); |
| assertTrue(unExpectedStr + " events received unexpectedly", |
| unExpectedEventQueue.size() == 0); |
| } |
| |
| private void testCameraManagerListenerCallbacks(boolean useExecutor) throws Exception { |
| |
| final LinkedBlockingQueue<String> availableEventQueue = new LinkedBlockingQueue<>(); |
| final LinkedBlockingQueue<String> unavailableEventQueue = new LinkedBlockingQueue<>(); |
| final Executor executor = useExecutor ? new HandlerExecutor(mHandler) : null; |
| |
| final LinkedBlockingQueue<Pair<String, String>> availablePhysicalCamEventQueue = |
| new LinkedBlockingQueue<>(); |
| final LinkedBlockingQueue<Pair<String, String>> unavailablePhysicalCamEventQueue = |
| new LinkedBlockingQueue<>(); |
| |
| final LinkedBlockingQueue<String> onCameraOpenedEventQueue = new LinkedBlockingQueue<>(); |
| final LinkedBlockingQueue<String> onCameraClosedEventQueue = new LinkedBlockingQueue<>(); |
| |
| CameraManager.AvailabilityCallback ac = new CameraManager.AvailabilityCallback() { |
| @Override |
| public void onCameraAvailable(String cameraId) { |
| // We allow this callback irrespective of mAdoptShellPerm since for this particular |
| // test, in the case when shell permissions are adopted we test all cameras, for |
| // simplicity. This is since when mAdoptShellPerm is false, we can't test for |
| // onCameraOpened/Closed callbacks (no CAMERA_OPEN_CLOSE_LISTENER permissions). |
| // So, to test all cameras, we test them when we adopt shell permission identity. |
| super.onCameraAvailable(cameraId); |
| availableEventQueue.offer(cameraId); |
| } |
| |
| @Override |
| public void onCameraUnavailable(String cameraId) { |
| super.onCameraUnavailable(cameraId); |
| unavailableEventQueue.offer(cameraId); |
| } |
| |
| @Override |
| public void onPhysicalCameraAvailable(String cameraId, String physicalCameraId) { |
| super.onPhysicalCameraAvailable(cameraId, physicalCameraId); |
| availablePhysicalCamEventQueue.offer(new Pair<>(cameraId, physicalCameraId)); |
| } |
| |
| @Override |
| public void onPhysicalCameraUnavailable(String cameraId, String physicalCameraId) { |
| super.onPhysicalCameraUnavailable(cameraId, physicalCameraId); |
| unavailablePhysicalCamEventQueue.offer(new Pair<>(cameraId, physicalCameraId)); |
| } |
| |
| @Override |
| public void onCameraOpened(String cameraId, String packageId) { |
| super.onCameraOpened(cameraId, packageId); |
| String curPackageId = mContext.getPackageName(); |
| assertTrue("Opening package should be " + curPackageId + ", was " + packageId, |
| curPackageId.equals(packageId)); |
| onCameraOpenedEventQueue.offer(cameraId); |
| } |
| |
| @Override |
| public void onCameraClosed(String cameraId) { |
| super.onCameraClosed(cameraId); |
| onCameraClosedEventQueue.offer(cameraId); |
| } |
| |
| }; |
| |
| if (useExecutor) { |
| mCameraManager.registerAvailabilityCallback(executor, ac); |
| } else { |
| mCameraManager.registerAvailabilityCallback(ac, mHandler); |
| } |
| String[] cameras = mCameraIdsUnderTest; |
| if (mAdoptShellPerm) { |
| //when mAdoptShellPerm is false, we can't test for |
| // onCameraOpened/Closed callbacks (no CAMERA_OPEN_CLOSE_LISTENER permissions). |
| // So, to test all cameras, we test them when we adopt shell permission identity. |
| cameras = mCameraManager.getCameraIdListNoLazy(); |
| } |
| |
| if (cameras.length == 0) { |
| Log.i(TAG, "No cameras present, skipping test mAdoprPerm"); |
| return; |
| } |
| |
| // Verify we received available for all cameras' initial state in a reasonable amount of time |
| HashSet<String> expectedAvailableCameras = new HashSet<String>(Arrays.asList(cameras)); |
| verifyAvailabilityCbsReceived(expectedAvailableCameras, availableEventQueue, |
| unavailableEventQueue, true /*available*/); |
| |
| // Verify transitions for individual cameras |
| for (String id : cameras) { |
| MockStateCallback mockListener = MockStateCallback.mock(); |
| mCameraListener = new BlockingStateCallback(mockListener); |
| |
| // Clear logical camera callback queue in case the initial state of certain physical |
| // cameras are unavailable. |
| unavailablePhysicalCamEventQueue.clear(); |
| |
| if (useExecutor) { |
| mCameraManager.openCamera(id, executor, mCameraListener); |
| } else { |
| mCameraManager.openCamera(id, mCameraListener, mHandler); |
| } |
| |
| // Block until opened |
| mCameraListener.waitForState(BlockingStateCallback.STATE_OPENED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| // Then verify only open happened, and get the camera handle |
| CameraDevice camera = verifyCameraStateOpened(id, mockListener); |
| |
| verifySingleAvailabilityCbsReceived(unavailableEventQueue, |
| availableEventQueue, id, "unavailability", "Availability"); |
| if (mAdoptShellPerm) { |
| // Verify that we see the expected 'onCameraOpened' event. |
| verifySingleAvailabilityCbsReceived(onCameraOpenedEventQueue, |
| onCameraClosedEventQueue, id, "onCameraOpened", "onCameraClosed"); |
| } |
| |
| // Verify that we see the expected 'unavailable' events if this camera is a physical |
| // camera of another logical multi-camera |
| HashSet<Pair<String, String>> relatedLogicalCameras = new HashSet<>(); |
| for (String multiCamId : cameras) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(multiCamId); |
| Set<String> physicalIds = props.getPhysicalCameraIds(); |
| if (physicalIds.contains(id)) { |
| relatedLogicalCameras.add(new Pair<String, String>(multiCamId, id)); |
| } |
| } |
| |
| HashSet<Pair<String, String>> expectedLogicalCameras = |
| new HashSet<>(relatedLogicalCameras); |
| verifyAvailabilityCbsReceived(expectedLogicalCameras, |
| unavailablePhysicalCamEventQueue, availablePhysicalCamEventQueue, |
| false /*available*/); |
| |
| // Verify that we see the expected 'available' event after closing the camera |
| |
| camera.close(); |
| mCameraListener.waitForState(BlockingStateCallback.STATE_CLOSED, |
| CameraTestUtils.CAMERA_CLOSE_TIMEOUT_MS); |
| |
| verifySingleAvailabilityCbsReceived(availableEventQueue, unavailableEventQueue, |
| id, "availability", "Unavailability"); |
| |
| if (mAdoptShellPerm) { |
| verifySingleAvailabilityCbsReceived(onCameraClosedEventQueue, |
| onCameraOpenedEventQueue, id, "onCameraClosed", "onCameraOpened"); |
| } |
| |
| expectedLogicalCameras = new HashSet<Pair<String, String>>(relatedLogicalCameras); |
| verifyAvailabilityCbsReceived(expectedLogicalCameras, |
| availablePhysicalCamEventQueue, unavailablePhysicalCamEventQueue, |
| true /*available*/); |
| } |
| |
| // Verify that we can unregister the listener and see no more events |
| assertTrue("Availability events received unexpectedly", |
| availableEventQueue.size() == 0); |
| assertTrue("Unavailability events received unexpectedly", |
| unavailableEventQueue.size() == 0); |
| |
| mCameraManager.unregisterAvailabilityCallback(ac); |
| |
| { |
| // Open an arbitrary camera and make sure we don't hear about it |
| |
| MockStateCallback mockListener = MockStateCallback.mock(); |
| mCameraListener = new BlockingStateCallback(mockListener); |
| |
| if (useExecutor) { |
| mCameraManager.openCamera(cameras[0], executor, mCameraListener); |
| } else { |
| mCameraManager.openCamera(cameras[0], mCameraListener, mHandler); |
| } |
| |
| // Block until opened |
| mCameraListener.waitForState(BlockingStateCallback.STATE_OPENED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| // Then verify only open happened, and close the camera |
| CameraDevice camera = verifyCameraStateOpened(cameras[0], mockListener); |
| |
| camera.close(); |
| |
| mCameraListener.waitForState(BlockingStateCallback.STATE_CLOSED, |
| CameraTestUtils.CAMERA_CLOSE_TIMEOUT_MS); |
| |
| // No unavailability or availability callback should have occured |
| String candidateId = unavailableEventQueue.poll(AVAILABILITY_TIMEOUT_MS, |
| java.util.concurrent.TimeUnit.MILLISECONDS); |
| assertTrue(String.format("Received unavailability notice for ID %s unexpectedly ", |
| candidateId), |
| candidateId == null); |
| |
| candidateId = availableEventQueue.poll(AVAILABILITY_TIMEOUT_MS, |
| java.util.concurrent.TimeUnit.MILLISECONDS); |
| assertTrue(String.format("Received availability notice for ID %s unexpectedly ", |
| candidateId), |
| candidateId == null); |
| |
| Pair<String, String> candidatePhysicalIds = unavailablePhysicalCamEventQueue.poll( |
| AVAILABILITY_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS); |
| assertTrue("Received unavailability physical camera notice unexpectedly ", |
| candidatePhysicalIds == null); |
| |
| candidatePhysicalIds = availablePhysicalCamEventQueue.poll( |
| AVAILABILITY_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS); |
| assertTrue("Received availability notice for physical camera unexpectedly ", |
| candidatePhysicalIds == null); |
| } |
| |
| if (mAdoptShellPerm) { |
| // Open an arbitrary camera and make sure subsequently subscribed listener receives |
| // correct onCameraOpened/onCameraClosed callbacks |
| |
| MockStateCallback mockListener = MockStateCallback.mock(); |
| mCameraListener = new BlockingStateCallback(mockListener); |
| |
| if (useExecutor) { |
| mCameraManager.openCamera(cameras[0], executor, mCameraListener); |
| } else { |
| mCameraManager.openCamera(cameras[0], mCameraListener, mHandler); |
| } |
| |
| // Block until opened |
| mCameraListener.waitForState(BlockingStateCallback.STATE_OPENED, |
| CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS); |
| // Then verify only open happened, and close the camera |
| CameraDevice camera = verifyCameraStateOpened(cameras[0], mockListener); |
| |
| if (useExecutor) { |
| mCameraManager.registerAvailabilityCallback(executor, ac); |
| } else { |
| mCameraManager.registerAvailabilityCallback(ac, mHandler); |
| } |
| |
| // Verify that we see the expected 'onCameraOpened' event. |
| verifySingleAvailabilityCbsReceived(onCameraOpenedEventQueue, |
| onCameraClosedEventQueue, cameras[0], "onCameraOpened", "onCameraClosed"); |
| |
| camera.close(); |
| |
| mCameraListener.waitForState(BlockingStateCallback.STATE_CLOSED, |
| CameraTestUtils.CAMERA_CLOSE_TIMEOUT_MS); |
| |
| verifySingleAvailabilityCbsReceived(onCameraClosedEventQueue, |
| onCameraOpenedEventQueue, cameras[0], "onCameraClosed", "onCameraOpened"); |
| } |
| } // testCameraManagerListenerCallbacks |
| |
| // Verify no LEGACY-level devices appear on devices first launched in the Q release or newer |
| @Test |
| @AppModeFull(reason = "Instant apps can't access Test API") |
| public void testNoLegacyOnQ() throws Exception { |
| if(PropertyUtil.getFirstApiLevel() < Build.VERSION_CODES.Q){ |
| // LEGACY still allowed for devices upgrading to Q |
| return; |
| } |
| String[] ids = mCameraIdsUnderTest; |
| for (int i = 0; i < ids.length; i++) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(ids[i]); |
| assertNotNull( |
| String.format("Can't get camera characteristics from: ID %s", ids[i]), props); |
| Integer hardwareLevel = props.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); |
| assertNotNull( |
| String.format("Can't get hardware level from: ID %s", ids[i]), hardwareLevel); |
| assertTrue(String.format( |
| "Camera device %s cannot be LEGACY level for devices launching on Q", |
| ids[i]), |
| hardwareLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY); |
| } |
| } |
| |
| @Test |
| public void testCameraManagerWithDnD() throws Exception { |
| String[] cameras = mCameraIdsUnderTest; |
| if (cameras.length == 0) { |
| Log.i(TAG, "No cameras present, skipping test"); |
| return; |
| } |
| // Allow the test package to adjust notification policy |
| toggleNotificationPolicyAccess(mContext.getPackageName(), |
| InstrumentationRegistry.getInstrumentation(), true); |
| |
| // Enable DnD filtering |
| |
| NotificationManager nm = mContext.getSystemService(NotificationManager.class); |
| try { |
| nm.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE); |
| |
| // Try to use the camera API |
| |
| for (String cameraId : cameras) { |
| try { |
| CameraCharacteristics c = mCameraManager.getCameraCharacteristics(cameraId); |
| assertTrue("Unable to get camera characteristics when DnD is enabled", |
| c != null); |
| } catch (RuntimeException e) { |
| fail("RuntimeException thrown when attempting to access camera " + |
| "characteristics with DnD enabled. " + |
| "https://android-review.googlesource.com/c/platform/frameworks/base/+" + |
| "/747089/ may be missing."); |
| } |
| } |
| } finally { |
| // Restore notifications to normal |
| |
| nm.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL); |
| } |
| } |
| |
| @Test |
| public void testCameraManagerAutomotiveCameras() throws Exception { |
| if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { |
| // Execute this test only on the automotive device implementations |
| Log.i(TAG, "Skips this test on non automotive device implementations"); |
| return; |
| } |
| |
| String[] cameraIds = mCameraIdsUnderTest; |
| if (cameraIds.length < 1) { |
| Log.i(TAG, "No cameras present, skipping test"); |
| return; |
| } |
| |
| /** |
| * On automotive device implementations, all cameras must have android.automotive.location |
| * and android.automotive.lens.facing in their static metadata. Also, |
| * android.lens.poseTranslation and android.lens.poseRotation must present in a camera's |
| * static metadata, and android.lens.poseReference should be set as |
| * LENS_POSE_REFERENCE_AUTOMOTIVE in following conditions. |
| * |
| * - android.automotive.location has AUTOMOTIVE_LOCATION_EXTERIOR_OTHER or |
| * AUTOMOTIVE_LOCATION_EXTRA_OTHER |
| * - android.automotive.lens.facing has AUTOMOTIVE_LENS_FACING_EXTERIOR_OTHER or |
| * AUTOMOTIVE_LENS_FACING_INTERIOR_OTHER |
| * - One or more camera has the same android.automotive.location and |
| * android.automotive.lens.facing values |
| */ |
| Map<Pair<Integer, Integer>, ArrayList<String>> cameraGroup = new HashMap<>(); |
| for (String cameraId : cameraIds) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(cameraId); |
| assertNotNull( |
| String.format("Can't get camera characteristics from: ID %s", cameraId), props); |
| |
| Integer lensFacing = props.get(CameraCharacteristics.LENS_FACING); |
| if (lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_EXTERNAL) { |
| // Automotive device implementations may have external cameras but they are exempted |
| // from this test case. |
| continue; |
| } |
| |
| Integer cameraLocation = props.get(CameraCharacteristics.AUTOMOTIVE_LOCATION); |
| assertNotNull( |
| String.format("Can't get a camera location from: ID %s", cameraId), |
| cameraLocation); |
| |
| int[] automotiveLensFacing = props.get(CameraCharacteristics.AUTOMOTIVE_LENS_FACING); |
| assertNotNull( |
| String.format("Can't get a lens facing direction from: ID %s", cameraId), |
| automotiveLensFacing); |
| |
| if (cameraLocation == CameraCharacteristics.AUTOMOTIVE_LOCATION_EXTERIOR_OTHER || |
| cameraLocation == CameraCharacteristics.AUTOMOTIVE_LOCATION_EXTRA_OTHER || |
| automotiveLensFacing[0] == |
| CameraCharacteristics.AUTOMOTIVE_LENS_FACING_EXTERIOR_OTHER || |
| automotiveLensFacing[0] == |
| CameraCharacteristics.AUTOMOTIVE_LENS_FACING_INTERIOR_OTHER) { |
| checkAutomotiveLensPoseCharacteristics(cameraId, props); |
| } else { |
| Pair<Integer, Integer> key = new Pair<>(cameraLocation, automotiveLensFacing[0]); |
| if (cameraGroup.containsKey(key)) { |
| cameraGroup.get(key).add(cameraId); |
| } else { |
| cameraGroup.put(key, new ArrayList<>(Arrays.asList(cameraId))); |
| } |
| } |
| } |
| |
| for (Map.Entry<Pair<Integer, Integer>, ArrayList<String>> entry : cameraGroup.entrySet()) { |
| ArrayList<String> cameraIdsToVerify = entry.getValue(); |
| if (cameraIdsToVerify.size() > 1) { |
| for (String id : cameraIdsToVerify) { |
| CameraCharacteristics props = mCameraManager.getCameraCharacteristics(id); |
| checkAutomotiveLensPoseCharacteristics(id, props); |
| } |
| } |
| } |
| } |
| |
| private void checkAutomotiveLensPoseCharacteristics(String cameraId, |
| CameraCharacteristics props) { |
| Integer reference = props.get(CameraCharacteristics.LENS_POSE_REFERENCE); |
| assertNotNull( |
| String.format("Can't get a lens pose reference from: ID %s", cameraId), |
| reference); |
| assertTrue("Lens pose reference must be AUTOMOTIVE", |
| reference == CameraCharacteristics.LENS_POSE_REFERENCE_AUTOMOTIVE); |
| float[] translation = props.get(CameraCharacteristics.LENS_POSE_TRANSLATION); |
| assertNotNull( |
| String.format("Can't get a lens pose translation from: ID %s", cameraId), |
| translation); |
| float[] rotation = props.get(CameraCharacteristics.LENS_POSE_ROTATION); |
| assertNotNull( |
| String.format("Can't get a lens pose rotation from: ID %s", cameraId), |
| rotation); |
| } |
| |
| |
| private void toggleNotificationPolicyAccess(String packageName, |
| Instrumentation instrumentation, boolean on) throws IOException { |
| |
| String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName; |
| |
| runCommand(command, instrumentation); |
| |
| NotificationManager nm = mContext.getSystemService(NotificationManager.class); |
| assertEquals("Notification Policy Access Grant is " + |
| nm.isNotificationPolicyAccessGranted() + " not " + on, on, |
| nm.isNotificationPolicyAccessGranted()); |
| } |
| |
| private void runCommand(String command, Instrumentation instrumentation) throws IOException { |
| UiAutomation uiAutomation = instrumentation.getUiAutomation(); |
| // Execute command |
| ParcelFileDescriptor fd = mUiAutomation.executeShellCommand(command); |
| assertNotNull("Failed to execute shell command: " + command, fd); |
| // Wait for the command to finish by reading until EOF |
| try (InputStream in = new FileInputStream(fd.getFileDescriptor())) { |
| byte[] buffer = new byte[4096]; |
| while (in.read(buffer) > 0) {} |
| } catch (IOException e) { |
| throw new IOException("Could not read stdout of command: " + command, e); |
| } |
| } |
| |
| } |