/*
 * 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.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 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 = CameraTestUtils.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 = CameraTestUtils.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.
        }
    }

    /**
     * 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 = CameraTestUtils.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 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));
        CameraTestUtils.verifyAvailabilityCbsReceived(expectedAvailableCameras, availableEventQueue,
                unavailableEventQueue, true /*available*/);

        // Clear physical camera callback queue in case the initial state of certain physical
        // cameras are unavailable.
        unavailablePhysicalCamEventQueue.clear();

        // Verify transitions for individual cameras
        for (String id : cameras) {
            MockStateCallback mockListener = MockStateCallback.mock();
            mCameraListener = new BlockingStateCallback(mockListener);

            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 = CameraTestUtils.verifyCameraStateOpened(id, mockListener);

            CameraTestUtils.verifySingleAvailabilityCbsReceived(unavailableEventQueue,
                        availableEventQueue, id, "unavailability", "Availability");
            if (mAdoptShellPerm) {
                // Verify that we see the expected 'onCameraOpened' event.
                CameraTestUtils.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);
            CameraTestUtils.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);

            CameraTestUtils.verifySingleAvailabilityCbsReceived(availableEventQueue,
                    unavailableEventQueue, id, "availability", "Unavailability");

            if (mAdoptShellPerm) {
                CameraTestUtils.verifySingleAvailabilityCbsReceived(onCameraClosedEventQueue,
                        onCameraOpenedEventQueue, id, "onCameraClosed", "onCameraOpened");
            }

            expectedLogicalCameras = new HashSet<Pair<String, String>>(relatedLogicalCameras);
            CameraTestUtils.verifyAvailabilityCbsReceived(expectedLogicalCameras,
                    availablePhysicalCamEventQueue,
                    null /*unExpectedEventQueue*/,
                    true /*available*/);

            // Clear physical camera callback queue in case the initial state of certain physical
            // cameras are unavailable.
            unavailablePhysicalCamEventQueue.clear();
        }

        // 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 = CameraTestUtils.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 = CameraTestUtils.verifyCameraStateOpened(cameras[0], mockListener);

            if (useExecutor) {
                mCameraManager.registerAvailabilityCallback(executor, ac);
            } else {
                mCameraManager.registerAvailabilityCallback(ac, mHandler);
            }

            // Verify that we see the expected 'onCameraOpened' event.
            CameraTestUtils.verifySingleAvailabilityCbsReceived(onCameraOpenedEventQueue,
                    onCameraClosedEventQueue, cameras[0], "onCameraOpened", "onCameraClosed");

            camera.close();

            mCameraListener.waitForState(BlockingStateCallback.STATE_CLOSED,
                    CameraTestUtils.CAMERA_CLOSE_TIMEOUT_MS);

            CameraTestUtils.verifySingleAvailabilityCbsReceived(onCameraClosedEventQueue,
                    onCameraOpenedEventQueue, cameras[0], "onCameraClosed", "onCameraOpened");

            mCameraManager.unregisterAvailabilityCallback(ac);
        }
    } // testCameraManagerListenerCallbacks

    /**
     * Test that the physical camera available/unavailable callback behavior is consistent
     * between:
     *
     * - No camera is open,
     * - After camera is opened, and
     * - After camera is closed,
     */
    @Test
    public void testPhysicalCameraAvailabilityConsistency() throws Throwable {
        CameraTestUtils.testPhysicalCameraAvailabilityConsistencyHelper(mCameraIdsUnderTest,
                mCameraManager, mHandler, true /*expectInitialCallbackAfterOpen*/);
    }

    // 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);
        }
    }

}
