Merge "Implement ResolutionMerger" into androidx-main
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
index 45efcbd..9498690 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -74,7 +74,7 @@
private final Rational mFullFovRatio;
private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
- SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
+ public SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
@Nullable Size activeArraySize) {
mCameraInfoInternal = cameraInfoInternal;
mSensorOrientation = mCameraInfoInternal.getSensorRotationDegrees();
@@ -120,7 +120,7 @@
* will be sorted according to the legacy resolution API settings and logic.
*/
@NonNull
- List<Size> getSortedSupportedOutputSizes(@NonNull UseCaseConfig<?> useCaseConfig) {
+ public List<Size> getSortedSupportedOutputSizes(@NonNull UseCaseConfig<?> useCaseConfig) {
ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
List<Size> customOrderedResolutions = imageOutputConfig.getCustomOrderedResolutions(null);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java
new file mode 100644
index 0000000..f97ebed
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java
@@ -0,0 +1,478 @@
+/*
+ * 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.streamsharing;
+
+
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+
+import static java.lang.Math.sqrt;
+
+import android.util.Pair;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.core.internal.SupportedOutputSizesSorter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class for calculating parent resolutions based on the children's configs.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ResolutionsMerger {
+
+ private static final String TAG = "ResolutionsMerger";
+ // The width to height ratio that has same area when cropping into 4:3 and 16:9.
+ private static final double SAME_AREA_WIDTH_HEIGHT_RATIO = sqrt(4.0 / 3.0 * 16.0 / 9.0);
+
+ @NonNull
+ private final Rational mSensorAspectRatio;
+ @NonNull
+ private final Rational mFallbackAspectRatio;
+ @NonNull
+ private final Set<UseCaseConfig<?>> mChildrenConfigs;
+ @NonNull
+ private final SupportedOutputSizesSorter mSizeSorter;
+ @NonNull
+ private final List<Size> mCameraSupportedSizes;
+ @NonNull
+ private final Map<UseCaseConfig<?>, List<Size>> mChildSizesCache = new HashMap<>();
+
+ ResolutionsMerger(@NonNull CameraInternal cameraInternal,
+ @NonNull Set<UseCaseConfig<?>> childrenConfigs) {
+ this(rectToSize(cameraInternal.getCameraControlInternal().getSensorRect()),
+ cameraInternal.getCameraInfoInternal(), childrenConfigs);
+ }
+
+ private ResolutionsMerger(@NonNull Size sensorSize, @NonNull CameraInfoInternal cameraInfo,
+ @NonNull Set<UseCaseConfig<?>> childrenConfigs) {
+ this(sensorSize, childrenConfigs, new SupportedOutputSizesSorter(cameraInfo, sensorSize),
+ cameraInfo.getSupportedResolutions(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+ }
+
+ @VisibleForTesting
+ ResolutionsMerger(@NonNull Size sensorSize, @NonNull Set<UseCaseConfig<?>> childrenConfigs,
+ @NonNull SupportedOutputSizesSorter supportedOutputSizesSorter,
+ @NonNull List<Size> cameraSupportedResolutions) {
+ mSensorAspectRatio = getSensorAspectRatio(sensorSize);
+ mFallbackAspectRatio = getFallbackAspectRatio(mSensorAspectRatio);
+ mChildrenConfigs = childrenConfigs;
+ mSizeSorter = supportedOutputSizesSorter;
+ mCameraSupportedSizes = cameraSupportedResolutions;
+ }
+
+ /**
+ * Returns a list of {@link Surface} resolution sorted by priority.
+ *
+ * <p> This method calculates the resolution for the parent {@link StreamSharing} based on 1)
+ * the supported PRIV resolutions, 2) the sensor size and 3) the children's configs.
+ */
+ @NonNull
+ List<Size> getMergedResolutions(@NonNull MutableConfig parentConfig) {
+ List<Size> candidateSizes = mCameraSupportedSizes;
+
+ // Use parent config's supported resolutions when it is set (e.g. Extensions may have
+ // its limitations on resolutions).
+ List<Pair<Integer, Size[]>> parentSupportedSizesMap =
+ parentConfig.retrieveOption(OPTION_SUPPORTED_RESOLUTIONS, null);
+ if (parentSupportedSizesMap != null) {
+ candidateSizes = getSupportedPrivResolutions(parentSupportedSizesMap);
+ }
+
+ return mergeChildrenResolutions(candidateSizes);
+ }
+
+ /**
+ * Returns the preferred child size with considering parent size and child's configuration.
+ *
+ * <p>Returns the first size in the child's ordered size list that can be cropped from {@code
+ * parentSize} without upscaling it and causing double-cropping, or {@code parentSize} if no
+ * matching is found.
+ *
+ * <p>Notes that the input {@code childConfig} is expected to be one of the values that use to
+ * construct the {@link ResolutionsMerger}, if not an IllegalArgumentException will be thrown.
+ */
+ @NonNull
+ Size getPreferredChildSize(@NonNull Size parentSize, @NonNull UseCaseConfig<?> childConfig) {
+ boolean isParentCropped = !isSensorAspectRatio(parentSize);
+
+ List<Size> candidateChildSizes = getSortedChildSizes(childConfig);
+ for (Size childSize : candidateChildSizes) {
+ // Skip child sizes that need another cropping when parent is already cropped.
+ if (isParentCropped) {
+ boolean needAnotherCropping = !(isFallbackAspectRatio(parentSize)
+ && isFallbackAspectRatio(childSize));
+ if (needAnotherCropping) {
+ continue;
+ }
+ }
+
+ if (!hasUpscaling(childSize, parentSize)) {
+ return childSize;
+ }
+ }
+
+ return parentSize;
+ }
+
+ @NonNull
+ private List<Size> mergeChildrenResolutions(@NonNull List<Size> candidateParentResolutions) {
+ // The following sequence of parent resolution selection is used to prevent double-cropping
+ // from happening:
+ // 1. Add sensor aspect-ratio resolutions, which will not cause double-cropping when the
+ // child resolution is in any aspect-ratio. This is to provide parent resolution that can
+ // be accepted by children in general cases.
+ // 2. Add fallback aspect-ratio resolutions, which will not cause double-cropping only when
+ // the child resolution is in fallback aspect-ratio.
+ List<Size> result = new ArrayList<>();
+
+ // Add resolutions for sensor aspect-ratio.
+ if (needToAddSensorResolutions()) {
+ result.addAll(mergeChildrenResolutionsByAspectRatio(mSensorAspectRatio,
+ candidateParentResolutions));
+ }
+
+ // Add resolutions for fallback aspect-ratio if needed.
+ if (needToAddFallbackResolutions()) {
+ result.addAll(mergeChildrenResolutionsByAspectRatio(mFallbackAspectRatio,
+ candidateParentResolutions));
+ }
+
+ // TODO(b/315098647): When the resulting parent resolution list is empty, consider adding
+ // resolutions that are neither 4:3 nor 16:9, but have a high overlap area (e.g. 80%)
+ // compared to the sensor size, which do not cause severe reduction of FOV, to prevent
+ // binding failures in some edge cases.
+
+ Logger.d(TAG, "Parent resolutions: " + result);
+
+ return result;
+ }
+
+ private List<Size> mergeChildrenResolutionsByAspectRatio(@NonNull Rational aspectRatio,
+ @NonNull List<Size> candidateParentResolutions) {
+ List<Size> candidates = filterResolutionsByAspectRatio(aspectRatio,
+ candidateParentResolutions);
+ sortInDescendingOrder(candidates);
+
+ // Filter resolutions that are too small and track resolutions that might be too large.
+ Set<Size> sizesTooLarge = new HashSet<>(candidates);
+ for (UseCaseConfig<?> childConfig : mChildrenConfigs) {
+ List<Size> childSizes = getSortedChildSizes(childConfig);
+ candidates = filterOutParentSizeThatIsTooSmall(childSizes, candidates);
+ sizesTooLarge.retainAll(getParentSizesThatAreTooLarge(childSizes, candidates));
+ }
+
+ // Filter out sizes that are too large.
+ List<Size> result = new ArrayList<>();
+ for (Size candidate : candidates) {
+ if (!sizesTooLarge.contains(candidate)) {
+ result.add(candidate);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Gets child sizes sorted by {@link SupportedOutputSizesSorter}.
+ *
+ * <p>Notes that the input {@code childConfig} is expected to be one of the values that use to
+ * construct the {@link ResolutionsMerger}, if not an IllegalArgumentException will be thrown.
+ *
+ * <p>When {@link SupportedOutputSizesSorter#getSortedSupportedOutputSizes(UseCaseConfig)}
+ * returns an empty list, which means no child required resolution is supported, an
+ * IllegalArgumentException will also be thrown.
+ */
+ @NonNull
+ private List<Size> getSortedChildSizes(@NonNull UseCaseConfig<?> childConfig) {
+ if (!mChildrenConfigs.contains(childConfig)) {
+ throw new IllegalArgumentException("Invalid child config: " + childConfig);
+ }
+
+ // Since getSortedSupportedOutputSizes() might be time consuming, use caching to improve
+ // the performance.
+ if (mChildSizesCache.containsKey(childConfig)) {
+ return Objects.requireNonNull(mChildSizesCache.get(childConfig));
+ }
+
+ List<Size> childSizes = mSizeSorter.getSortedSupportedOutputSizes(childConfig);
+ if (childSizes.isEmpty()) {
+ throw new IllegalArgumentException(
+ "No supported resolution for child config: " + childConfig);
+ }
+ mChildSizesCache.put(childConfig, childSizes);
+
+ return childSizes;
+ }
+
+ private boolean needToAddSensorResolutions() {
+ // Need to add sensor resolutions if any required resolution is not fallback aspect-ratio.
+ for (Size size : getChildrenRequiredResolutions()) {
+ if (!hasMatchingAspectRatio(size, mFallbackAspectRatio)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean needToAddFallbackResolutions() {
+ // Need to add fallback resolutions if any required resolution is fallback aspect-ratio.
+ for (Size size : getChildrenRequiredResolutions()) {
+ if (hasMatchingAspectRatio(size, mFallbackAspectRatio)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @NonNull
+ private Set<Size> getChildrenRequiredResolutions() {
+ Set<Size> result = new HashSet<>();
+ for (UseCaseConfig<?> childConfig : mChildrenConfigs) {
+ List<Size> childSizes = getSortedChildSizes(childConfig);
+ result.addAll(childSizes);
+ }
+
+ return result;
+ }
+
+ private boolean isSensorAspectRatio(@NonNull Size size) {
+ return hasMatchingAspectRatio(size, mSensorAspectRatio);
+ }
+
+ private boolean isFallbackAspectRatio(@NonNull Size size) {
+ return hasMatchingAspectRatio(size, mFallbackAspectRatio);
+ }
+
+ @NonNull
+ private static List<Size> getSupportedPrivResolutions(
+ @NonNull List<Pair<Integer, Size[]>> supportedResolutionsMap) {
+ for (Pair<Integer, Size[]> pair : supportedResolutionsMap) {
+ if (pair.first.equals(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)) {
+ return Arrays.asList(pair.second);
+ }
+ }
+
+ return new ArrayList<>();
+ }
+
+ /**
+ * Returns the aspect-ratio of 4:3 or 16:9 that is closer to the sensor size.
+ *
+ * <p>Parent resolutions with sensor aspect-ratio are considered to be non-cropped, so child
+ * resolution can have a different aspect-ratio than the parents without causing
+ * double-cropping.
+ */
+ @NonNull
+ private static Rational getSensorAspectRatio(@NonNull Size sensorSize) {
+ Rational result = findCloserAspectRatio(sensorSize);
+ Logger.d(TAG, "The closer aspect ratio to the sensor size (" + sensorSize + ") is "
+ + result + ".");
+
+ return result;
+ }
+
+ @NonNull
+ private static Rational findCloserAspectRatio(@NonNull Size size) {
+ double widthHeightRatio = size.getWidth() / (double) size.getHeight();
+
+ if (widthHeightRatio > SAME_AREA_WIDTH_HEIGHT_RATIO) {
+ return ASPECT_RATIO_16_9;
+ } else {
+ return ASPECT_RATIO_4_3;
+ }
+ }
+
+ /**
+ * Returns the aspect-ratio of 4:3 or 16:9 that is not the sensor aspect-ratio.
+ *
+ * <p>Parent resolutions with fallback aspect-ratio are considered to be cropped, so child
+ * resolution should not different to the parent or double-cropping will happen.
+ */
+ @NonNull
+ private static Rational getFallbackAspectRatio(@NonNull Rational sensorAspectRatio) {
+ if (sensorAspectRatio.equals(ASPECT_RATIO_4_3)) {
+ return ASPECT_RATIO_16_9;
+ } else if (sensorAspectRatio.equals(ASPECT_RATIO_16_9)) {
+ return ASPECT_RATIO_4_3;
+ } else {
+ throw new IllegalArgumentException("Invalid sensor aspect-ratio: " + sensorAspectRatio);
+ }
+ }
+
+ /**
+ * Sorts the input resolutions in descending order.
+ */
+ @VisibleForTesting
+ static void sortInDescendingOrder(@NonNull List<Size> resolutions) {
+ Collections.sort(resolutions, new CompareSizesByArea(true));
+ }
+
+ /**
+ * Returns a list of resolution that all resolutions are with the input aspect-ratio.
+ *
+ * <p>The order of the {@code resolutionsToFilter} will be preserved in the resulting list.
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<Size> filterResolutionsByAspectRatio(@NonNull Rational aspectRatio,
+ @NonNull List<Size> resolutionsToFilter) {
+ List<Size> result = new ArrayList<>();
+ for (Size resolution : resolutionsToFilter) {
+ if (hasMatchingAspectRatio(resolution, aspectRatio)) {
+ result.add(resolution);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Filters out the parent size that is too small with consider target children sizes.
+ *
+ * <p>A size is too small if it cannot find any child size that can be cropped out without
+ * upscaling.
+ *
+ * <p>The order of the {@code sortedParentSizes} will be preserved in the resulting list.
+ *
+ * <p>Assuming {@code sortedParentSizes} is sorted in descending order and all sizes have same
+ * aspect-ratio.
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<Size> filterOutParentSizeThatIsTooSmall(
+ @NonNull Collection<Size> childSizes, @NonNull List<Size> sortedParentSizes) {
+ int n = sortedParentSizes.size();
+
+ // Find the smallest parent size that can be cropped to at least one child size without
+ // upscaling by using binary search.
+ int lo = 0;
+ int hi = n - 1;
+ while (lo < hi) {
+ int mid = lo + (hi - lo + 1) / 2;
+ Size parentSize = sortedParentSizes.get(mid);
+ if (isAnyChildSizeCanBeCroppedOutWithoutUpscalingParent(childSizes, parentSize)) {
+ lo = mid;
+ } else {
+ hi = mid - 1;
+ }
+ }
+
+ // Add all parent sizes that can be cropped to at least one child size.
+ List<Size> result = new ArrayList<>();
+ for (int i = 0; i <= lo; i++) {
+ result.add(sortedParentSizes.get(i));
+ }
+
+ return result;
+ }
+
+ private static boolean isAnyChildSizeCanBeCroppedOutWithoutUpscalingParent(
+ @NonNull Collection<Size> childSizes, @NonNull Size parentSize) {
+ for (Size childSize : childSizes) {
+ if (!hasUpscaling(childSize, parentSize)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns resolutions that are too large with consider target children sizes.
+ *
+ * <p>A size is too large if there is another size smaller than that size and all children
+ * sizes can be cropped from that other size without upscaling.
+ *
+ * <p>The order of the {@code sortedParentSizes} will be preserved in the resulting list.
+ *
+ * <p>Assuming {@code sortedParentSizes} is sorted in descending order and all sizes have same
+ * aspect-ratio.
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<Size> getParentSizesThatAreTooLarge(@NonNull Collection<Size> childSizes,
+ @NonNull List<Size> sortedParentSizes) {
+ int n = sortedParentSizes.size();
+
+ // Find the smallest parent size that can be cropped to all child sizes without upscaling
+ // by using binary search.
+ int lo = 0;
+ int hi = n - 1;
+ while (lo < hi) {
+ int mid = lo + (hi - lo + 1) / 2;
+ Size parentSize = sortedParentSizes.get(mid);
+ if (isAllChildSizesCanBeCroppedOutWithoutUpscalingParent(childSizes, parentSize)) {
+ lo = mid;
+ } else {
+ hi = mid - 1;
+ }
+ }
+
+ // Add all parent sizes that can be cropped to all child sizes, except the smallest one.
+ List<Size> result = new ArrayList<>();
+ for (int i = 0; i < lo; i++) {
+ result.add(sortedParentSizes.get(i));
+ }
+
+ return result;
+ }
+
+ private static boolean isAllChildSizesCanBeCroppedOutWithoutUpscalingParent(
+ @NonNull Collection<Size> childSizes, @NonNull Size parentSize) {
+ for (Size childSize : childSizes) {
+ if (hasUpscaling(childSize, parentSize)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether the parent size needs upscaling to fill the child size.
+ */
+ @VisibleForTesting
+ static boolean hasUpscaling(@NonNull Size childSize, @NonNull Size parentSize) {
+ // Upscaling is needed if child size is larger than the parent.
+ return childSize.getHeight() > parentSize.getHeight()
+ || childSize.getWidth() > parentSize.getWidth();
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt
new file mode 100644
index 0000000..6caf41a
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt
@@ -0,0 +1,348 @@
+/*
+ * 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.streamsharing
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.internal.SupportedOutputSizesSorter
+import androidx.camera.core.streamsharing.ResolutionsMerger.filterOutParentSizeThatIsTooSmall
+import androidx.camera.core.streamsharing.ResolutionsMerger.filterResolutionsByAspectRatio
+import androidx.camera.core.streamsharing.ResolutionsMerger.getParentSizesThatAreTooLarge
+import androidx.camera.core.streamsharing.ResolutionsMerger.hasUpscaling
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [ResolutionsMerger].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionsMergerTest {
+
+ @Test(expected = IllegalArgumentException::class)
+ fun getMergedResolutions_whenNoSupportedChildSize_throwsException() {
+ // Arrange.
+ val sensorSize = SIZE_3264_2448 // 4:3
+ val config1 = createUseCaseConfig()
+ val config2 = createUseCaseConfig()
+ val childConfigs = setOf(config1, config2)
+ val candidateChildSizes1 = listOf(SIZE_1920_1080, SIZE_1280_720) // 16:9
+ val candidateChildSizes2 = emptyList<Size>() // no supported size
+ val sorter = FakeSupportedOutputSizesSorter(
+ mapOf(
+ config1 to candidateChildSizes1,
+ config2 to candidateChildSizes2
+ )
+ )
+ val merger = ResolutionsMerger(sensorSize, childConfigs, sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act & Assert.
+ val parentConfig = MutableOptionsBundle.create()
+ merger.getMergedResolutions(parentConfig)
+ }
+
+ @Test
+ fun getMergedResolutions_whenChildRequiresSensorAndNonSensorAspectRatio_canReturnCorrectly() {
+ // Arrange.
+ val sensorSize = SIZE_3264_2448 // 4:3
+ val config1 = createUseCaseConfig()
+ val config2 = createUseCaseConfig()
+ val childConfigs = setOf(config1, config2)
+ val candidateChildSizes1 = listOf(SIZE_1920_1080, SIZE_1280_720) // 16:9
+ val candidateChildSizes2 = listOf(SIZE_1280_960, SIZE_960_720, SIZE_640_480) // 4:3
+ val sorter = FakeSupportedOutputSizesSorter(
+ mapOf(
+ config1 to candidateChildSizes1,
+ config2 to candidateChildSizes2
+ )
+ )
+ val merger = ResolutionsMerger(sensorSize, childConfigs, sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act & Assert, should returns a list that concatenates 4:3 resolutions before 16:9
+ // resolutions and removes resolutions that are too large (no need for multiple resolutions
+ // that can be cropped to all child sizes) and too small (causing upscaling).
+ val parentConfig = MutableOptionsBundle.create()
+ assertThat(merger.getMergedResolutions(parentConfig)).containsExactly(
+ SIZE_1920_1440, SIZE_1280_960, SIZE_1920_1080, SIZE_1280_720
+ ).inOrder()
+ }
+
+ @Test
+ fun getMergedResolutions_whenChildRequiresOnlySensorAspectRatio_canReturnCorrectly() {
+ // Arrange.
+ val sensorSize = SIZE_3264_2448 // 4:3
+ val config1 = createUseCaseConfig()
+ val config2 = createUseCaseConfig()
+ val childConfigs = setOf(config1, config2)
+ val candidateChildSizes1 = listOf(SIZE_2560_1920, SIZE_1920_1440) // 4:3
+ val candidateChildSizes2 = listOf(SIZE_1280_960, SIZE_960_720) // 4:3
+ val sorter = FakeSupportedOutputSizesSorter(
+ mapOf(
+ config1 to candidateChildSizes1,
+ config2 to candidateChildSizes2
+ )
+ )
+ val merger = ResolutionsMerger(sensorSize, childConfigs, sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act & Assert, should returns a list of 4:3 resolutions and removes resolutions that are
+ // too large and too small.
+ val parentConfig = MutableOptionsBundle.create()
+ assertThat(merger.getMergedResolutions(parentConfig)).containsExactly(
+ SIZE_2560_1920, SIZE_1920_1440
+ ).inOrder()
+ }
+
+ @Test
+ fun getMergedResolutions_whenChildRequiresOnlyNonSensorAspectRatio_canReturnCorrectly() {
+ // Arrange.
+ val sensorSize = SIZE_3264_2448 // 4:3
+ val config1 = createUseCaseConfig()
+ val config2 = createUseCaseConfig()
+ val childConfigs = setOf(config1, config2)
+ val candidateChildSizes1 = listOf(SIZE_2560_1440, SIZE_1280_720) // 16:9
+ val candidateChildSizes2 = listOf(SIZE_1920_1080, SIZE_960_540) // 16:9
+ val sorter = FakeSupportedOutputSizesSorter(
+ mapOf(
+ config1 to candidateChildSizes1,
+ config2 to candidateChildSizes2
+ )
+ )
+ val merger = ResolutionsMerger(sensorSize, childConfigs, sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act & Assert, should returns a list of 16:9 resolutions and removes resolutions that are
+ // too large and too small.
+ val parentConfig = MutableOptionsBundle.create()
+ assertThat(merger.getMergedResolutions(parentConfig)).containsExactly(
+ SIZE_2560_1440, SIZE_1920_1080, SIZE_1280_720
+ ).inOrder()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun getPreferredChildSize_withConfigNotPassedToConstructor_throwsException() {
+ // Arrange.
+ val config = createUseCaseConfig()
+ val sorter = FakeSupportedOutputSizesSorter(mapOf(config to SIZES_16_9))
+ val merger = ResolutionsMerger(SENSOR_SIZE, setOf(config), sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act.
+ val useCaseConfigNotPassedToConstructor = createUseCaseConfig()
+ merger.getPreferredChildSize(SIZE_1920_1440, useCaseConfigNotPassedToConstructor)
+ }
+
+ @Test
+ fun getPreferredChildSize_whenParentSizeIsSensorAspectRatio_canReturnCorrectly() {
+ // Arrange.
+ val config = createUseCaseConfig()
+ val candidateChildSizes = listOf(SIZE_2560_1440, SIZE_1920_1080, SIZE_960_540) // 16:9
+ val sorter = FakeSupportedOutputSizesSorter(mapOf(config to candidateChildSizes))
+ val merger = ResolutionsMerger(SENSOR_SIZE, setOf(config), sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act & Assert, should returns the first child size that do not need upscale.
+ assertThat(merger.getPreferredChildSize(SIZE_1920_1440, config)).isEqualTo(SIZE_1920_1080)
+ assertThat(merger.getPreferredChildSize(SIZE_1280_960, config)).isEqualTo(SIZE_960_540)
+
+ // Act & Assert, should returns parent size when no matching.
+ assertThat(merger.getPreferredChildSize(SIZE_640_480, config)).isEqualTo(SIZE_640_480)
+ }
+
+ @Test
+ fun getPreferredChildSize_whenParentSizeIsNotSensorAspectRatio_canReturnCorrectly() {
+ // Arrange.
+ val config = createUseCaseConfig()
+ val candidateChildSizes = listOf(
+ // 4:3
+ SIZE_2560_1920,
+ SIZE_1920_1440,
+ SIZE_1280_960,
+ SIZE_960_720,
+ // 16:9
+ SIZE_1920_1080,
+ SIZE_960_540
+ )
+ val sorter = FakeSupportedOutputSizesSorter(mapOf(config to candidateChildSizes))
+ val merger = ResolutionsMerger(SENSOR_SIZE, setOf(config), sorter, CAMERA_SUPPORTED_SIZES)
+
+ // Act & Assert, should returns the first child size that do not need upscale and cause
+ // double-cropping.
+ assertThat(merger.getPreferredChildSize(SIZE_2560_1440, config)).isEqualTo(SIZE_1920_1080)
+ assertThat(merger.getPreferredChildSize(SIZE_1280_720, config)).isEqualTo(SIZE_960_540)
+
+ // Act & Assert, should returns parent size when no matching.
+ assertThat(merger.getPreferredChildSize(SIZE_192_108, config)).isEqualTo(SIZE_192_108)
+ }
+
+ @Test
+ fun filterResolutionsByAspectRatio_canFilter_4_3() {
+ val sizes = SIZES_4_3 + SIZES_16_9 + SIZES_OTHER_ASPECT_RATIO
+ assertThat(filterResolutionsByAspectRatio(ASPECT_RATIO_4_3, sizes)).containsExactly(
+ *SIZES_4_3.toTypedArray()
+ ).inOrder()
+ }
+
+ @Test
+ fun filterResolutionsByAspectRatio_canFilter_16_9() {
+ val sizes = SIZES_4_3 + SIZES_16_9 + SIZES_OTHER_ASPECT_RATIO
+ assertThat(filterResolutionsByAspectRatio(ASPECT_RATIO_16_9, sizes)).containsExactly(
+ *SIZES_16_9.toTypedArray()
+ ).inOrder()
+ }
+
+ @Test
+ fun filterOutParentSizeThatIsTooSmall_canFilterOutSmallSizes() {
+ val parentSizes = listOf(
+ SIZE_3264_2448,
+ SIZE_2560_1920,
+ SIZE_1920_1440,
+ SIZE_1280_960,
+ SIZE_960_720,
+ SIZE_640_480,
+ SIZE_320_240
+ )
+ val childSizes = setOf(SIZE_1920_1080, SIZE_1280_720, SIZE_960_540)
+ assertThat(filterOutParentSizeThatIsTooSmall(childSizes, parentSizes)).containsExactly(
+ SIZE_3264_2448,
+ SIZE_2560_1920,
+ SIZE_1920_1440,
+ SIZE_1280_960,
+ SIZE_960_720,
+ ).inOrder()
+ }
+
+ @Test
+ fun getParentSizesThatAreTooLarge_canReturnLargeSizes() {
+ val parentSizes = listOf(
+ SIZE_3264_2448,
+ SIZE_2560_1920,
+ SIZE_1920_1440,
+ SIZE_1280_960,
+ SIZE_960_720,
+ SIZE_640_480,
+ SIZE_320_240
+ )
+ val childSizes = setOf(SIZE_1920_1080, SIZE_1280_720, SIZE_960_540)
+ assertThat(getParentSizesThatAreTooLarge(childSizes, parentSizes)).containsExactly(
+ SIZE_3264_2448,
+ SIZE_2560_1920
+ ).inOrder()
+ }
+
+ @Test
+ fun hasUpscaling_return_false_whenTwoSizesAreEqualed() {
+ assertThat(hasUpscaling(SIZE_1280_960, SIZE_1280_960)).isFalse()
+ assertThat(hasUpscaling(SIZE_1920_1080, SIZE_1920_1080)).isFalse()
+ }
+
+ @Test
+ fun hasUpscaling_return_false_whenChildSizeIsSmaller() {
+ assertThat(hasUpscaling(SIZE_1280_960, SIZE_1920_1440)).isFalse()
+ assertThat(hasUpscaling(SIZE_1280_720, SIZE_1920_1080)).isFalse()
+ }
+
+ @Test
+ fun hasUpscaling_return_true_whenChildSizeIsLarger() {
+ assertThat(hasUpscaling(SIZE_1920_1440, SIZE_1280_960)).isTrue()
+ assertThat(hasUpscaling(SIZE_1920_1080, SIZE_1280_720)).isTrue()
+ }
+
+ @Test
+ fun hasUpscaling_return_true_whenChildSizeIsLargerOnWidth() {
+ assertThat(hasUpscaling(SIZE_1440_720, SIZE_1280_960)).isTrue()
+ assertThat(hasUpscaling(SIZE_1440_720, SIZE_1280_720)).isTrue()
+ }
+
+ @Test
+ fun hasUpscaling_return_true_whenChildSizeIsLargerOnHeight() {
+ assertThat(hasUpscaling(SIZE_800_800, SIZE_1280_720)).isTrue()
+ assertThat(hasUpscaling(SIZE_720_720, SIZE_960_540)).isTrue()
+ }
+
+ private fun createUseCaseConfig(): UseCaseConfig<*> {
+ return FakeUseCaseConfig.Builder().useCaseConfig
+ }
+
+ /**
+ * A fake implementation of [SupportedOutputSizesSorter] for testing.
+ */
+ private class FakeSupportedOutputSizesSorter(
+ private val supportedOutputSizes: Map<UseCaseConfig<*>, List<Size>>
+ ) : SupportedOutputSizesSorter(FakeCameraInfoInternal(), null) {
+ override fun getSortedSupportedOutputSizes(useCaseConfig: UseCaseConfig<*>): List<Size> {
+ return supportedOutputSizes[useCaseConfig]!!
+ }
+ }
+
+ companion object {
+ // 4:3 resolutions.
+ private val SIZE_3264_2448 = Size(3264, 2448)
+ private val SIZE_2560_1920 = Size(2560, 1920)
+ private val SIZE_1920_1440 = Size(1920, 1440)
+ private val SIZE_1280_960 = Size(1280, 960)
+ private val SIZE_960_720 = Size(960, 720)
+ private val SIZE_640_480 = Size(640, 480)
+ private val SIZE_320_240 = Size(320, 240)
+ private val SIZES_4_3 = listOf(
+ SIZE_3264_2448,
+ SIZE_2560_1920,
+ SIZE_1920_1440,
+ SIZE_1280_960,
+ SIZE_960_720,
+ SIZE_640_480,
+ SIZE_320_240
+ )
+ // 16:9 resolutions.
+ private val SIZE_3840_2160 = Size(3840, 2160)
+ private val SIZE_2560_1440 = Size(2560, 1440)
+ private val SIZE_1920_1080 = Size(1920, 1080)
+ private val SIZE_1280_720 = Size(1280, 720)
+ private val SIZE_960_540 = Size(960, 540)
+ private val SIZE_192_108 = Size(192, 108)
+ private val SIZES_16_9 = listOf(
+ SIZE_3840_2160,
+ SIZE_2560_1440,
+ SIZE_1920_1080,
+ SIZE_1280_720,
+ SIZE_960_540,
+ SIZE_192_108
+ )
+ // Other aspect-ratio resolutions.
+ private val SIZE_1440_720 = Size(1440, 720)
+ private val SIZE_800_800 = Size(800, 800)
+ private val SIZE_720_720 = Size(720, 720)
+ private val SIZE_500_400 = Size(500, 400)
+ private val SIZE_176_144 = Size(176, 144)
+ private val SIZES_OTHER_ASPECT_RATIO = listOf(
+ SIZE_1440_720,
+ SIZE_800_800,
+ SIZE_720_720,
+ SIZE_500_400,
+ SIZE_176_144
+ )
+ private val CAMERA_SUPPORTED_SIZES = SIZES_4_3 + SIZES_16_9 + SIZES_OTHER_ASPECT_RATIO
+ private val SENSOR_SIZE = SIZE_3264_2448 // 4:3
+ }
+}