SupportedSurfaceCombination.java

/*
 * Copyright 2022 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;

import static android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT;
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES;

import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;

import android.content.Context;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.util.Range;
import android.util.Size;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
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.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceSizeDefinition;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.utils.CompareSizesByArea;
import androidx.core.util.Preconditions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Camera device supported surface configuration combinations
 *
 * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
 * guaranteed stream combinations for different hardware level devices. It defines what combination
 * of surface configuration type and size pairs can be supported for different hardware level camera
 * devices. This structure is used to store a list of surface combinations that are guaranteed to
 * support for this camera device.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
final class SupportedSurfaceCombination {
    private static final String TAG = "SupportedSurfaceCombination";
    private final List<SurfaceCombination> mSurfaceCombinations = new ArrayList<>();
    private final List<SurfaceCombination> mConcurrentSurfaceCombinations = new ArrayList<>();
    private final String mCameraId;
    private final CamcorderProfileHelper mCamcorderProfileHelper;
    private final CameraCharacteristicsCompat mCharacteristics;
    private final ExtraSupportedSurfaceCombinationsContainer
            mExtraSupportedSurfaceCombinationsContainer;
    private final int mHardwareLevel;
    private boolean mIsRawSupported = false;
    private boolean mIsBurstCaptureSupported = false;
    @VisibleForTesting
    SurfaceSizeDefinition mSurfaceSizeDefinition;
    @NonNull
    private final DisplayInfoManager mDisplayInfoManager;
    private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();

    SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
            @NonNull CameraManagerCompat cameraManagerCompat,
            @NonNull CamcorderProfileHelper camcorderProfileHelper)
            throws CameraUnavailableException {
        mCameraId = Preconditions.checkNotNull(cameraId);
        mCamcorderProfileHelper = Preconditions.checkNotNull(camcorderProfileHelper);
        mExtraSupportedSurfaceCombinationsContainer =
                new ExtraSupportedSurfaceCombinationsContainer();
        mDisplayInfoManager = DisplayInfoManager.getInstance(context);

        try {
            mCharacteristics = cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId);
            Integer keyValue = mCharacteristics.get(
                    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
            mHardwareLevel = keyValue != null ? keyValue
                    : CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
        } catch (CameraAccessExceptionCompat e) {
            throw CameraUnavailableExceptionHelper.createFrom(e);
        }

        int[] availableCapabilities =
                mCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);

        if (availableCapabilities != null) {
            for (int capability : availableCapabilities) {
                if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) {
                    mIsRawSupported = true;
                } else if (capability
                        == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
                    mIsBurstCaptureSupported = true;
                }
            }
        }

        generateSupportedCombinationList();
        if (context.getPackageManager().hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
            generateConcurrentSupportedCombinationList();
        }
        generateSurfaceSizeDefinition();
        checkCustomization();
    }

    String getCameraId() {
        return mCameraId;
    }

    boolean isRawSupported() {
        return mIsRawSupported;
    }

    boolean isBurstCaptureSupported() {
        return mIsBurstCaptureSupported;
    }

    /**
     * Check whether the input surface configuration list is under the capability of any combination
     * of this object.
     *
     * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise false.
     * @param surfaceConfigList the surface configuration list to be compared
     * @return the check result that whether it could be supported
     */
    boolean checkSupported(
            boolean isConcurrentCameraModeOn,
            List<SurfaceConfig> surfaceConfigList) {
        boolean isSupported = false;

        List<SurfaceCombination> targetSurfaceCombinations = isConcurrentCameraModeOn
                ? mConcurrentSurfaceCombinations : mSurfaceCombinations;
        for (SurfaceCombination surfaceCombination : targetSurfaceCombinations) {
            isSupported = surfaceCombination.isSupported(surfaceConfigList);

            if (isSupported) {
                break;
            }
        }

        return isSupported;
    }

    /**
     * Transform to a SurfaceConfig object with image format and size info
     *
     * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise false.
     * @param imageFormat the image format info for the surface configuration object
     * @param size        the size info for the surface configuration object
     * @return new {@link SurfaceConfig} object
     */
    SurfaceConfig transformSurfaceConfig(
            boolean isConcurrentCameraModeOn,
            int imageFormat,
            Size size) {
        Size maxOutputSizeForConcurrentMode = isConcurrentCameraModeOn
                ? getMaxOutputSizeByFormat(imageFormat) : null;
        return SurfaceConfig.transformSurfaceConfig(
                isConcurrentCameraModeOn,
                imageFormat,
                size,
                mSurfaceSizeDefinition,
                maxOutputSizeForConcurrentMode);
    }

    static int getMaxFramerate(CameraCharacteristicsCompat characteristics, int imageFormat,
            Size size) {
        int maxFramerate = 0;
        try {
            maxFramerate = (int) (1000000000.0
                    / characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                    .getOutputMinFrameDuration(imageFormat,
                            size));
        } catch (Exception e) {
            //TODO
            //this try catch is in place for the rare that a surface config has a size
            // incompatible for getOutputMinFrameDuration...  put into a Quirk
        }
        return maxFramerate;
    }

    /**
     *
     * @param range
     * @return the length of the range
     */
    private static int getRangeLength(Range<Integer> range) {
        return (range.getUpper() - range.getLower()) + 1;
    }

    /**
     * @return the distance between the nearest limits of two non-intersecting ranges
     */
    private static int getRangeDistance(Range<Integer> firstRange, Range<Integer> secondRange) {
        Preconditions.checkState(
                !firstRange.contains(secondRange.getUpper())
                        && !firstRange.contains(secondRange.getLower()),
                "Ranges must not intersect");
        if (firstRange.getLower() > secondRange.getUpper()) {
            return firstRange.getLower() - secondRange.getUpper();
        } else {
            return secondRange.getLower() - firstRange.getUpper();
        }
    }

    /**
     * @param targetFps the target frame rate range used while comparing to device-supported ranges
     * @param storedRange the device-supported range that is currently saved and intersects with
     *                    targetFps
     * @param newRange a new potential device-supported range that intersects with targetFps
     * @return the device-supported range that better matches the target fps
     */
    private static Range<Integer> compareIntersectingRanges(Range<Integer> targetFps,
            Range<Integer> storedRange, Range<Integer> newRange) {
        // TODO(b/272075984): some ranges may may have a larger intersection but may also have an
        //  excessively large portion that is non-intersecting. Will want to do further
        //  investigation to find a more optimized way to decide when a potential range has too
        //  much non-intersecting value and discard it

        double storedIntersectionsize = getRangeLength(storedRange.intersect(targetFps));
        double newIntersectionSize = getRangeLength(newRange.intersect(targetFps));

        double newRangeRatio = newIntersectionSize / getRangeLength(newRange);
        double storedRangeRatio = storedIntersectionsize / getRangeLength(storedRange);

        if (newIntersectionSize > storedIntersectionsize) {
            // if new, the new range must have at least 50% of its range intersecting, OR has a
            // larger percentage of intersection than the previous stored range
            if (newRangeRatio >= .5 || newRangeRatio >= storedRangeRatio) {
                return newRange;
            }
        } else if (newIntersectionSize == storedIntersectionsize) {
            // if intersecting ranges have same length... pick the one that has the higher
            // intersection ratio
            if (newRangeRatio > storedRangeRatio) {
                return newRange;
            } else if (newRangeRatio == storedRangeRatio
                    && newRange.getLower() > storedRange.getLower()) {
                // if equal intersection size AND ratios pick the higher range
                return newRange;
            }

        } else if (storedRangeRatio < .5
                && newRangeRatio > storedRangeRatio) {
            // if the new one has a smaller range... only change if existing has an intersection
            // ratio < 50% and the new one has an intersection ratio > than the existing one
            return newRange;
        }
        return storedRange;
    }

    /**
     * Finds a frame rate range supported by the device that is closest to the target framerate
     *
     * @param targetFrameRate the Target Frame Rate resolved from all current existing surfaces
     *                        and incoming new use cases
     * @return a frame rate range supported by the device that is closest to targetFrameRate
     */

    @NonNull
    private Range<Integer> getClosestSupportedDeviceFrameRate(Range<Integer> targetFrameRate,
            int maxFps) {
        if (targetFrameRate == null) {
            return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
        }

        // get all fps ranges supported by device
        Range<Integer>[] availableFpsRanges =
                mCharacteristics.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);

        if (availableFpsRanges == null) {
            return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
        }
        // if  whole target framerate range > maxFps of configuration, the target for this
        // calculation will be [max,max].

        // if the range is partially larger than  maxFps, the target for this calculation will be
        // [target.lower, max] for the sake of this calculation
        targetFrameRate = new Range<>(
                Math.min(targetFrameRate.getLower(), maxFps),
                Math.min(targetFrameRate.getUpper(), maxFps)
        );

        Range<Integer> bestRange = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
        int currentIntersectSize = 0;


        for (Range<Integer> potentialRange : availableFpsRanges) {
            // ignore ranges completely larger than configuration's maximum fps
            if (maxFps >= potentialRange.getLower()) {
                if (bestRange.equals(StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED)) {
                    bestRange = potentialRange;
                }
                // take if range is a perfect match
                if (potentialRange.equals(targetFrameRate)) {
                    bestRange = potentialRange;
                    break;
                }

                try {
                    // bias towards a range that intersects on the upper end
                    Range<Integer> newIntersection = potentialRange.intersect(targetFrameRate);
                    int newIntersectSize = getRangeLength(newIntersection);
                    // if this range intersects our target + no other range was already
                    if (currentIntersectSize == 0) {
                        bestRange = potentialRange;
                        currentIntersectSize = newIntersectSize;
                    } else if (newIntersectSize >= currentIntersectSize) {
                        // if the currently stored range + new range both intersect, check to see
                        // which one should be picked over the other
                        bestRange = compareIntersectingRanges(targetFrameRate, bestRange,
                                potentialRange);
                        currentIntersectSize = getRangeLength(targetFrameRate.intersect(bestRange));
                    }
                } catch (IllegalArgumentException e) {
                    // if no intersection is present, pick the range that is closer to our target
                    if (currentIntersectSize == 0) {
                        if (getRangeDistance(potentialRange, targetFrameRate)
                                < getRangeDistance(bestRange, targetFrameRate)) {
                            bestRange = potentialRange;
                        } else if (getRangeDistance(potentialRange, targetFrameRate)
                                == getRangeDistance(bestRange, targetFrameRate)) {
                            if (potentialRange.getLower() > bestRange.getUpper()) {
                                // if they both have the same distance, pick the higher range
                                bestRange = potentialRange;
                            } else if (getRangeLength(potentialRange) < getRangeLength(bestRange)) {
                                // if one isn't higher than the other, pick the range with the
                                // shorter length
                                bestRange = potentialRange;
                            }
                        }
                    }
                }
            }

        }
        return bestRange;
    }

    /**
     * @param newTargetFramerate    an incoming framerate range
     * @param storedTargetFramerate a stored framerate range to be modified
     * @return adjusted target frame rate
     *
     * If the two ranges are both nonnull and disjoint of each other, then the range that was
     * already stored will be used
     */
    private Range<Integer> getUpdatedTargetFramerate(Range<Integer> newTargetFramerate,
            Range<Integer> storedTargetFramerate) {
        Range<Integer> updatedTarget = storedTargetFramerate;

        if (storedTargetFramerate == null) {
            // if stored value was null before, set it to the new value
            updatedTarget = newTargetFramerate;
        } else if (newTargetFramerate != null) {
            try {
                // get intersection of existing target fps
                updatedTarget =
                        storedTargetFramerate
                                .intersect(newTargetFramerate);
            } catch (IllegalArgumentException e) {
                // no intersection, keep the previously stored value
                updatedTarget = storedTargetFramerate;
            }
        }
        return updatedTarget;
    }

    /**
     * @param currentMaxFps the previously stored Max FPS
     * @param imageFormat   the image format of the incoming surface
     * @param size          the size of the incoming surface
     */
    private int getUpdatedMaximumFps(int currentMaxFps, int imageFormat, Size size) {
        return Math.min(currentMaxFps, getMaxFramerate(mCharacteristics, imageFormat, size));
    }

    /**
     * Finds the suggested stream specifications of the newly added UseCaseConfig.
     *
     * @param isConcurrentCameraModeOn          true if concurrent camera mode is on, otherwise
     *                                          false.
     * @param attachedSurfaces                  the existing surfaces.
     * @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output
     *                                          sizes map.
     * @return the suggested stream specifications, which is a mapping from UseCaseConfig to the
     * suggested stream specification.
     * @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
     *                                  found. This may be due to no available output size or no
     *                                  available surface combination.
     */
    @NonNull
    Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecifications(
            boolean isConcurrentCameraModeOn,
            @NonNull List<AttachedSurfaceInfo> attachedSurfaces,
            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
        // Refresh Preview Size based on current display configurations.
        refreshPreviewSize();
        List<SurfaceConfig> surfaceConfigs = new ArrayList<>();
        for (AttachedSurfaceInfo attachedSurface : attachedSurfaces) {
            surfaceConfigs.add(attachedSurface.getSurfaceConfig());
        }

        List<UseCaseConfig<?>> newUseCaseConfigs = new ArrayList<>(
                newUseCaseConfigsSupportedSizeMap.keySet());
        // Use the small size (640x480) for new use cases to check whether there is any possible
        // supported combination first
        for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
            Size maxOutputSizeForConcurrentMode = isConcurrentCameraModeOn
                    ? getMaxOutputSizeByFormat(useCaseConfig.getInputFormat()) : null;
            surfaceConfigs.add(
                    SurfaceConfig.transformSurfaceConfig(
                            isConcurrentCameraModeOn,
                            useCaseConfig.getInputFormat(),
                            new Size(640, 480),
                            mSurfaceSizeDefinition,
                            maxOutputSizeForConcurrentMode));
        }

        if (!checkSupported(isConcurrentCameraModeOn, surfaceConfigs)) {
            throw new IllegalArgumentException(
                    "No supported surface combination is found for camera device - Id : "
                            + mCameraId + ".  May be attempting to bind too many use cases. "
                            + "Existing surfaces: " + attachedSurfaces + " New configs: "
                            + newUseCaseConfigs);
        }

        Range<Integer> targetFramerateForConfig = null;
        int existingSurfaceFrameRateCeiling = Integer.MAX_VALUE;

        for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
            // init target fps range for new configs from existing surfaces
            targetFramerateForConfig = getUpdatedTargetFramerate(
                    attachedSurfaceInfo.getTargetFrameRate(),
                    targetFramerateForConfig);
            //get the fps ceiling for existing surfaces
            existingSurfaceFrameRateCeiling = getUpdatedMaximumFps(
                    existingSurfaceFrameRateCeiling,
                    attachedSurfaceInfo.getImageFormat(), attachedSurfaceInfo.getSize());
        }

        // Get the index order list by the use case priority for finding stream configuration
        List<Integer> useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCaseConfigs);
        List<List<Size>> supportedOutputSizesList = new ArrayList<>();

        // Collect supported output sizes for all use cases
        for (Integer index : useCasesPriorityOrder) {
            UseCaseConfig<?> useCaseConfig = newUseCaseConfigs.get(index);
            List<Size> supportedOutputSizes = newUseCaseConfigsSupportedSizeMap.get(useCaseConfig);
            supportedOutputSizes = mResolutionCorrector.insertOrPrioritize(
                    SurfaceConfig.getConfigType(useCaseConfig.getInputFormat()),
                    supportedOutputSizes);
            supportedOutputSizesList.add(supportedOutputSizes);
        }

        // Get all possible size arrangements
        List<List<Size>> allPossibleSizeArrangements =
                getAllPossibleSizeArrangements(
                        supportedOutputSizesList);

        // update target fps for new configs using new use cases' priority order
        for (Integer index : useCasesPriorityOrder) {
            targetFramerateForConfig =
                    getUpdatedTargetFramerate(
                            newUseCaseConfigs.get(index).getTargetFramerate(null),
                            targetFramerateForConfig);
        }

        Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap;
        List<Size> savedSizes = null;
        int savedConfigMaxFps = Integer.MAX_VALUE;

        // Transform use cases to SurfaceConfig list and find the first (best) workable combination
        for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
            // Attach SurfaceConfig of original use cases since it will impact the new use cases
            List<SurfaceConfig> surfaceConfigList = new ArrayList<>();
            int currentConfigFramerateCeiling = existingSurfaceFrameRateCeiling;
            boolean isConfigFrameRateAcceptable = true;

            for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
                surfaceConfigList.add(attachedSurfaceInfo.getSurfaceConfig());
            }

            // Attach SurfaceConfig of new use cases
            for (int i = 0; i < possibleSizeList.size(); i++) {
                Size size = possibleSizeList.get(i);
                UseCaseConfig<?> newUseCase =
                        newUseCaseConfigs.get(useCasesPriorityOrder.get(i));
                // add new use case/size config to list of surfaces
                Size maxOutputSizeForConcurrentMode = isConcurrentCameraModeOn
                        ? getMaxOutputSizeByFormat(newUseCase.getInputFormat()) : null;
                surfaceConfigList.add(
                        SurfaceConfig.transformSurfaceConfig(
                                isConcurrentCameraModeOn,
                                newUseCase.getInputFormat(),
                                size,
                                mSurfaceSizeDefinition,
                                maxOutputSizeForConcurrentMode));

                // get the maximum fps of the new surface and update the maximum fps of the
                // proposed configuration
                currentConfigFramerateCeiling = getUpdatedMaximumFps(
                        currentConfigFramerateCeiling,
                        newUseCase.getInputFormat(),
                        size);
            }
            if (targetFramerateForConfig != null) {
                if (existingSurfaceFrameRateCeiling > currentConfigFramerateCeiling
                        && currentConfigFramerateCeiling < targetFramerateForConfig.getLower()) {
                    // if the max fps before adding new use cases supports our target fps range
                    // BUT the max fps of the new configuration is below
                    // our target fps range, we'll want to check the next configuration until we
                    // get one that supports our target FPS
                    isConfigFrameRateAcceptable = false;
                }
            }

            // only change the saved config if you get another that has a better max fps
            if (checkSupported(isConcurrentCameraModeOn, surfaceConfigList)) {
                // if the config is supported by the device but doesn't meet the target framerate,
                // save the config
                if (savedConfigMaxFps == Integer.MAX_VALUE) {
                    savedConfigMaxFps = currentConfigFramerateCeiling;
                    savedSizes = possibleSizeList;
                } else if (savedConfigMaxFps < currentConfigFramerateCeiling) {
                    // only change the saved config if the max fps is better
                    savedConfigMaxFps = currentConfigFramerateCeiling;
                    savedSizes = possibleSizeList;
                }

                // if we have a configuration where the max fps is acceptable for our target, break
                if (isConfigFrameRateAcceptable) {
                    savedConfigMaxFps = currentConfigFramerateCeiling;
                    savedSizes = possibleSizeList;
                    break;
                }
            }
        }

        // Map the saved supported SurfaceConfig combination
        if (savedSizes != null) {
            Range<Integer> targetFramerateForDevice = null;
            if (targetFramerateForConfig != null) {
                targetFramerateForDevice =
                        getClosestSupportedDeviceFrameRate(targetFramerateForConfig,
                                savedConfigMaxFps);
            }
            suggestedStreamSpecMap = new HashMap<>();
            for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
                suggestedStreamSpecMap.put(
                        useCaseConfig,
                        targetFramerateForDevice != null
                                ? StreamSpec.builder(savedSizes.get(useCasesPriorityOrder.indexOf(
                                        newUseCaseConfigs.indexOf(useCaseConfig))))
                                .setExpectedFrameRateRange(targetFramerateForDevice).build()
                                : StreamSpec.builder(savedSizes.get(useCasesPriorityOrder.indexOf(
                                        newUseCaseConfigs.indexOf(useCaseConfig)))).build());
            }
        } else {
            throw new IllegalArgumentException(
                    "No supported surface combination is found for camera device - Id : "
                            + mCameraId + " and Hardware level: " + mHardwareLevel
                            + ". May be the specified resolution is too large and not supported."
                            + " Existing surfaces: " + attachedSurfaces
                            + " New configs: " + newUseCaseConfigs);
        }
        return suggestedStreamSpecMap;
    }

    private List<Integer> getUseCasesPriorityOrder(List<UseCaseConfig<?>> newUseCaseConfigs) {
        List<Integer> priorityOrder = new ArrayList<>();

        /*
         * Once the stream resource is occupied by one use case, it will impact the other use cases.
         * Therefore, we need to define the priority for stream resource usage. For the use cases
         * with the higher priority, we will try to find the best one for them in priority as
         * possible.
         */
        List<Integer> priorityValueList = new ArrayList<>();

        for (UseCaseConfig<?> config : newUseCaseConfigs) {
            int priority = config.getSurfaceOccupancyPriority(0);
            if (!priorityValueList.contains(priority)) {
                priorityValueList.add(priority);
            }
        }

        Collections.sort(priorityValueList);
        // Reverse the priority value list in descending order since larger value means higher
        // priority
        Collections.reverse(priorityValueList);

        for (int priorityValue : priorityValueList) {
            for (UseCaseConfig<?> config : newUseCaseConfigs) {
                if (priorityValue == config.getSurfaceOccupancyPriority(0)) {
                    priorityOrder.add(newUseCaseConfigs.indexOf(config));
                }
            }
        }

        return priorityOrder;
    }

    private List<List<Size>> getAllPossibleSizeArrangements(
            List<List<Size>> supportedOutputSizesList) {
        int totalArrangementsCount = 1;

        for (List<Size> supportedOutputSizes : supportedOutputSizesList) {
            totalArrangementsCount *= supportedOutputSizes.size();
        }

        // If totalArrangementsCount is 0 means that there may some problem to get
        // supportedOutputSizes
        // for some use case
        if (totalArrangementsCount == 0) {
            throw new IllegalArgumentException("Failed to find supported resolutions.");
        }

        List<List<Size>> allPossibleSizeArrangements = new ArrayList<>();

        // Initialize allPossibleSizeArrangements for the following operations
        for (int i = 0; i < totalArrangementsCount; i++) {
            List<Size> sizeList = new ArrayList<>();
            allPossibleSizeArrangements.add(sizeList);
        }

        /*
         * Try to list out all possible arrangements by attaching all possible size of each column
         * in sequence. We have generated supportedOutputSizesList by the priority order for
         * different use cases. And the supported outputs sizes for each use case are also arranged
         * from large to small. Therefore, the earlier size arrangement in the result list will be
         * the better one to choose if finally it won't exceed the camera device's stream
         * combination capability.
         */
        int currentRunCount = totalArrangementsCount;
        int nextRunCount = currentRunCount / supportedOutputSizesList.get(0).size();

        for (int currentIndex = 0; currentIndex < supportedOutputSizesList.size(); currentIndex++) {
            List<Size> supportedOutputSizes = supportedOutputSizesList.get(currentIndex);
            for (int i = 0; i < totalArrangementsCount; i++) {
                List<Size> surfaceConfigList = allPossibleSizeArrangements.get(i);

                surfaceConfigList.add(
                        supportedOutputSizes.get((i % currentRunCount) / nextRunCount));
            }

            if (currentIndex < supportedOutputSizesList.size() - 1) {
                currentRunCount = nextRunCount;
                nextRunCount =
                        currentRunCount / supportedOutputSizesList.get(currentIndex + 1).size();
            }
        }

        return allPossibleSizeArrangements;
    }

    /**
     * Get max supported output size for specific image format
     *
     * @param imageFormat the image format info
     * @return the max supported output size for the image format
     */
    Size getMaxOutputSizeByFormat(int imageFormat) {
        // Needs to retrieve the output size from the original stream configuration map without
        // quirks applied.
        StreamConfigurationMap map =
                mCharacteristics.getStreamConfigurationMapCompat().toStreamConfigurationMap();
        Size[] outputSizes;
        if (imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
            // This is a little tricky that 0x22 that is internal defined in
            // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
            // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
            // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
            // retrieve the output sizes information.
            outputSizes = map.getOutputSizes(SurfaceTexture.class);
        } else {
            outputSizes = map.getOutputSizes(imageFormat);
        }
        return Collections.max(Arrays.asList(outputSizes), new CompareSizesByArea());
    }

    private void generateSupportedCombinationList() {
        mSurfaceCombinations.addAll(
                GuaranteedConfigurationsUtil.generateSupportedCombinationList(mHardwareLevel,
                        mIsRawSupported, mIsBurstCaptureSupported));

        mSurfaceCombinations.addAll(
                mExtraSupportedSurfaceCombinationsContainer.get(mCameraId, mHardwareLevel));
    }

    private void generateConcurrentSupportedCombinationList() {
        mConcurrentSurfaceCombinations.addAll(
                GuaranteedConfigurationsUtil.getConcurrentSupportedCombinationList());
    }

    private void checkCustomization() {
        // TODO(b/119466260): Integrate found feasible stream combinations into supported list
    }

    // Utility classes and methods:
    // *********************************************************************************************

    private void generateSurfaceSizeDefinition() {
        Size analysisSize = new Size(640, 480);
        Size s720p = new Size(1280, 720);
        Size previewSize = mDisplayInfoManager.getPreviewSize();
        Size s1440p = new Size(1920, 1440);
        Size recordSize = getRecordSize();
        mSurfaceSizeDefinition =
                SurfaceSizeDefinition.create(analysisSize, s720p, previewSize, s1440p, recordSize);
    }

    private void refreshPreviewSize() {
        mDisplayInfoManager.refresh();
        if (mSurfaceSizeDefinition == null) {
            generateSurfaceSizeDefinition();
        } else {
            Size previewSize = mDisplayInfoManager.getPreviewSize();
            mSurfaceSizeDefinition = SurfaceSizeDefinition.create(
                    mSurfaceSizeDefinition.getAnalysisSize(),
                    mSurfaceSizeDefinition.getS720pSize(),
                    previewSize,
                    mSurfaceSizeDefinition.getS1440pSize(),
                    mSurfaceSizeDefinition.getRecordSize());
        }
    }

    /**
     * RECORD refers to the camera device's maximum supported recording resolution, as determined by
     * CamcorderProfile.
     */
    @NonNull
    private Size getRecordSize() {
        int cameraId;

        try {
            cameraId = Integer.parseInt(mCameraId);
        } catch (NumberFormatException e) {
            // The camera Id is not an integer because the camera may be a removable device. Use
            // StreamConfigurationMap to determine the RECORD size.
            return getRecordSizeFromStreamConfigurationMap();
        }

        CamcorderProfile profile = null;

        if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_HIGH);
        }

        if (profile != null) {
            return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
        }

        return getRecordSizeByHasProfile(cameraId);
    }

    /**
     * Return the maximum supported video size for cameras using data from the stream
     * configuration map.
     *
     * @return Maximum supported video size.
     */
    @NonNull
    private Size getRecordSizeFromStreamConfigurationMap() {
        // Determining the record size needs to retrieve the output size from the original stream
        // configuration map without quirks applied.
        StreamConfigurationMapCompat mapCompat = mCharacteristics.getStreamConfigurationMapCompat();
        Size[] videoSizeArr = mapCompat.toStreamConfigurationMap().getOutputSizes(
                MediaRecorder.class);

        if (videoSizeArr == null) {
            return RESOLUTION_480P;
        }

        Arrays.sort(videoSizeArr, new CompareSizesByArea(true));

        for (Size size : videoSizeArr) {
            // Returns the largest supported size under 1080P
            if (size.getWidth() <= RESOLUTION_1080P.getWidth()
                    && size.getHeight() <= RESOLUTION_1080P.getHeight()) {
                return size;
            }
        }

        return RESOLUTION_480P;
    }

    /**
     * Return the maximum supported video size for cameras by
     * {@link CamcorderProfile#hasProfile(int, int)}.
     *
     * @return Maximum supported video size.
     */
    @NonNull
    private Size getRecordSizeByHasProfile(int cameraId) {
        Size recordSize = RESOLUTION_480P;
        CamcorderProfile profile = null;

        // Check whether 4KDCI, 2160P, 2K, 1080P, 720P, 480P (sorted by size) are supported by
        // CamcorderProfile
        if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_4KDCI)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_4KDCI);
        } else if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_2160P);
        } else if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_2K)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_2K);
        } else if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_1080P);
        } else if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_720P);
        } else if (mCamcorderProfileHelper.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
            profile = mCamcorderProfileHelper.get(cameraId, CamcorderProfile.QUALITY_480P);
        }

        if (profile != null) {
            recordSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight);
        }

        return recordSize;
    }
}