Merge "Allow Preview to select 1080p resolution when device display has one shorter edge but larger area size" into androidx-main
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index 416b0e4..4d083da 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -35,7 +35,6 @@
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.ImageCaptureConfig
 import androidx.camera.core.impl.ImageOutputConfig
-import androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR
 import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.OptionsBundle
 import androidx.camera.core.impl.PreviewConfig
@@ -43,8 +42,6 @@
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
-import androidx.camera.core.resolutionselector.ResolutionSelector
-import androidx.camera.core.resolutionselector.ResolutionStrategy
 
 /**
  * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
@@ -136,15 +133,6 @@
                 ImageOutputConfig.OPTION_MAX_RESOLUTION,
                 previewSize
             )
-            mutableConfig.insertOption(
-                OPTION_RESOLUTION_SELECTOR,
-                ResolutionSelector.Builder().setResolutionStrategy(
-                    ResolutionStrategy(
-                        previewSize,
-                        ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
-                    )
-                ).build()
-            )
         }
 
         mutableConfig.insertOption(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
index 3d271cd..0010019 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
@@ -17,7 +17,6 @@
 package androidx.camera.camera2.internal;
 
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
@@ -37,8 +36,6 @@
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.resolutionselector.ResolutionSelector;
-import androidx.camera.core.resolutionselector.ResolutionStrategy;
 
 /**
  * Implementation of UseCaseConfigFactory to provide the default camera2 configurations for use
@@ -86,11 +83,6 @@
         if (captureType == CaptureType.PREVIEW) {
             Size previewSize = mDisplayInfoManager.getPreviewSize();
             mutableConfig.insertOption(OPTION_MAX_RESOLUTION, previewSize);
-            ResolutionStrategy resolutionStrategy = new ResolutionStrategy(previewSize,
-                    ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER);
-            mutableConfig.insertOption(OPTION_RESOLUTION_SELECTOR,
-                    new ResolutionSelector.Builder().setResolutionStrategy(
-                            resolutionStrategy).build());
         }
 
         // The default rotation value should be determined by the max non-state-off display.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 6661ecb..e36507c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -1080,10 +1080,8 @@
          * size match to the device's screen resolution, or to 1080p (1920x1080), whichever is
          * smaller. See the
          * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
-         * section in {@link android.hardware.camera2.CameraDevice}'. {@link Preview} has a
-         * default {@link ResolutionStrategy} with the {@code PREVIEW} bound size and
-         * {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER} to achieve this. Applications
-         * can override this default strategy with a different resolution strategy.
+         * section in {@link android.hardware.camera2.CameraDevice}'. Applications can set any
+         * {@link ResolutionStrategy} to override it.
          *
          * <p>Note that due to compatibility reasons, CameraX may select a resolution that is
          * larger than the default screen resolution on certain devices.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 396e646..a002de7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -19,6 +19,7 @@
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
@@ -226,6 +227,17 @@
             }
         }
 
+        // Removes the default max resolution setting if application sets any ResolutionStrategy
+        // to override it.
+        if (mUseCaseConfig.containsOption(OPTION_RESOLUTION_SELECTOR)
+                && mergedConfig.containsOption(OPTION_MAX_RESOLUTION)) {
+            ResolutionSelector resolutionSelector =
+                    mUseCaseConfig.retrieveOption(OPTION_RESOLUTION_SELECTOR);
+            if (resolutionSelector.getResolutionStrategy() != null) {
+                mergedConfig.removeOption(OPTION_MAX_RESOLUTION);
+            }
+        }
+
         // If any options need special handling, this is the place to do it. For now we'll just copy
         // over all options.
         for (Option<?> opt : mUseCaseConfig.listOptions()) {
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 7bbe75e..be65059 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
@@ -40,6 +40,7 @@
 import androidx.camera.core.impl.utils.AspectRatioUtil;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.core.internal.utils.SizeUtil;
 import androidx.camera.core.resolutionselector.AspectRatioStrategy;
 import androidx.camera.core.resolutionselector.ResolutionFilter;
 import androidx.camera.core.resolutionselector.ResolutionSelector;
@@ -216,6 +217,13 @@
                 applyAspectRatioStrategy(resolutionCandidateList,
                         resolutionSelector.getAspectRatioStrategy());
 
+
+        // Applies the max resolution setting
+        Size maxResolution = ((ImageOutputConfig) useCaseConfig).getMaxResolution(null);
+        if (maxResolution != null) {
+            applyMaxResolutionRestriction(aspectRatioSizeListMap, maxResolution);
+        }
+
         // Applies the resolution strategy onto the resolution candidate list.
         applyResolutionStrategy(aspectRatioSizeListMap, resolutionSelector.getResolutionStrategy());
 
@@ -436,6 +444,32 @@
     }
 
     /**
+     * Applies the max resolution restriction.
+     *
+     * <p>Filters out the output sizes that exceed the max resolution in area size.
+     *
+     * @param sortedAspectRatioSizeListMap the aspect ratio to size list linked hash map. The
+     *                                     entries order should not be changed.
+     * @param maxResolution                the max resolution size.
+     */
+    private static void applyMaxResolutionRestriction(
+            @NonNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap,
+            @NonNull Size maxResolution) {
+        int maxResolutionAreaSize = SizeUtil.getArea(maxResolution);
+        for (Rational key : sortedAspectRatioSizeListMap.keySet()) {
+            List<Size> supportedSizesList = sortedAspectRatioSizeListMap.get(key);
+            List<Size> filteredResultList = new ArrayList<>();
+            for (Size size : supportedSizesList) {
+                if (SizeUtil.getArea(size) <= maxResolutionAreaSize) {
+                    filteredResultList.add(size);
+                }
+            }
+            supportedSizesList.clear();
+            supportedSizesList.addAll(filteredResultList);
+        }
+    }
+
+    /**
      * Applies the resolution filtered to the sorted output size list.
      *
      * @param sizeList         the supported size list which has been filtered and sorted by the
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
index 3cb7587..e79c482 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -540,6 +540,28 @@
         supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
     }
 
+    @Test
+    fun canKeepFhdResolution_whenMaxResolutionHasShorterEdgeButLargerArea() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            maxResolution = Size(2244, 1008),
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(1920, 1080), // 16:9, this can be kept even the max resolution
+                                               // setting has a shorter edge of 1008
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        )
+    }
+
     private fun verifySupportedOutputSizesWithResolutionSelectorSettings(
         outputSizesSorter: SupportedOutputSizesSorter = supportedOutputSizesSorter,
         captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
@@ -551,6 +573,7 @@
         resolutionFilter: ResolutionFilter? = null,
         allowedResolutionMode: Int = ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION,
         highResolutionForceDisabled: Boolean = false,
+        maxResolution: Size? = null,
         expectedList: List<Size> = Collections.emptyList(),
     ) {
         val useCaseConfig = createUseCaseConfig(
@@ -562,7 +585,8 @@
             resolutionFallbackRule,
             resolutionFilter,
             allowedResolutionMode,
-            highResolutionForceDisabled
+            highResolutionForceDisabled,
+            maxResolution,
         )
         val resultList = outputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
         assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
@@ -578,6 +602,7 @@
         resolutionFilter: ResolutionFilter? = null,
         allowedResolutionMode: Int = ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION,
         highResolutionForceDisabled: Boolean = false,
+        maxResolution: Size? = null,
     ): UseCaseConfig<*> {
         val useCaseConfigBuilder = FakeUseCaseConfig.Builder(captureType, ImageFormat.JPEG)
         val resolutionSelectorBuilder = ResolutionSelector.Builder()
@@ -616,6 +641,9 @@
         // Sets the high resolution force disabled setting
         useCaseConfigBuilder.setHighResolutionDisabled(highResolutionForceDisabled)
 
+        // Sets the max resolution setting
+        maxResolution?.let { useCaseConfigBuilder.setMaxResolution(it) }
+
         return useCaseConfigBuilder.useCaseConfig
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index 00a4ae3..569a333 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -35,6 +35,7 @@
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+import androidx.camera.core.resolutionselector.ResolutionStrategy
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -583,6 +584,39 @@
         Truth.assertThat(surfaceFutureSemaphore!!.tryAcquire(10, TimeUnit.SECONDS)).isTrue()
     }
 
+    @Test
+    fun defaultMaxResolutionCanBeKept_whenResolutionStrategyIsNotSet() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        val useCase = Preview.Builder().build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context!!,
+            CameraSelector.DEFAULT_BACK_CAMERA, useCase
+        )
+        Truth.assertThat(
+            useCase.currentConfig.containsOption(
+                ImageOutputConfig.OPTION_MAX_RESOLUTION
+            )
+        ).isTrue()
+    }
+
+    @Test
+    fun defaultMaxResolutionCanBeRemoved_whenResolutionStrategyIsSet() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        val useCase = Preview.Builder().setResolutionSelector(
+            ResolutionSelector.Builder()
+                .setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build()
+        ).build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context!!,
+            CameraSelector.DEFAULT_BACK_CAMERA, useCase
+        )
+        Truth.assertThat(
+            useCase.currentConfig.containsOption(
+                ImageOutputConfig.OPTION_MAX_RESOLUTION
+            )
+        ).isFalse()
+    }
+
     private val workExecutorWithNamedThread: Executor
         get() {
             val threadFactory =