SupportedOutputSizesSorterLegacy.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.core.internal;

import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
import static androidx.camera.core.internal.SupportedOutputSizesSorter.getResolutionListGroupingAspectRatioKeys;
import static androidx.camera.core.internal.SupportedOutputSizesSorter.groupSizesByAspectRatio;
import static androidx.camera.core.internal.SupportedOutputSizesSorter.sortSupportedSizesByFallbackRuleClosestHigherThenLower;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_ZERO;
import static androidx.camera.core.internal.utils.SizeUtil.getArea;

import android.hardware.camera2.CameraCharacteristics;
import android.util.Rational;
import android.util.Size;
import android.view.Surface;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.utils.AspectRatioUtil;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.CompareSizesByArea;

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

/**
 * A class used to sort the supported output sizes according to the legacy use case configs
 */
@RequiresApi(21)
class SupportedOutputSizesSorterLegacy {
    private static final String TAG = "SupportedOutputSizesCollector";
    private final int mSensorOrientation;
    private final int mLensFacing;
    private final Rational mFullFovRatio;
    private final boolean mIsSensorLandscapeResolution;

    SupportedOutputSizesSorterLegacy(@NonNull CameraInfoInternal cameraInfoInternal,
            @Nullable Rational fullFovRatio) {
        mSensorOrientation = cameraInfoInternal.getSensorRotationDegrees();
        mLensFacing = cameraInfoInternal.getLensFacing();
        mFullFovRatio = fullFovRatio;
        // Determines the sensor resolution orientation info by the full FOV ratio.
        mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
                >= mFullFovRatio.getDenominator() : true;
    }

    /**
     * Sorts the resolution candidate list by the following steps:
     *
     * 1. Filters out the candidate list according to the mini and max resolution.
     * 2. Sorts the candidate list according to legacy target aspect ratio or resolution settings.
     */
    @NonNull
    List<Size> sortSupportedOutputSizes(
            @NonNull List<Size> resolutionCandidateList,
            @NonNull UseCaseConfig<?> useCaseConfig) {
        if (resolutionCandidateList.isEmpty()) {
            return resolutionCandidateList;
        }

        List<Size> descendingSizeList = new ArrayList<>(resolutionCandidateList);

        // Sort the result sizes. The Comparator result must be reversed to have a descending
        // order result.
        Collections.sort(descendingSizeList, new CompareSizesByArea(true));

        List<Size> filteredSizeList = new ArrayList<>();
        ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
        Size maxSize = imageOutputConfig.getMaxResolution(null);
        Size maxSupportedOutputSize = descendingSizeList.get(0);

        // Set maxSize as the max resolution setting or the max supported output size for the
        // image format, whichever is smaller.
        if (maxSize == null || getArea(maxSupportedOutputSize) < getArea(maxSize)) {
            maxSize = maxSupportedOutputSize;
        }

        Size targetSize = getTargetSize(imageOutputConfig);
        Size minSize = RESOLUTION_VGA;
        int defaultSizeArea = getArea(RESOLUTION_VGA);
        int maxSizeArea = getArea(maxSize);
        // When maxSize is smaller than 640x480, set minSize as 0x0. It means the min size bound
        // will be ignored. Otherwise, set the minimal size according to min(DEFAULT_SIZE,
        // TARGET_RESOLUTION).
        if (maxSizeArea < defaultSizeArea) {
            minSize = RESOLUTION_ZERO;
        } else if (targetSize != null && getArea(targetSize) < defaultSizeArea) {
            minSize = targetSize;
        }

        // Filter out the ones that exceed the maximum size and the minimum size. The output
        // sizes candidates list won't have duplicated items.
        for (Size outputSize : descendingSizeList) {
            if (getArea(outputSize) <= getArea(maxSize) && getArea(outputSize) >= getArea(minSize)
                    && !filteredSizeList.contains(outputSize)) {
                filteredSizeList.add(outputSize);
            }
        }

        if (filteredSizeList.isEmpty()) {
            throw new IllegalArgumentException(
                    "All supported output sizes are filtered out according to current resolution "
                            + "selection settings. \nminSize = "
                            + minSize + "\nmaxSize = " + maxSize + "\ninitial size list: "
                            + descendingSizeList);
        }

        Rational aspectRatio = getTargetAspectRatioByLegacyApi(imageOutputConfig, filteredSizeList);

        // Check the default resolution if the target resolution is not set
        targetSize = targetSize == null ? imageOutputConfig.getDefaultResolution(null) : targetSize;

        List<Size> resultSizeList = new ArrayList<>();
        Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();

        if (aspectRatio == null) {
            // If no target aspect ratio is set, all sizes can be added to the result list
            // directly. No need to sort again since the source list has been sorted previously.
            resultSizeList.addAll(filteredSizeList);

            // If the target resolution is set, use it to sort the sizes list.
            if (targetSize != null) {
                sortSupportedSizesByFallbackRuleClosestHigherThenLower(resultSizeList, targetSize,
                        true);
            }
        } else {
            // Rearrange the supported size to put the ones with the same aspect ratio in the front
            // of the list and put others in the end from large to small. Some low end devices may
            // not able to get an supported resolution that match the preferred aspect ratio.

            // Group output sizes by aspect ratio.
            aspectRatioSizeListMap = groupSizesByAspectRatio(filteredSizeList);

            // If the target resolution is set, sort the sizes against it.
            if (targetSize != null) {
                for (Rational key : aspectRatioSizeListMap.keySet()) {
                    sortSupportedSizesByFallbackRuleClosestHigherThenLower(
                            aspectRatioSizeListMap.get(key), targetSize, true);
                }
            }

            // Sort the aspect ratio key set by the target aspect ratio.
            List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
            Collections.sort(aspectRatios,
                    new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
                            aspectRatio, mFullFovRatio));

            // Put available sizes into final result list by aspect ratio distance to target ratio.
            for (Rational rational : aspectRatios) {
                for (Size size : aspectRatioSizeListMap.get(rational)) {
                    // A size may exist in multiple groups in mod16 condition. Keep only one in
                    // the final list.
                    if (!resultSizeList.contains(size)) {
                        resultSizeList.add(size);
                    }
                }
            }
        }

        return resultSizeList;
    }

    /**
     * Returns the target aspect ratio rational value according to the legacy API settings.
     */
    private Rational getTargetAspectRatioByLegacyApi(@NonNull ImageOutputConfig imageOutputConfig,
            @NonNull List<Size> resolutionCandidateList) {
        Rational outputRatio = null;

        if (imageOutputConfig.hasTargetAspectRatio()) {
            @AspectRatio.Ratio int aspectRatio = imageOutputConfig.getTargetAspectRatio();
            outputRatio = SupportedOutputSizesSorter.getTargetAspectRatioRationalValue(aspectRatio,
                    mIsSensorLandscapeResolution);
        } else {
            // The legacy resolution API will use the aspect ratio of the target size to
            // be the fallback target aspect ratio value when the use case has no target
            // aspect ratio setting.
            Size targetSize = getTargetSize(imageOutputConfig);
            if (targetSize != null) {
                outputRatio = getAspectRatioGroupKeyOfTargetSize(targetSize,
                        resolutionCandidateList);
            }
        }

        return outputRatio;
    }

    @Nullable
    private Size getTargetSize(@NonNull ImageOutputConfig imageOutputConfig) {
        int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
        // Calibrate targetSize by the target rotation value.
        Size targetSize = imageOutputConfig.getTargetResolution(null);
        targetSize = flipSizeByRotation(targetSize, targetRotation, mLensFacing,
                mSensorOrientation);
        return targetSize;
    }

    /**
     * Returns the aspect ratio group key of the target size when grouping the input resolution
     * candidate list.
     *
     * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
     * also need to consider the mod 16 factor to find which aspect ratio of group the target size
     * might be put in. So that sizes of the group will be selected to use in the highest priority.
     */
    @Nullable
    private static Rational getAspectRatioGroupKeyOfTargetSize(@Nullable Size targetSize,
            @NonNull List<Size> resolutionCandidateList) {
        if (targetSize == null) {
            return null;
        }

        List<Rational> aspectRatios = getResolutionListGroupingAspectRatioKeys(
                resolutionCandidateList);

        for (Rational aspectRatio : aspectRatios) {
            if (hasMatchingAspectRatio(targetSize, aspectRatio)) {
                return aspectRatio;
            }
        }

        return new Rational(targetSize.getWidth(), targetSize.getHeight());
    }

    // Use target rotation to calibrate the size.
    @Nullable
    private static Size flipSizeByRotation(@Nullable Size size, int targetRotation, int lensFacing,
            int sensorOrientation) {
        Size outputSize = size;
        // Calibrates the size with the display and sensor rotation degrees values.
        if (size != null && isRotationNeeded(targetRotation, lensFacing, sensorOrientation)) {
            outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
        }
        return outputSize;
    }

    private static boolean isRotationNeeded(int targetRotation, int lensFacing,
            int sensorOrientation) {
        int relativeRotationDegrees =
                CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);

        // Currently this assumes that a back-facing camera is always opposite to the screen.
        // This may not be the case for all devices, so in the future we may need to handle that
        // scenario.
        boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;

        int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
                relativeRotationDegrees,
                sensorOrientation,
                isOppositeFacingScreen);
        return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
    }
}