Update CompareAspectRatiosByDistanceToTargetRatio to compare by overlapping area

Added additional full FOV ratio parameter so that we can check whether mapping area of the checking ratio can cover the mapping area of the target aspect ratio in the full active array ratio space. And we can also check the overlapping area size in the full active array ratio space to know which ratio is closer to the target aspect ratio value.

Rename CompareAspectRatiosByDistanceToTargetRatio to CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace to match its new algorithm

Bug: 247471840
Test: AspectRationUtilTest && SupportedSurfaceCombinationTest

Change-Id: Ifc45e8e9537a177cf9ef2bcc8d4f226434ea6950
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index fc355fe..9b9c238 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -42,7 +42,8 @@
 import androidx.camera.core.impl.SurfaceSizeDefinition
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.utils.AspectRatioUtil
-import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByDistanceToTargetRatio
+import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.core.internal.utils.SizeUtil
@@ -85,6 +86,8 @@
     internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
     private val displayManager: DisplayManager =
         (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
+    private val activeArraySize =
+        cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]
 
     init {
         checkCapabilities()
@@ -583,10 +586,90 @@
     }
 
     /**
-     * Obtains the target aspect ratio from ImageOutputConfig
+     * 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.
      */
-    private fun getTargetAspectRatio(imageOutputConfig: ImageOutputConfig): Rational? {
-        val targetSize = getTargetSize(imageOutputConfig)
+    private fun getAspectRatioGroupKeyOfTargetSize(
+        targetSize: Size?,
+        resolutionCandidateList: List<Size>
+    ): Rational? {
+        if (targetSize == null) {
+            return null
+        }
+
+        val aspectRatios = getResolutionListGroupingAspectRatioKeys(
+            resolutionCandidateList
+        )
+        aspectRatios.forEach {
+            if (hasMatchingAspectRatio(targetSize, it)) {
+                return it
+            }
+        }
+        return Rational(targetSize.width, targetSize.height)
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+     * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    private fun getResolutionListGroupingAspectRatioKeys(
+        resolutionCandidateList: List<Size>
+    ): List<Rational> {
+        val aspectRatios: MutableList<Rational> = mutableListOf()
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_4_3)
+        aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_16_9)
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        resolutionCandidateList.forEach { size ->
+            val newRatio = Rational(size.width, size.height)
+            var aspectRatioFound = aspectRatios.contains(newRatio)
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                var hasMatchingAspectRatio = false
+                aspectRatios.forEach loop@{ aspectRatio ->
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true
+                        return@loop
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio)
+                }
+            }
+        }
+        return aspectRatios
+    }
+
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by TargetAspectRatio quirk (not implemented yet).
+     * 2. The use case's original aspect ratio if TargetAspectRatio quirk returns RATIO_ORIGINAL
+     * and the use case has target aspect ratio setting.
+     * 3. The aspect ratio of use case's target size setting if TargetAspectRatio quirk returns
+     * RATIO_ORIGINAL and the use case has no target aspect ratio but has target size setting.
+     *
+     * @param imageOutputConfig       the image output config of the use case.
+     * @param resolutionCandidateList the resolution candidate list which will be used to
+     *                                determine the aspect ratio by target size when target
+     *                                aspect ratio setting is not set.
+     */
+    private fun getTargetAspectRatio(
+        imageOutputConfig: ImageOutputConfig,
+        resolutionCandidateList: List<Size>
+    ): Rational? {
         var outputRatio: Rational? = null
         // TODO(b/245622117) Get the corrected aspect ratio from quirks instead of always using
         //  TargetAspectRatio.RATIO_ORIGINAL
@@ -603,11 +686,17 @@
                     "Undefined target aspect ratio: $aspectRatio"
                 )
             }
-        } else if (targetSize != null) {
-            // Target size is calculated from the target resolution. If target size is not
-            // null, sizes which aspect ratio is nearest to the aspect ratio of target size
-            // will be selected in priority.
-            outputRatio = Rational(targetSize.width, targetSize.height)
+        } 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.
+            val targetSize = getTargetSize(imageOutputConfig)
+            if (targetSize != null) {
+                outputRatio = getAspectRatioGroupKeyOfTargetSize(
+                    targetSize,
+                    resolutionCandidateList
+                )
+            }
         }
         return outputRatio
     }
@@ -656,33 +745,22 @@
      * Groups sizes together according to their aspect ratios.
      */
     private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
-        val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = java.util.HashMap()
+        val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = mutableMapOf()
 
-        // Add 4:3 and 16:9 entries first. Most devices should mainly have supported sizes of
-        // these two aspect ratios. Adding them first can avoid that if the first one 4:3 or 16:9
-        // size is a mod16 alignment size, the aspect ratio key may be different from the 4:3 or
-        // 16:9 value.
-        aspectRatioSizeListMap[AspectRatioUtil.ASPECT_RATIO_4_3] = ArrayList()
-        aspectRatioSizeListMap[AspectRatioUtil.ASPECT_RATIO_16_9] = ArrayList()
-        for (outputSize in sizes) {
-            var matchedKey: Rational? = null
-            for (key in aspectRatioSizeListMap.keys) {
+        val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+        aspectRatioKeys.forEach {
+            aspectRatioSizeListMap[it] = mutableListOf()
+        }
+
+        sizes.forEach { size ->
+            aspectRatioSizeListMap.keys.forEach { aspectRatio ->
                 // Put the size into all groups that is matched in mod16 condition since a size
                 // may match multiple aspect ratio in mod16 algorithm.
-                if (AspectRatioUtil.hasMatchingAspectRatio(outputSize, key)) {
-                    matchedKey = key
-                    val sizeList = aspectRatioSizeListMap[matchedKey]!!
-                    if (!sizeList.contains(outputSize)) {
-                        sizeList.add(outputSize)
-                    }
+                if (hasMatchingAspectRatio(size, aspectRatio)) {
+                    aspectRatioSizeListMap[aspectRatio]?.add(size)
                 }
             }
-
-            // Create new item if no matching group is found.
-            if (matchedKey == null) {
-                aspectRatioSizeListMap[Rational(outputSize.width, outputSize.height)] =
-                    ArrayList(setOf(outputSize))
-            }
         }
         return aspectRatioSizeListMap
     }
@@ -714,8 +792,8 @@
         // result.
         Arrays.sort(outputSizes, CompareSizesByArea(true))
         var targetSize: Size? = getTargetSize(imageOutputConfig)
-        var minSize = SizeUtil.RESOLUTION_VGA
-        val defaultSizeArea = SizeUtil.getArea(SizeUtil.RESOLUTION_VGA)
+        var minSize = RESOLUTION_VGA
+        val defaultSizeArea = SizeUtil.getArea(RESOLUTION_VGA)
         val maxSizeArea = SizeUtil.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,
@@ -742,7 +820,8 @@
                     imageFormat
             )
         }
-        val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig)
+
+        val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates)
 
         // Check the default resolution if the target resolution is not set
         targetSize = targetSize ?: imageOutputConfig.getDefaultResolution(null)
@@ -773,9 +852,17 @@
 
             // Sort the aspect ratio key set by the target aspect ratio.
             val aspectRatios: List<Rational?> = ArrayList(aspectRatioSizeListMap.keys)
+            val fullFovRatio = if (activeArraySize != null) {
+                Rational(activeArraySize.width(), activeArraySize.height())
+            } else {
+                null
+            }
             Collections.sort(
                 aspectRatios,
-                CompareAspectRatiosByDistanceToTargetRatio(aspectRatio)
+                CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                    aspectRatio,
+                    fullFovRatio
+                )
             )
 
             // Put available sizes into final result list by aspect ratio distance to target ratio.
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 84788a9..2249cca 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -697,10 +697,10 @@
             mockCamcorderProfileAdapter
         )
 
-        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
         // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
         // checks whether 1280x720 is selected or not.
-        val targetResolution = Size(1200, 720)
+        val targetResolution = Size(1280, 640)
         val imageCapture = ImageCapture.Builder().setTargetResolution(
             targetResolution
         ).setTargetRotation(Surface.ROTATION_90).build()
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 721b2ddf..15f8049 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -20,6 +20,7 @@
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
@@ -28,6 +29,7 @@
 
 import android.content.Context;
 import android.graphics.ImageFormat;
+import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
@@ -104,6 +106,8 @@
     private final DisplayInfoManager mDisplayInfoManager;
     private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
 
+    private final Size mActiveArraySize;
+
     SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
             @NonNull CameraManagerCompat cameraManagerCompat,
             @NonNull CamcorderProfileHelper camcorderProfileHelper)
@@ -140,6 +144,9 @@
             }
         }
 
+        Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
+
         generateSupportedCombinationList();
         generateSurfaceSizeDefinition();
         checkCustomization();
@@ -289,7 +296,26 @@
         return suggestedResolutionsMap;
     }
 
-    private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig) {
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by {@link TargetAspectRatio} if it is
+     * {@link TargetAspectRatio#RATIO_4_3}, {@link TargetAspectRatio#RATIO_16_9} or
+     * {@link TargetAspectRatio#RATIO_MAX_JPEG}.
+     * 2. The use case's original aspect ratio if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has target aspect ratio setting.
+     * 3. The aspect ratio of use case's target size setting if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has no target aspect ratio but has
+     * target size setting.
+     *
+     * @param imageOutputConfig       the image output config of the use case.
+     * @param resolutionCandidateList the resolution candidate list which will be used to
+     *                                determine the aspect ratio by target size when target
+     *                                aspect ratio setting is not set.
+     */
+    private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig,
+            @NonNull List<Size> resolutionCandidateList) {
         Rational outputRatio = null;
         // Gets the corrected aspect ratio due to device constraints or null if no correction is
         // needed.
@@ -307,7 +333,6 @@
                 outputRatio = new Rational(maxJpegSize.getWidth(), maxJpegSize.getHeight());
                 break;
             case TargetAspectRatio.RATIO_ORIGINAL:
-                Size targetSize = getTargetSize(imageOutputConfig);
                 if (imageOutputConfig.hasTargetAspectRatio()) {
                     @AspectRatio.Ratio int aspectRatio = imageOutputConfig.getTargetAspectRatio();
                     switch (aspectRatio) {
@@ -322,11 +347,15 @@
                         default:
                             Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
                     }
-                } else if (targetSize != null) {
-                    // Target size is calculated from the target resolution. If target size is not
-                    // null, sizes which aspect ratio is nearest to the aspect ratio of target size
-                    // will be selected in priority.
-                    outputRatio = new Rational(targetSize.getWidth(), targetSize.getHeight());
+                } 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);
+                    }
                 }
                 break;
             default:
@@ -430,7 +459,7 @@
                             + imageFormat);
         }
 
-        Rational aspectRatio = getTargetAspectRatio(imageOutputConfig);
+        Rational aspectRatio = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates);
 
         // Check the default resolution if the target resolution is not set
         targetSize = targetSize == null ? imageOutputConfig.getDefaultResolution(null) : targetSize;
@@ -465,8 +494,11 @@
 
             // Sort the aspect ratio key set by the target aspect ratio.
             List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+            Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+                    mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
             Collections.sort(aspectRatios,
-                    new AspectRatioUtil.CompareAspectRatiosByDistanceToTargetRatio(aspectRatio));
+                    new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                            aspectRatio, fullFovRatio));
 
             // Put available sizes into final result list by aspect ratio distance to target ratio.
             for (Rational rational : aspectRatios) {
@@ -496,6 +528,73 @@
         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 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());
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * <p>Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+     * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    @NonNull
+    private List<Rational> getResolutionListGroupingAspectRatioKeys(
+            @NonNull List<Size> resolutionCandidateList) {
+        List<Rational> aspectRatios = new ArrayList<>();
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(ASPECT_RATIO_4_3);
+        aspectRatios.add(ASPECT_RATIO_16_9);
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        for (Size size : resolutionCandidateList) {
+            Rational newRatio = new Rational(size.getWidth(), size.getHeight());
+            boolean aspectRatioFound = aspectRatios.contains(newRatio);
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                boolean hasMatchingAspectRatio = false;
+                for (Rational aspectRatio : aspectRatios) {
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true;
+                        break;
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio);
+                }
+            }
+        }
+
+        return aspectRatios;
+    }
+
     // Use target rotation to calibrate the size.
     @Nullable
     private Size flipSizeByRotation(@Nullable Size size, int targetRotation) {
@@ -542,35 +641,20 @@
     private Map<Rational, List<Size>> groupSizesByAspectRatio(List<Size> sizes) {
         Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
 
-        // Add 4:3 and 16:9 entries first. Most devices should mainly have supported sizes of
-        // these two aspect ratios. Adding them first can avoid that if the first one 4:3 or 16:9
-        // size is a mod16 alignment size, the aspect ratio key may be different from the 4:3 or
-        // 16:9 value.
-        aspectRatioSizeListMap.put(ASPECT_RATIO_4_3, new ArrayList<>());
-        aspectRatioSizeListMap.put(ASPECT_RATIO_16_9, new ArrayList<>());
+        List<Rational> aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes);
+
+        for (Rational aspectRatio: aspectRatioKeys) {
+            aspectRatioSizeListMap.put(aspectRatio, new ArrayList<>());
+        }
 
         for (Size outputSize : sizes) {
-            Rational matchedKey = null;
-
             for (Rational key : aspectRatioSizeListMap.keySet()) {
                 // Put the size into all groups that is matched in mod16 condition since a size
                 // may match multiple aspect ratio in mod16 algorithm.
-                if (AspectRatioUtil.hasMatchingAspectRatio(outputSize, key)) {
-                    matchedKey = key;
-
-                    List<Size> sizeList = aspectRatioSizeListMap.get(matchedKey);
-                    if (!sizeList.contains(outputSize)) {
-                        sizeList.add(outputSize);
-                    }
+                if (hasMatchingAspectRatio(outputSize, key)) {
+                    aspectRatioSizeListMap.get(key).add(outputSize);
                 }
             }
-
-            // Create new item if no matching group is found.
-            if (matchedKey == null) {
-                aspectRatioSizeListMap.put(
-                        new Rational(outputSize.getWidth(), outputSize.getHeight()),
-                        new ArrayList<>(Collections.singleton(outputSize)));
-            }
         }
 
         return aspectRatioSizeListMap;
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 63f772c1..c16a5b5 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -137,7 +137,7 @@
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class SupportedSurfaceCombinationTest() {
+class SupportedSurfaceCombinationTest {
     private val mockCamcorderProfileHelper = Mockito.mock(
         CamcorderProfileHelper::class.java
     )
@@ -624,10 +624,10 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
         // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
         // checks whether 1280x720 is selected or not.
-        val resolution = Size(1200, 720)
+        val resolution = Size(1280, 640)
         val useCase = createUseCase(
             FAKE_USE_CASE,
             Surface.ROTATION_90,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
index 7528936..e0eac35 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.internal.utils.SizeUtil.getArea;
 
+import android.graphics.RectF;
 import android.util.Rational;
 import android.util.Size;
 
@@ -38,6 +39,7 @@
     public static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
     public static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
     public static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
+
     private static final int ALIGN16 = 16;
 
     private AspectRatioUtil() {
@@ -94,7 +96,6 @@
         return false;
     }
 
-
     private static boolean ratioIntersectsMod16Segment(int height, int mod16Width,
             Rational aspectRatio) {
         Preconditions.checkArgument(mod16Width % 16 == 0);
@@ -104,14 +105,26 @@
                 mod16Width + ALIGN16);
     }
 
-    /** Comparator based on how close they are to the target aspect ratio. */
+    /**
+     * Comparator based on how close they are to the target aspect ratio by comparing the
+     * transformed mapping area in the full FOV ratio space.
+     *
+     * The mapping area will be the region that the images of the specific aspect ratio cropped
+     * from the full FOV images. Therefore, we can compare the mapping areas to know which one is
+     * closer to the mapping area of the target aspect ratio setting.
+     */
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-    public static final class CompareAspectRatiosByDistanceToTargetRatio implements
+    public static final class CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace implements
             Comparator<Rational> {
-        private Rational mTargetRatio;
+        private final Rational mTargetRatio;
+        private final RectF mTransformedMappingArea;
+        private final Rational mFullFovRatio;
 
-        public CompareAspectRatiosByDistanceToTargetRatio(@NonNull Rational targetRatio) {
+        public CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                @NonNull Rational targetRatio, @Nullable Rational fullFovRatio) {
             mTargetRatio = targetRatio;
+            mFullFovRatio = fullFovRatio != null ? fullFovRatio : new Rational(4, 3);
+            mTransformedMappingArea = getTransformedMappingArea(mTargetRatio);
         }
 
         @Override
@@ -120,11 +133,81 @@
                 return 0;
             }
 
-            final Float lhsRatioDelta = Math.abs(lhs.floatValue() - mTargetRatio.floatValue());
-            final Float rhsRatioDelta = Math.abs(rhs.floatValue() - mTargetRatio.floatValue());
+            RectF lhsMappingArea = getTransformedMappingArea(lhs);
+            RectF rhsMappingArea = getTransformedMappingArea(rhs);
 
-            int result = (int) Math.signum(lhsRatioDelta - rhsRatioDelta);
-            return result;
+            boolean isCoveredByLhs = isMappingAreaCovered(lhsMappingArea,
+                    mTransformedMappingArea);
+            boolean isCoveredByRhs = isMappingAreaCovered(rhsMappingArea,
+                    mTransformedMappingArea);
+
+            if (isCoveredByLhs && isCoveredByRhs) {
+                // When both ratios can cover the transformed target aspect mapping area in the
+                // full FOV space, checks which area is smaller to determine which ratio is
+                // closer to the target aspect ratio.
+                return (int) Math.signum(
+                        getMappingAreaSize(lhsMappingArea) - getMappingAreaSize(rhsMappingArea));
+            } else if (isCoveredByLhs) {
+                return -1;
+            } else if (isCoveredByRhs) {
+                return 1;
+            } else {
+                // When both ratios can't cover the transformed target aspect mapping area in the
+                // full FOV space, checks which overlapping area is larger to determine which
+                // ratio is closer to the target aspect ratio.
+                float lhsOverlappingArea = getOverlappingAreaSize(lhsMappingArea,
+                        mTransformedMappingArea);
+                float rhsOverlappingArea = getOverlappingAreaSize(rhsMappingArea,
+                        mTransformedMappingArea);
+                return -((int) Math.signum(lhsOverlappingArea - rhsOverlappingArea));
+            }
+        }
+
+        /**
+         * Returns the rectangle after transforming the input rational into full FOV aspect ratio
+         * space.
+         */
+        private RectF getTransformedMappingArea(Rational ratio) {
+            if (ratio.floatValue() == mFullFovRatio.floatValue()) {
+                return new RectF(0, 0, mFullFovRatio.getNumerator(),
+                        mFullFovRatio.getDenominator());
+            } else if (ratio.floatValue() > mFullFovRatio.floatValue()) {
+                return new RectF(0, 0, mFullFovRatio.getNumerator(),
+                        (float) ratio.getDenominator() * (float) mFullFovRatio.getNumerator()
+                                / (float) ratio.getNumerator());
+            } else {
+                return new RectF(0, 0,
+                        (float) ratio.getNumerator() * (float) mFullFovRatio.getDenominator()
+                                / (float) ratio.getDenominator(), mFullFovRatio.getDenominator());
+            }
+        }
+
+        /**
+         * Returns {@code true} if the source transformed mapping area can fully cover the target
+         * transformed mapping area. Otherwise, returns {@code false};
+         */
+        private boolean isMappingAreaCovered(RectF sourceMappingArea, RectF targetMappingArea) {
+            return sourceMappingArea.width() >= targetMappingArea.width()
+                    && sourceMappingArea.height() >= targetMappingArea.height();
+        }
+
+        /**
+         * Returns the input mapping area's size value.
+         */
+        private float getMappingAreaSize(RectF mappingArea) {
+            return mappingArea.width() * mappingArea.height();
+        }
+
+        /**
+         * Returns the overlapping area value between the input two mapping areas in the full FOV
+         * space.
+         */
+        private float getOverlappingAreaSize(RectF mappingArea1, RectF mappingArea2) {
+            float overlappingAreaWidth = mappingArea1.width() < mappingArea2.width()
+                    ? mappingArea1.width() : mappingArea2.width();
+            float overlappingAreaHeight = mappingArea1.height() < mappingArea2.height()
+                    ? mappingArea1.height() : mappingArea2.height();
+            return overlappingAreaWidth * overlappingAreaHeight;
         }
     }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
index f42c007..c1ee35b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
@@ -19,7 +19,9 @@
 import android.os.Build
 import android.util.Rational
 import android.util.Size
-import com.google.common.truth.Truth
+import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
+import com.google.common.truth.Truth.assertThat
+import java.util.Collections
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
@@ -31,7 +33,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withNullAspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(16, 9),
                 null
@@ -41,7 +43,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withSameAspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(16, 9),
                 Rational(16, 9)
@@ -51,7 +53,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_720p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1280, 720),
                 Rational(16, 9)
@@ -61,7 +63,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1080p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1920, 1088),
                 Rational(16, 9)
@@ -71,7 +73,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1440p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(2560, 1440),
                 Rational(16, 9)
@@ -81,7 +83,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_2160p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(3840, 2160),
                 Rational(16, 9)
@@ -91,7 +93,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1x1() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1088, 1088),
                 Rational(1, 1)
@@ -101,7 +103,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_4x3() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1024, 768),
                 Rational(4, 3)
@@ -111,11 +113,44 @@
 
     @Test
     fun testHasMatchingAspectRatio_withNonMod16AspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1281, 721),
                 Rational(16, 9)
             )
         ).isFalse()
     }
+
+    @Test
+    fun sortAspectRatios() {
+        // Sort the aspect ratio key set by the target aspect ratio.
+        val aspectRatios = listOf(
+            Rational(1, 1),
+            Rational(4, 3),
+            Rational(16, 9),
+            Rational(18, 9),
+            Rational(14, 9),
+        )
+
+        val targetAspectRatio = Rational(16, 9)
+        val fullFovAspectRatio = Rational(4, 3)
+
+        Collections.sort(
+            aspectRatios,
+            CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                targetAspectRatio,
+                fullFovAspectRatio
+            )
+        )
+
+        val expectedResult = listOf(
+            Rational(16, 9),
+            Rational(14, 9),
+            Rational(4, 3),
+            Rational(18, 9),
+            Rational(1, 1),
+        )
+
+        assertThat(aspectRatios == expectedResult).isTrue()
+    }
 }
\ No newline at end of file