Camera2CameraCoordinator.java

/*
 * Copyright 2023 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 androidx.camera.camera2.internal.concurrent;

import android.hardware.camera2.CameraCharacteristics;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.Logger;
import androidx.camera.core.concurrent.CameraCoordinator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Implementation for {@link CameraCoordinator}.
 */
@RequiresApi(21)
public class Camera2CameraCoordinator implements CameraCoordinator {

    private static final String TAG = "Camera2CameraCoordinator";

    @NonNull private final CameraManagerCompat mCameraManager;
    @NonNull private final List<ConcurrentCameraModeListener> mConcurrentCameraModeListeners;
    @NonNull private final Map<String, List<String>> mConcurrentCameraIdMap;
    @NonNull private List<CameraInfo> mActiveConcurrentCameraInfos;
    @NonNull private Set<Set<String>> mConcurrentCameraIds;

    @CameraOperatingMode private int mCameraOperatingMode = CAMERA_OPERATING_MODE_UNSPECIFIED;

    public Camera2CameraCoordinator(@NonNull CameraManagerCompat cameraManager) {
        mCameraManager = cameraManager;
        mConcurrentCameraIdMap = new HashMap<>();
        mConcurrentCameraIds = new HashSet<>();
        mConcurrentCameraModeListeners = new ArrayList<>();
        mActiveConcurrentCameraInfos = new ArrayList<>();
        retrieveConcurrentCameraIds();
    }

    @NonNull
    @Override
    public List<List<CameraSelector>> getConcurrentCameraSelectors() {
        List<List<CameraSelector>> concurrentCameraSelectorLists = new ArrayList<>();
        for (Set<String> concurrentCameraIdList: mConcurrentCameraIds) {
            List<CameraSelector> cameraSelectors = new ArrayList<>();
            for (String concurrentCameraId : concurrentCameraIdList) {
                cameraSelectors.add(createCameraSelectorById(mCameraManager, concurrentCameraId));
            }
            concurrentCameraSelectorLists.add(cameraSelectors);
        }
        return concurrentCameraSelectorLists;
    }

    @NonNull
    @Override
    public List<CameraInfo> getActiveConcurrentCameraInfos() {
        return mActiveConcurrentCameraInfos;
    }

    @Override
    public void setActiveConcurrentCameraInfos(@NonNull List<CameraInfo> cameraInfos) {
        mActiveConcurrentCameraInfos = new ArrayList<>(cameraInfos);
    }

    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    @Nullable
    @Override
    public String getPairedConcurrentCameraId(@NonNull String cameraId) {
        if (!mConcurrentCameraIdMap.containsKey(cameraId)) {
            return null;
        }
        for (String pairedCameraId : mConcurrentCameraIdMap.get(cameraId)) {
            for (CameraInfo cameraInfo : mActiveConcurrentCameraInfos) {
                if (pairedCameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
                    return pairedCameraId;
                }
            }
        }
        return null;
    }

    @CameraOperatingMode
    @Override
    public int getCameraOperatingMode() {
        return mCameraOperatingMode;
    }

    @Override
    public void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode) {
        if (cameraOperatingMode != mCameraOperatingMode) {
            for (ConcurrentCameraModeListener listener : mConcurrentCameraModeListeners) {
                listener.onCameraOperatingModeUpdated(
                        mCameraOperatingMode,
                        cameraOperatingMode);
            }
        }
        // Clear the cached active camera infos if concurrent mode is off.
        if (mCameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT
                && cameraOperatingMode != CAMERA_OPERATING_MODE_CONCURRENT) {
            mActiveConcurrentCameraInfos.clear();
        }
        mCameraOperatingMode = cameraOperatingMode;
    }

    @Override
    public void addListener(@NonNull ConcurrentCameraModeListener listener) {
        mConcurrentCameraModeListeners.add(listener);
    }

    @Override
    public void removeListener(@NonNull ConcurrentCameraModeListener listener) {
        mConcurrentCameraModeListeners.remove(listener);
    }

    private void retrieveConcurrentCameraIds() {
        try {
            mConcurrentCameraIds = mCameraManager.getConcurrentCameraIds();
        } catch (CameraAccessExceptionCompat e) {
            Logger.e(TAG, "Failed to get concurrent camera ids");
        }

        for (Set<String> concurrentCameraIdList: mConcurrentCameraIds) {
            List<String> cameraIdList = new ArrayList<>(concurrentCameraIdList);

            if (cameraIdList.size() >= 2) {
                String cameraId1 = cameraIdList.get(0);
                String cameraId2 = cameraIdList.get(1);
                if (!mConcurrentCameraIdMap.containsKey(cameraId1)) {
                    mConcurrentCameraIdMap.put(cameraId1, new ArrayList<>());
                }
                if (!mConcurrentCameraIdMap.containsKey(cameraId2)) {
                    mConcurrentCameraIdMap.put(cameraId2, new ArrayList<>());
                }
                mConcurrentCameraIdMap.get(cameraIdList.get(0)).add(cameraIdList.get(1));
                mConcurrentCameraIdMap.get(cameraIdList.get(1)).add(cameraIdList.get(0));
            }
        }
    }

    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    private static CameraSelector createCameraSelectorById(
            @NonNull CameraManagerCompat cameraManager,
            @NonNull String cameraId) {
        CameraSelector.Builder builder =
                new CameraSelector.Builder().addCameraFilter(cameraInfos -> {
                    for (CameraInfo cameraInfo : cameraInfos) {
                        if (cameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
                            return Collections.singletonList(cameraInfo);
                        }
                    }

                    throw new IllegalArgumentException("No camera can be find for id: " + cameraId);
                });

        try {
            CameraCharacteristicsCompat cameraCharacteristics =
                    cameraManager.getCameraCharacteristicsCompat(cameraId);
            Integer cameraLensFacing = cameraCharacteristics.get(
                    CameraCharacteristics.LENS_FACING);
            builder.requireLensFacing(cameraLensFacing);
        } catch (CameraAccessExceptionCompat e) {
            throw new RuntimeException(e);
        }

        return builder.build();
    }
}