Merge "[CameraPipe] Port proper resolution selection for MeteringRepeating" 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 8aec946..db29d61 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
@@ -17,16 +17,13 @@
 package androidx.camera.camera2.pipe.integration.adapter
 
 import android.content.Context
-import android.graphics.Point
 import android.hardware.camera2.CameraCaptureSession.CaptureCallback
 import android.hardware.camera2.CameraDevice
-import android.hardware.display.DisplayManager
-import android.util.Size
-import android.view.Display
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.core.Log.info
 import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
+import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
 import androidx.camera.camera2.pipe.integration.impl.SESSION_PHYSICAL_CAMERA_ID_OPTION
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.impl.CameraCaptureCallback
@@ -48,17 +45,7 @@
 @Suppress("DEPRECATION")
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 class CameraUseCaseAdapter(context: Context) : UseCaseConfigFactory {
-    private val MAX_PREVIEW_SIZE = Size(1920, 1080)
-
-    private val displayManager: DisplayManager by lazy {
-        context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
-    }
-    private val defaultDisplay: Display by lazy {
-        getMaxSizeDisplay()
-    }
-    private val previewSize: Size by lazy {
-        calculatePreviewSize()
-    }
+    private val displayInfoManager by lazy { DisplayInfoManager(context) }
 
     init {
         if (context === context.applicationContext) {
@@ -132,56 +119,17 @@
         if (captureType == UseCaseConfigFactory.CaptureType.PREVIEW) {
             mutableConfig.insertOption(
                 ImageOutputConfig.OPTION_MAX_RESOLUTION,
-                previewSize
+                displayInfoManager.previewSize
             )
         }
 
         mutableConfig.insertOption(
             ImageOutputConfig.OPTION_TARGET_ROTATION,
-            defaultDisplay.rotation
+            displayInfoManager.defaultDisplay.rotation
         )
         return OptionsBundle.from(mutableConfig)
     }
 
-    private fun getMaxSizeDisplay(): Display {
-        val displays = displayManager.displays
-        var maxDisplay: Display? = null
-        var maxDisplaySize = -1
-        for (display: Display in displays) {
-            val displaySize = Point()
-            // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
-            //  is deprecated since API level 31.
-            display.getRealSize(displaySize)
-            if (displaySize.x * displaySize.y > maxDisplaySize) {
-                maxDisplaySize = displaySize.x * displaySize.y
-                maxDisplay = display
-            }
-        }
-        return checkNotNull(maxDisplay) { "No displays found from ${displayManager.displays}!" }
-    }
-
-    /**
-     * Calculates the device's screen resolution, or MAX_PREVIEW_SIZE, whichever is smaller.
-     */
-    private fun calculatePreviewSize(): Size {
-        val displaySize = Point()
-        val display: Display = defaultDisplay
-        display.getRealSize(displaySize)
-        var displayViewSize: Size
-        displayViewSize = if (displaySize.x > displaySize.y) {
-            Size(displaySize.x, displaySize.y)
-        } else {
-            Size(displaySize.y, displaySize.x)
-        }
-        if (displayViewSize.width * displayViewSize.height
-            > MAX_PREVIEW_SIZE.width * MAX_PREVIEW_SIZE.height
-        ) {
-            displayViewSize = MAX_PREVIEW_SIZE
-        }
-        // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
-        return displayViewSize
-    }
-
     object DefaultCaptureOptionsUnpacker : CaptureConfig.OptionUnpacker {
         @OptIn(ExperimentalCamera2Interop::class)
         override fun unpack(config: UseCaseConfig<*>, builder: CaptureConfig.Builder) {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
new file mode 100644
index 0000000..1babffd
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.display.DisplayManager
+import android.util.Size
+import android.view.Display
+import androidx.annotation.RequiresApi
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Suppress("DEPRECATION") // getRealSize
+@Singleton
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class DisplayInfoManager @Inject constructor(context: Context) {
+    private val MAX_PREVIEW_SIZE = Size(1920, 1080)
+
+    private val displayManager: DisplayManager by lazy {
+        context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+    }
+
+    // TODO(b/198257203): Fetch latest display information for devices where display size is not
+    //  guaranteed to be fixed. (e.g. foldable devices or devices with multiple displays)
+
+    val defaultDisplay: Display by lazy {
+        getMaxSizeDisplay()
+    }
+
+    val previewSize: Size by lazy {
+        calculatePreviewSize()
+    }
+
+    private fun getMaxSizeDisplay(): Display {
+        val displays = displayManager.displays
+        var maxDisplay: Display? = null
+        var maxDisplaySize = -1
+
+        // TODO(b/211945950, b/255170076): Handle STATE_OFF displays.
+
+        for (display: Display in displays) {
+            val displaySize = Point()
+            // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
+            //  is deprecated since API level 31.
+            display.getRealSize(displaySize)
+            if (displaySize.x * displaySize.y > maxDisplaySize) {
+                maxDisplaySize = displaySize.x * displaySize.y
+                maxDisplay = display
+            }
+        }
+        return checkNotNull(maxDisplay) { "No displays found from ${displayManager.displays}!" }
+    }
+
+    /**
+     * Calculates the device's screen resolution, or MAX_PREVIEW_SIZE, whichever is smaller.
+     */
+    private fun calculatePreviewSize(): Size {
+        val displaySize = Point()
+        val display: Display = defaultDisplay
+        // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
+        //  is deprecated since API level 31.
+        display.getRealSize(displaySize)
+        var displayViewSize: Size
+        displayViewSize = if (displaySize.x > displaySize.y) {
+            Size(displaySize.x, displaySize.y)
+        } else {
+            Size(displaySize.y, displaySize.x)
+        }
+        if (displayViewSize.width * displayViewSize.height
+            > MAX_PREVIEW_SIZE.width * MAX_PREVIEW_SIZE.height
+        ) {
+            displayViewSize = MAX_PREVIEW_SIZE
+        }
+        // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
+        return displayViewSize
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 0f8323fa..684eb88 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -43,6 +43,7 @@
 import androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import kotlin.math.min
 
 private val DEFAULT_PREVIEW_SIZE = Size(0, 0)
 
@@ -55,9 +56,10 @@
 class MeteringRepeating(
     private val cameraProperties: CameraProperties,
     config: MeteringRepeatingConfig,
+    private val displayInfoManager: DisplayInfoManager
 ) : UseCase(config) {
 
-    private val meteringSurfaceSize = cameraProperties.getMinimumPreviewSize()
+    private val meteringSurfaceSize = getProperPreviewSize()
 
     private val deferrableSurfaceLock = Any()
 
@@ -65,9 +67,10 @@
     private var deferrableSurface: DeferrableSurface? = null
 
     override fun getDefaultConfig(applyDefaultConfig: Boolean, factory: UseCaseConfigFactory) =
-        Builder(cameraProperties).useCaseConfig
+        Builder(cameraProperties, displayInfoManager).useCaseConfig
 
-    override fun getUseCaseConfigBuilder(config: Config) = Builder(cameraProperties)
+    override fun getUseCaseConfigBuilder(config: Config) =
+        Builder(cameraProperties, displayInfoManager)
 
     override fun onSuggestedResolutionUpdated(suggestedResolution: Size): Size {
         updateSessionConfig(createPipeline().build())
@@ -120,28 +123,57 @@
             }
     }
 
-    private fun CameraProperties.getMinimumPreviewSize(): Size {
-        val map = metadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
-        if (map == null) {
+    private fun CameraProperties.getOutputSizes(): Array<Size>? {
+        val map = metadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP] ?: run {
             error { "Can not retrieve SCALER_STREAM_CONFIGURATION_MAP." }
-            return DEFAULT_PREVIEW_SIZE
+            return null
         }
 
-        val outputSizes = if (Build.VERSION.SDK_INT < 23) {
+        return if (Build.VERSION.SDK_INT < 23) {
             // ImageFormat.PRIVATE is only public after Android level 23. Therefore, use
             // SurfaceTexture.class to get the supported output sizes before Android level 23.
             map.getOutputSizes(SurfaceTexture::class.java)
         } else {
             map.getOutputSizes(ImageFormat.PRIVATE)
         }
+    }
+
+    private fun getProperPreviewSize(): Size {
+        val outputSizes = cameraProperties.getOutputSizes()
 
         if (outputSizes == null) {
             error { "Can not get output size list." }
             return DEFAULT_PREVIEW_SIZE
         }
 
-        check(outputSizes.isNotEmpty()) { "Output sizes empty" }
-        return outputSizes.minWithOrNull { size1, size2 -> size1.area().compareTo(size2.area()) }!!
+        if (outputSizes.isEmpty()) {
+            error { "Output sizes empty" }
+            return DEFAULT_PREVIEW_SIZE
+        }
+
+        // TODO(b/256805716): get supported output sizes handling quirks.
+
+        outputSizes.sortBy { size -> size.width.toLong() * size.height.toLong() }
+
+        // Find maximum supported resolution that is <= min(VGA, display resolution)
+        // Using minimum supported size could cause some issue on certain devices.
+        val previewSize = displayInfoManager.previewSize
+        val maxSizeProduct =
+            min(640L * 480L, previewSize.width.toLong() * previewSize.height.toLong())
+
+        var previousSize: Size? = null
+        for (outputSize in outputSizes) {
+            val product = outputSize.width.toLong() * outputSize.height.toLong()
+            if (product == maxSizeProduct) {
+                return outputSize
+            } else if (product > maxSizeProduct) {
+                return previousSize ?: break // fallback to minimum size.
+            }
+            previousSize = outputSize
+        }
+
+        // If not found, return the minimum size.
+        return outputSizes[0]
     }
 
     class MeteringRepeatingConfig : UseCaseConfig<MeteringRepeating>, ImageInputConfig {
@@ -157,7 +189,10 @@
         override fun getInputFormat() = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
     }
 
-    class Builder(private val cameraProperties: CameraProperties) :
+    class Builder(
+        private val cameraProperties: CameraProperties,
+        private val displayInfoManager: DisplayInfoManager
+    ) :
         UseCaseConfig.Builder<MeteringRepeating, MeteringRepeatingConfig, Builder> {
 
         override fun getMutableConfig() = MutableOptionsBundle.create()
@@ -185,7 +220,7 @@
         override fun setZslDisabled(disabled: Boolean) = this
 
         override fun build(): MeteringRepeating {
-            return MeteringRepeating(cameraProperties, useCaseConfig)
+            return MeteringRepeating(cameraProperties, useCaseConfig, displayInfoManager)
         }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index cdef936..537adf9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -66,7 +66,8 @@
     @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Java version required for Dagger
     private val controls: java.util.Set<UseCaseCameraControl>,
     private val camera2CameraControl: Camera2CameraControl,
-    cameraProperties: CameraProperties
+    cameraProperties: CameraProperties,
+    displayInfoManager: DisplayInfoManager
 ) {
     private val lock = Any()
 
@@ -76,7 +77,12 @@
     @GuardedBy("lock")
     private val activeUseCases = mutableSetOf<UseCase>()
 
-    private val meteringRepeating by lazy { MeteringRepeating.Builder(cameraProperties).build() }
+    private val meteringRepeating by lazy {
+        MeteringRepeating.Builder(
+            cameraProperties,
+            displayInfoManager
+        ).build()
+    }
 
     @Volatile
     private var _activeComponent: UseCaseCameraComponent? = null
@@ -285,4 +291,4 @@
     private fun <T> Collection<T>.only(predicate: (T) -> Boolean): Boolean {
         return isNotEmpty() && all(predicate)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
new file mode 100644
index 0000000..f550d39
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.display.DisplayManager
+import android.util.Size
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowDisplayManager
+
+@Suppress("DEPRECATION") // getRealSize
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+class DisplayInfoManagerTest {
+    private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+
+    private fun addDisplay(width: Int, height: Int) {
+        ShadowDisplayManager.addDisplay(String.format("w%ddp-h%ddp", width, height))
+    }
+
+    @Test
+    fun defaultDisplayIsDeviceDisplay_whenOneDisplay() {
+        // Arrange
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        val currentDisplaySize = Point()
+        displayManager.displays[0].getRealSize(currentDisplaySize)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(currentDisplaySize, size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenMultipleDisplay() {
+        // Arrange
+        addDisplay(2000, 3000)
+        addDisplay(480, 640)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun throwsCorrectExceptionForDefaultDisplay_whenNoDisplay() {
+        // Arrange
+        ShadowDisplayManager.removeDisplay(0)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+    }
+
+    @Test
+    fun previewSizeIsProperSize_whenDisplaySmallerThan1080P() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act & Assert
+        assertEquals(Size(640, 480), displayInfoManager.previewSize)
+    }
+
+    @Test
+    fun previewSizeIsMaxPreviewSize_whenDisplayLargerThan1080P() {
+        // Arrange
+        addDisplay(2000, 3000)
+
+        // Act & Assert
+        assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
new file mode 100644
index 0000000..f40a4cf
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class MeteringRepeatingTest {
+    companion object {
+        val dummyZeroSize = Size(0, 0)
+
+        val dummySizeListWithout640x480 = listOf(
+            Size(4160, 3120),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(320, 240),
+            Size(240, 144),
+        )
+
+        val dummySizeListWith640x480 = listOf(
+            Size(4160, 3120),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(640, 480),
+            Size(320, 240),
+        )
+
+        val dummySizeListWithoutSmaller = listOf(
+            Size(4160, 3120),
+            Size(1920, 1080),
+            Size(1280, 720)
+        )
+
+        val dummySizeListSmallerThan640x480 = listOf(
+            Size(320, 480),
+            Size(320, 240),
+            Size(240, 144),
+        )
+
+        fun getFakeMetadata(sizeList: List<Size>): FakeCameraMetadata {
+            val shuffledList = sizeList.shuffled()
+
+            val builder = StreamConfigurationMapBuilder.newBuilder()
+            for (size in shuffledList) {
+                builder.addOutputSize(size)
+            }
+
+            return FakeCameraMetadata(
+                mapOf(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to builder.build(),
+                )
+            )
+        }
+    }
+
+    private lateinit var meteringRepeating: MeteringRepeating
+
+    private fun addDisplay(width: Int, height: Int) {
+        ShadowDisplayManager.addDisplay(String.format("w%ddp-h%ddp", width, height))
+    }
+
+    private fun getMeteringRepeatingAndInitDisplay(outputSizeList: List<Size>): MeteringRepeating {
+        for (size in outputSizeList) {
+            addDisplay(size.width, size.height)
+        }
+
+        return MeteringRepeating.Builder(
+            FakeCameraProperties(
+                getFakeMetadata(
+                    outputSizeList
+                )
+            ),
+            DisplayInfoManager(ApplicationProvider.getApplicationContext())
+        ).build()
+    }
+
+    @Test
+    fun attachedSurfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
+        meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)
+
+        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+
+        assertEquals(Size(320, 240), meteringRepeating.attachedSurfaceResolution)
+    }
+
+    @Test
+    fun attachedSurfaceResolutionIs640x480_when640x480PresentInOutputSizes() {
+        meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWith640x480)
+
+        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+
+        assertEquals(Size(640, 480), meteringRepeating.attachedSurfaceResolution)
+    }
+
+    @Test
+    fun attachedSurfaceResolutionFallsBackToMinimum_whenAllOutputSizesLargerThan640x480() {
+        meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithoutSmaller)
+
+        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+
+        assertEquals(Size(1280, 720), meteringRepeating.attachedSurfaceResolution)
+    }
+
+    @Test
+    fun attachedSurfaceResolutionIsLargestWithinPreviewSize_whenAllOutputSizesLessThan640x480() {
+        meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListSmallerThan640x480)
+
+        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+
+        assertEquals(Size(320, 480), meteringRepeating.attachedSurfaceResolution)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 865ad2c..13ef7aa 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -27,6 +27,7 @@
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraComponentBuilder
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -183,6 +184,7 @@
             FakeCamera2CameraControlCompat(),
             useCaseThreads,
             ComboRequestListener()
-        )
+        ),
+        displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
     )
 }