Implement compat version of EncoderProfiles

* Add EncoderProfilesProxy to wrap EncoderProfiles information.
* Add EncoderProfilesProxyCompat and corresponded implementations to
  handle methods for different API levels and back compatible with
  CamcorderProfile.
* Add EncoderProfilesProvider implementations for camera-camera2
  and camera-pipe.
* Add codes to handle resolution quirk.
* Add tests and test utils.

For details, please see design doc: go/camerax-encoder-profiles-compat.

Bug: 264514768
Test: manual test and ./gradlew bOS
Change-Id: I4bcb04e644922bf7fcc8e56b4e2a422f7dcc681b
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
new file mode 100644
index 0000000..c61aa16
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.camera2.pipe.integration
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.media.EncoderProfiles.VideoProfile.YUV_420
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.CameraUtil
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class EncoderProfilesProviderAdapterDeviceTest(private val quality: Int) {
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters
+        fun data(): Array<Array<Int>> = arrayOf(
+            arrayOf(CamcorderProfile.QUALITY_LOW),
+            arrayOf(CamcorderProfile.QUALITY_HIGH),
+            arrayOf(CamcorderProfile.QUALITY_QCIF),
+            arrayOf(CamcorderProfile.QUALITY_CIF),
+            arrayOf(CamcorderProfile.QUALITY_480P),
+            arrayOf(CamcorderProfile.QUALITY_720P),
+            arrayOf(CamcorderProfile.QUALITY_1080P),
+            arrayOf(CamcorderProfile.QUALITY_QVGA),
+            arrayOf(CamcorderProfile.QUALITY_2160P),
+            arrayOf(CamcorderProfile.QUALITY_VGA),
+            arrayOf(CamcorderProfile.QUALITY_4KDCI),
+            arrayOf(CamcorderProfile.QUALITY_QHD),
+            arrayOf(CamcorderProfile.QUALITY_2K)
+        )
+    }
+
+    private lateinit var encoderProfilesProvider: EncoderProfilesProviderAdapter
+    private var cameraId = ""
+    private var intCameraId = -1
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
+
+    @Before
+    fun setup() {
+        skipTestOnProblematicBuildsOfCuttlefishApi33()
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+        cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
+        intCameraId = cameraId.toInt()
+
+        encoderProfilesProvider = EncoderProfilesProviderAdapter(cameraId)
+    }
+
+    @Test
+    fun hasProfile_returnSameResult() {
+        assertThat(encoderProfilesProvider.hasProfile(quality))
+            .isEqualTo(CamcorderProfile.hasProfile(intCameraId, quality))
+    }
+
+    @Test
+    fun hasProfile_getReturnNonNull() {
+        Assume.assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
+
+        assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
+    }
+
+    @Test
+    fun notHasProfile_getReturnNull() {
+        Assume.assumeTrue(!CamcorderProfile.hasProfile(intCameraId, quality))
+
+        assertThat(encoderProfilesProvider.getAll(quality)).isNull()
+    }
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun hasSameContentAsCamcorderProfile() {
+        Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+        val profile = CamcorderProfile.get(quality)
+        val encoderProfiles = encoderProfilesProvider.getAll(quality)
+        val videoProfile = encoderProfiles!!.videoProfiles[0]
+        val audioProfile = encoderProfiles.audioProfiles[0]
+
+        assertThat(encoderProfiles.defaultDurationSeconds).isEqualTo(profile.duration)
+        assertThat(encoderProfiles.recommendedFileFormat).isEqualTo(profile.fileFormat)
+        assertThat(videoProfile.codec).isEqualTo(profile.videoCodec)
+        assertThat(videoProfile.bitrate).isEqualTo(profile.videoBitRate)
+        assertThat(videoProfile.frameRate).isEqualTo(profile.videoFrameRate)
+        assertThat(videoProfile.width).isEqualTo(profile.videoFrameWidth)
+        assertThat(videoProfile.height).isEqualTo(profile.videoFrameHeight)
+        assertThat(audioProfile.codec).isEqualTo(profile.audioCodec)
+        assertThat(audioProfile.bitrate).isEqualTo(profile.audioBitRate)
+        assertThat(audioProfile.sampleRate).isEqualTo(profile.audioSampleRate)
+        assertThat(audioProfile.channels).isEqualTo(profile.audioChannels)
+    }
+
+    @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 32)
+    @Test
+    fun api31Api32_hasSameContentAsEncoderProfiles() {
+        Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+        val profiles = CamcorderProfile.getAll(cameraId, quality)
+        val video = profiles!!.videoProfiles[0]
+        val audio = profiles.audioProfiles[0]
+        val profilesProxy = encoderProfilesProvider.getAll(quality)
+        val videoProxy = profilesProxy!!.videoProfiles[0]
+        val audioProxy = profilesProxy.audioProfiles[0]
+
+        assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+        assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+        assertThat(videoProxy.codec).isEqualTo(video.codec)
+        assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+        assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+        assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+        assertThat(videoProxy.width).isEqualTo(video.width)
+        assertThat(videoProxy.height).isEqualTo(video.height)
+        assertThat(videoProxy.profile).isEqualTo(video.profile)
+        assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
+        assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
+        assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
+        assertThat(audioProxy.codec).isEqualTo(audio.codec)
+        assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+        assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+        assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+        assertThat(audioProxy.channels).isEqualTo(audio.channels)
+        assertThat(audioProxy.profile).isEqualTo(audio.profile)
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    fun afterApi33_hasSameContentAsEncoderProfiles() {
+        Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+        val profiles = CamcorderProfile.getAll(cameraId, quality)
+        val video = profiles!!.videoProfiles[0]
+        val audio = profiles.audioProfiles[0]
+        val profilesProxy = encoderProfilesProvider.getAll(quality)
+        val videoProxy = profilesProxy!!.videoProfiles[0]
+        val audioProxy = profilesProxy.audioProfiles[0]
+
+        assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+        assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+        assertThat(videoProxy.codec).isEqualTo(video.codec)
+        assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+        assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+        assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+        assertThat(videoProxy.width).isEqualTo(video.width)
+        assertThat(videoProxy.height).isEqualTo(video.height)
+        assertThat(videoProxy.profile).isEqualTo(video.profile)
+        assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
+        assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
+        assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
+        assertThat(audioProxy.codec).isEqualTo(audio.codec)
+        assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+        assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+        assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+        assertThat(audioProxy.channels).isEqualTo(audio.channels)
+        assertThat(audioProxy.profile).isEqualTo(audio.profile)
+    }
+
+    // TODO: removes after b/265613005 is fixed
+    private fun skipTestOnProblematicBuildsOfCuttlefishApi33() {
+        // Skip test for b/265613005
+        Assume.assumeFalse(
+            "Cuttlefish has null VideoProfile issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 33 &&
+                Build.ID.startsWith("TP1A")
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
new file mode 100644
index 0000000..224eeab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.camera2.pipe.integration.adapter
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.Nullable
+import androidx.annotation.RequiresApi
+import androidx.camera.core.Logger
+import androidx.camera.core.impl.EncoderProfilesProvider
+import androidx.camera.core.impl.EncoderProfilesProxy
+import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat
+
+/**
+ * Adapt the [EncoderProfilesProvider] interface to [CameraPipe].
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class EncoderProfilesProviderAdapter(private val cameraIdString: String) : EncoderProfilesProvider {
+    private val hasValidCameraId: Boolean
+    private val cameraId: Int
+
+    init {
+        var hasValidCameraId = false
+        var intCameraId = -1
+        try {
+            intCameraId = cameraIdString.toInt()
+            hasValidCameraId = true
+        } catch (e: NumberFormatException) {
+            Logger.w(
+                TAG, "Camera id is not an integer:  $cameraIdString, unable to create" +
+                    " EncoderProfilesProviderAdapter."
+            )
+        }
+        this.hasValidCameraId = hasValidCameraId
+        cameraId = intCameraId
+
+        // TODO(b/241296464): CamcorderProfileResolutionQuirk
+    }
+
+    override fun hasProfile(quality: Int): Boolean {
+        if (!hasValidCameraId) {
+            return false
+        }
+        return CamcorderProfile.hasProfile(cameraId, quality)
+    }
+
+    override fun getAll(quality: Int): EncoderProfilesProxy? {
+        if (!hasValidCameraId) {
+            return null
+        }
+        if (!CamcorderProfile.hasProfile(cameraId, quality)) {
+             return null
+        }
+        return getProfilesInternal(quality)
+    }
+
+    @Nullable
+    @Suppress("DEPRECATION")
+    private fun getProfilesInternal(quality: Int): EncoderProfilesProxy? {
+        return if (Build.VERSION.SDK_INT >= 31) {
+            val profiles: EncoderProfiles? = Api31Impl.getAll(cameraIdString, quality)
+            if (profiles != null) EncoderProfilesProxyCompat.from(profiles) else null
+        } else {
+            var profile: CamcorderProfile? = null
+            try {
+                profile = CamcorderProfile.get(cameraId, quality)
+            } catch (e: RuntimeException) {
+                // CamcorderProfile.get() will throw
+                // - RuntimeException if not able to retrieve camcorder profile params.
+                // - IllegalArgumentException if quality is not valid.
+                Logger.w(TAG, "Unable to get CamcorderProfile by quality: $quality", e)
+            }
+            if (profile != null) EncoderProfilesProxyCompat.from(profile) else null
+        }
+    }
+
+    @RequiresApi(31)
+    internal object Api31Impl {
+        @DoNotInline
+        fun getAll(cameraId: String, quality: Int): EncoderProfiles? {
+            return CamcorderProfile.getAll(cameraId, quality)
+        }
+    }
+
+    companion object {
+        private const val TAG = "EncoderProfilesProviderAdapter"
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..8a87d6a
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.camera2.internal
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles.VideoProfile.YUV_420
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.os.Build
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.CameraUtil
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class Camera2EncoderProfilesProviderTest(private val quality: Int) {
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters
+        fun data(): Array<Array<Int>> = arrayOf(
+            arrayOf(CamcorderProfile.QUALITY_LOW),
+            arrayOf(CamcorderProfile.QUALITY_HIGH),
+            arrayOf(CamcorderProfile.QUALITY_QCIF),
+            arrayOf(CamcorderProfile.QUALITY_CIF),
+            arrayOf(CamcorderProfile.QUALITY_480P),
+            arrayOf(CamcorderProfile.QUALITY_720P),
+            arrayOf(CamcorderProfile.QUALITY_1080P),
+            arrayOf(CamcorderProfile.QUALITY_QVGA),
+            arrayOf(CamcorderProfile.QUALITY_2160P),
+            arrayOf(CamcorderProfile.QUALITY_VGA),
+            arrayOf(CamcorderProfile.QUALITY_4KDCI),
+            arrayOf(CamcorderProfile.QUALITY_QHD),
+            arrayOf(CamcorderProfile.QUALITY_2K)
+        )
+    }
+
+    private lateinit var encoderProfilesProvider: Camera2EncoderProfilesProvider
+    private var cameraId = ""
+    private var intCameraId = -1
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
+
+    @Before
+    fun setup() {
+        skipTestOnProblematicBuildsOfCuttlefishApi33()
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+        cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
+        intCameraId = cameraId.toInt()
+
+        encoderProfilesProvider = Camera2EncoderProfilesProvider(cameraId)
+    }
+
+    @Test
+    fun hasProfile_returnSameResult() {
+        assertThat(encoderProfilesProvider.hasProfile(quality))
+            .isEqualTo(CamcorderProfile.hasProfile(intCameraId, quality))
+    }
+
+    @Test
+    fun hasProfile_getReturnNonNull() {
+        assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
+
+        assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
+    }
+
+    @Test
+    fun notHasProfile_getReturnNull() {
+        assumeTrue(!CamcorderProfile.hasProfile(intCameraId, quality))
+
+        assertThat(encoderProfilesProvider.getAll(quality)).isNull()
+    }
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun hasSameContentAsCamcorderProfile() {
+        assumeTrue(CamcorderProfile.hasProfile(quality))
+
+        val profile = CamcorderProfile.get(quality)
+        val encoderProfiles = encoderProfilesProvider.getAll(quality)
+        val videoProfile = encoderProfiles!!.videoProfiles[0]
+        val audioProfile = encoderProfiles.audioProfiles[0]
+
+        assertThat(encoderProfiles.defaultDurationSeconds).isEqualTo(profile.duration)
+        assertThat(encoderProfiles.recommendedFileFormat).isEqualTo(profile.fileFormat)
+        assertThat(videoProfile.codec).isEqualTo(profile.videoCodec)
+        assertThat(videoProfile.bitrate).isEqualTo(profile.videoBitRate)
+        assertThat(videoProfile.frameRate).isEqualTo(profile.videoFrameRate)
+        assertThat(videoProfile.width).isEqualTo(profile.videoFrameWidth)
+        assertThat(videoProfile.height).isEqualTo(profile.videoFrameHeight)
+        assertThat(audioProfile.codec).isEqualTo(profile.audioCodec)
+        assertThat(audioProfile.bitrate).isEqualTo(profile.audioBitRate)
+        assertThat(audioProfile.sampleRate).isEqualTo(profile.audioSampleRate)
+        assertThat(audioProfile.channels).isEqualTo(profile.audioChannels)
+    }
+
+    @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 32)
+    @Test
+    fun api31Api32_hasSameContentAsEncoderProfiles() {
+        assumeTrue(CamcorderProfile.hasProfile(quality))
+
+        val profiles = CamcorderProfile.getAll(cameraId, quality)
+        val video = profiles!!.videoProfiles[0]
+        val audio = profiles.audioProfiles[0]
+        val profilesProxy = encoderProfilesProvider.getAll(quality)
+        val videoProxy = profilesProxy!!.videoProfiles[0]
+        val audioProxy = profilesProxy.audioProfiles[0]
+
+        assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+        assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+        assertThat(videoProxy.codec).isEqualTo(video.codec)
+        assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+        assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+        assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+        assertThat(videoProxy.width).isEqualTo(video.width)
+        assertThat(videoProxy.height).isEqualTo(video.height)
+        assertThat(videoProxy.profile).isEqualTo(video.profile)
+        assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
+        assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
+        assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
+        assertThat(audioProxy.codec).isEqualTo(audio.codec)
+        assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+        assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+        assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+        assertThat(audioProxy.channels).isEqualTo(audio.channels)
+        assertThat(audioProxy.profile).isEqualTo(audio.profile)
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    fun afterApi33_hasSameContentAsEncoderProfiles() {
+        assumeTrue(CamcorderProfile.hasProfile(quality))
+
+        val profiles = CamcorderProfile.getAll(cameraId, quality)
+        val video = profiles!!.videoProfiles[0]
+        val audio = profiles.audioProfiles[0]
+        val profilesProxy = encoderProfilesProvider.getAll(quality)
+        val videoProxy = profilesProxy!!.videoProfiles[0]
+        val audioProxy = profilesProxy.audioProfiles[0]
+
+        assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+        assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+        assertThat(videoProxy.codec).isEqualTo(video.codec)
+        assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+        assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+        assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+        assertThat(videoProxy.width).isEqualTo(video.width)
+        assertThat(videoProxy.height).isEqualTo(video.height)
+        assertThat(videoProxy.profile).isEqualTo(video.profile)
+        assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
+        assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
+        assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
+        assertThat(audioProxy.codec).isEqualTo(audio.codec)
+        assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+        assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+        assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+        assertThat(audioProxy.channels).isEqualTo(audio.channels)
+        assertThat(audioProxy.profile).isEqualTo(audio.profile)
+    }
+
+    // TODO: removes after b/265613005 is fixed
+    private fun skipTestOnProblematicBuildsOfCuttlefishApi33() {
+        // Skip test for b/265613005
+        Assume.assumeFalse(
+            "Cuttlefish has null VideoProfile issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 33 &&
+                Build.ID.startsWith("TP1A")
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java
new file mode 100644
index 0000000..a35596c
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java
@@ -0,0 +1,113 @@
+/*
+ * 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.camera2.internal;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat;
+
+/** An implementation that provides the {@link EncoderProfilesProxy}. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2EncoderProfilesProvider implements EncoderProfilesProvider {
+
+    private static final String TAG = "Camera2EncoderProfilesProvider";
+
+    private final boolean mHasValidCameraId;
+    private final String mCameraId;
+    private final int mIntCameraId;
+
+    public Camera2EncoderProfilesProvider(@NonNull String cameraId) {
+        mCameraId = cameraId;
+        boolean hasValidCameraId = false;
+        int intCameraId = -1;
+        try {
+            intCameraId = Integer.parseInt(cameraId);
+            hasValidCameraId = true;
+        } catch (NumberFormatException e) {
+            Logger.w(TAG, "Camera id is not an integer: " + cameraId
+                    + ", unable to create Camera2EncoderProfilesProvider");
+        }
+        mHasValidCameraId = hasValidCameraId;
+        mIntCameraId = intCameraId;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasProfile(int quality) {
+        if (!mHasValidCameraId) {
+            return false;
+        }
+
+        return CamcorderProfile.hasProfile(mIntCameraId, quality);
+    }
+
+    /** {@inheritDoc} */
+    @Nullable
+    @Override
+    public EncoderProfilesProxy getAll(int quality) {
+        if (!mHasValidCameraId) {
+            return null;
+        }
+
+        if (!CamcorderProfile.hasProfile(mIntCameraId, quality)) {
+            return null;
+        }
+
+        return getProfilesInternal(quality);
+    }
+
+    @Nullable
+    @SuppressWarnings("deprecation")
+    private EncoderProfilesProxy getProfilesInternal(int quality) {
+        if (Build.VERSION.SDK_INT >= 31) {
+            EncoderProfiles profiles = Api31Impl.getAll(mCameraId, quality);
+            return profiles != null ? EncoderProfilesProxyCompat.from(profiles) : null;
+        } else {
+            CamcorderProfile profile = null;
+            try {
+                profile = CamcorderProfile.get(mIntCameraId, quality);
+            } catch (RuntimeException e) {
+                // CamcorderProfile.get() will throw
+                // - RuntimeException if not able to retrieve camcorder profile params.
+                // - IllegalArgumentException if quality is not valid.
+                Logger.w(TAG, "Unable to get CamcorderProfile by quality: " + quality, e);
+            }
+            return profile != null ? EncoderProfilesProxyCompat.from(profile) : null;
+        }
+    }
+
+    @RequiresApi(31)
+    static class Api31Impl {
+        @DoNotInline
+        static EncoderProfiles getAll(String cameraId, int quality) {
+            return CamcorderProfile.getAll(cameraId, quality);
+        }
+
+        // This class is not instantiable.
+        private Api31Impl() {
+        }
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
index fb2d013..86a0c16 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
@@ -28,7 +28,7 @@
 import androidx.camera.camera2.internal.compat.workaround.CamcorderProfileResolutionValidator;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.ImageFormatConstants;
-import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -55,7 +55,7 @@
  *     @see CamcorderProfileResolutionValidator
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class CamcorderProfileResolutionQuirk implements Quirk {
+public class CamcorderProfileResolutionQuirk implements ProfileResolutionQuirk {
     private static final String TAG = "CamcorderProfileResolutionQuirk";
 
     static boolean load(@NonNull CameraCharacteristicsCompat characteristicsCompat) {
@@ -86,6 +86,7 @@
     }
 
     /** Returns the supported video resolutions. */
+    @Override
     @NonNull
     public List<Size> getSupportedResolutions() {
         return new ArrayList<>(mSupportedResolutions);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java
new file mode 100644
index 0000000..1e041d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java
@@ -0,0 +1,60 @@
+/*
+ * 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.impl;
+
+import android.media.CamcorderProfile;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * EncoderProfilesProvider is used to obtain the {@link EncoderProfilesProxy}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface EncoderProfilesProvider {
+
+    /**
+     * Checks if the quality is supported on this device.
+     *
+     * <p>The quality should be one of quality constants defined in {@link CamcorderProfile}.
+     */
+    boolean hasProfile(int quality);
+
+    /**
+     * Gets the {@link EncoderProfilesProxy} if the quality is supported on the device.
+     *
+     * <p>The quality should be one of quality constants defined in {@link CamcorderProfile}.
+     *
+     * @see #hasProfile(int)
+     */
+    @Nullable
+    EncoderProfilesProxy getAll(int quality);
+
+    /** An implementation that contains no data. */
+    EncoderProfilesProvider EMPTY = new EncoderProfilesProvider() {
+        @Override
+        public boolean hasProfile(int quality) {
+            return false;
+        }
+
+        @Nullable
+        @Override
+        public EncoderProfilesProxy getAll(int quality) {
+            return null;
+        }
+    };
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
new file mode 100644
index 0000000..963e3d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
@@ -0,0 +1,227 @@
+/*
+ * 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.impl;
+
+import static android.media.MediaRecorder.AudioEncoder.AAC;
+import static android.media.MediaRecorder.AudioEncoder.AAC_ELD;
+import static android.media.MediaRecorder.AudioEncoder.AMR_NB;
+import static android.media.MediaRecorder.AudioEncoder.AMR_WB;
+import static android.media.MediaRecorder.AudioEncoder.HE_AAC;
+import static android.media.MediaRecorder.AudioEncoder.OPUS;
+import static android.media.MediaRecorder.AudioEncoder.VORBIS;
+import static android.media.MediaRecorder.VideoEncoder.AV1;
+import static android.media.MediaRecorder.VideoEncoder.DOLBY_VISION;
+import static android.media.MediaRecorder.VideoEncoder.H263;
+import static android.media.MediaRecorder.VideoEncoder.H264;
+import static android.media.MediaRecorder.VideoEncoder.HEVC;
+import static android.media.MediaRecorder.VideoEncoder.MPEG_4_SP;
+import static android.media.MediaRecorder.VideoEncoder.VP8;
+import static android.media.MediaRecorder.VideoEncoder.VP9;
+
+import static java.util.Collections.unmodifiableList;
+
+import android.media.EncoderProfiles;
+import android.media.MediaRecorder;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.auto.value.AutoValue;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * EncoderProfilesProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@AutoValue
+public abstract class EncoderProfilesProxy {
+
+    /** Constant representing no codec profile. */
+    public static final int CODEC_PROFILE_NONE = -1;
+
+    /** Creates an EncoderProfilesProxy instance. */
+    @NonNull
+    public static EncoderProfilesProxy create(
+            int defaultDurationSeconds,
+            int recommendedFileFormat,
+            @NonNull List<AudioProfileProxy> audioProfiles,
+            @NonNull List<VideoProfileProxy> videoProfiles) {
+        return new AutoValue_EncoderProfilesProxy(
+                defaultDurationSeconds,
+                recommendedFileFormat,
+                unmodifiableList(new ArrayList<>(audioProfiles)),
+                unmodifiableList(new ArrayList<>(videoProfiles))
+        );
+    }
+
+    /** @see EncoderProfiles#getDefaultDurationSeconds() */
+    public abstract int getDefaultDurationSeconds();
+
+    /** @see EncoderProfiles#getRecommendedFileFormat() */
+    public abstract int getRecommendedFileFormat();
+
+    /** @see EncoderProfiles#getAudioProfiles() */
+    @SuppressWarnings("AutoValueImmutableFields")
+    @NonNull
+    public abstract List<AudioProfileProxy> getAudioProfiles();
+
+    /** @see EncoderProfiles#getVideoProfiles() */
+    @SuppressWarnings("AutoValueImmutableFields")
+    @NonNull
+    public abstract List<VideoProfileProxy> getVideoProfiles();
+
+    /**
+     * VideoProfileProxy defines the get methods that is mapping to the fields of
+     * {@link EncoderProfiles.VideoProfile}.
+     */
+    @AutoValue
+    public abstract static class VideoProfileProxy {
+
+        /** Constant representing no media type. */
+        public static final String MEDIA_TYPE_NONE = "video/none";
+
+        /** Constant representing bit depth 8. */
+        public static final int BIT_DEPTH_8 = 8;
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({H263, H264, HEVC, VP8, MPEG_4_SP, VP9, DOLBY_VISION, AV1,
+                MediaRecorder.VideoEncoder.DEFAULT})
+        public @interface VideoEncoder {
+        }
+
+        /** Creates a VideoProfileProxy instance. */
+        @NonNull
+        public static VideoProfileProxy create(
+                @VideoEncoder int codec,
+                @NonNull String mediaType,
+                int bitrate,
+                int frameRate,
+                int width,
+                int height,
+                int profile,
+                int bitDepth,
+                int chromaSubsampling,
+                int hdrFormat) {
+            return new AutoValue_EncoderProfilesProxy_VideoProfileProxy(
+                    codec,
+                    mediaType,
+                    bitrate,
+                    frameRate,
+                    width,
+                    height,
+                    profile,
+                    bitDepth,
+                    chromaSubsampling,
+                    hdrFormat
+            );
+        }
+
+        /** @see EncoderProfiles.VideoProfile#getCodec() */
+        @VideoEncoder
+        public abstract int getCodec();
+
+        /** @see EncoderProfiles.VideoProfile#getMediaType() */
+        @NonNull
+        public abstract String getMediaType();
+
+        /** @see EncoderProfiles.VideoProfile#getBitrate() */
+        public abstract int getBitrate();
+
+        /** @see EncoderProfiles.VideoProfile#getFrameRate() */
+        public abstract int getFrameRate();
+
+        /** @see EncoderProfiles.VideoProfile#getWidth() */
+        public abstract int getWidth();
+
+        /** @see EncoderProfiles.VideoProfile#getHeight() */
+        public abstract int getHeight();
+
+        /** @see EncoderProfiles.VideoProfile#getProfile() */
+        public abstract int getProfile();
+
+        /** @see EncoderProfiles.VideoProfile#getBitDepth() */
+        public abstract int getBitDepth();
+
+        /** @see EncoderProfiles.VideoProfile#getChromaSubsampling() */
+        public abstract int getChromaSubsampling();
+
+        /** @see EncoderProfiles.VideoProfile#getHdrFormat() */
+        public abstract int getHdrFormat();
+    }
+
+    /**
+     * AudioProfileProxy defines the get methods that is mapping to the fields of
+     * {@link EncoderProfiles.AudioProfile}.
+     */
+    @AutoValue
+    public abstract static class AudioProfileProxy {
+
+        /** Constant representing no media type. */
+        public static final String MEDIA_TYPE_NONE = "audio/none";
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({AAC, AAC_ELD, AMR_NB, AMR_WB, HE_AAC, OPUS, VORBIS,
+                MediaRecorder.AudioEncoder.DEFAULT})
+        public @interface AudioEncoder {
+        }
+
+        /** Creates an AudioProfileProxy instance. */
+        @NonNull
+        public static AudioProfileProxy create(
+                @AudioEncoder int codec,
+                @NonNull String mediaType,
+                int bitRate,
+                int sampleRate,
+                int channels,
+                int profile) {
+            return new AutoValue_EncoderProfilesProxy_AudioProfileProxy(
+                    codec,
+                    mediaType,
+                    bitRate,
+                    sampleRate,
+                    channels,
+                    profile
+            );
+        }
+
+        /** @see EncoderProfiles.AudioProfile#getCodec() */
+        @AudioEncoder
+        public abstract int getCodec();
+
+        /** @see EncoderProfiles.AudioProfile#getMediaType() */
+        @NonNull
+        public abstract String getMediaType();
+
+        /** @see EncoderProfiles.AudioProfile#getBitrate() */
+        public abstract int getBitrate();
+
+        /** @see EncoderProfiles.AudioProfile#getSampleRate() */
+        public abstract int getSampleRate();
+
+        /** @see EncoderProfiles.AudioProfile#getChannels() */
+        public abstract int getChannels();
+
+        /** @see EncoderProfiles.AudioProfile#getProfile() */
+        public abstract int getProfile();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java
new file mode 100644
index 0000000..0d84cd4
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java
@@ -0,0 +1,124 @@
+/*
+ * 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.impl;
+
+import android.media.EncoderProfiles;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Validates the video resolution of {@link EncoderProfiles}.
+ *
+ * @see ProfileResolutionQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class EncoderProfilesResolutionValidator {
+
+    @NonNull
+    private final List<ProfileResolutionQuirk> mQuirks;
+    @NonNull
+    private final Set<Size> mSupportedResolutions;
+
+    public EncoderProfilesResolutionValidator(@Nullable List<ProfileResolutionQuirk> quirks) {
+        mQuirks = new ArrayList<>();
+        if (quirks != null) {
+            mQuirks.addAll(quirks);
+        }
+
+        mSupportedResolutions = generateSupportedResolutions(quirks);
+    }
+
+    @NonNull
+    private Set<Size> generateSupportedResolutions(@Nullable List<ProfileResolutionQuirk> quirks) {
+        if (quirks == null || quirks.isEmpty()) {
+            return Collections.emptySet();
+        }
+
+        Set<Size> supportedResolutions = new HashSet<>(quirks.get(0).getSupportedResolutions());
+        for (int i = 1; i < quirks.size(); i++) {
+            supportedResolutions.retainAll(quirks.get(i).getSupportedResolutions());
+        }
+
+        return supportedResolutions;
+    }
+
+    /** Checks if this validator contains quirk. */
+    public boolean hasQuirk() {
+        return !mQuirks.isEmpty();
+    }
+
+    /** Checks if any video resolution of EncoderProfiles is valid. */
+    public boolean hasValidVideoResolution(@Nullable EncoderProfilesProxy profiles) {
+        if (profiles == null) {
+            return false;
+        }
+
+        if (!hasQuirk()) {
+            return !profiles.getVideoProfiles().isEmpty();
+        }
+
+        boolean hasValidResolution = false;
+        for (VideoProfileProxy videoProfile : profiles.getVideoProfiles()) {
+            Size videoSize = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+            if (mSupportedResolutions.contains(videoSize)) {
+                hasValidResolution = true;
+                break;
+            }
+        }
+
+        return hasValidResolution;
+    }
+
+    /** Returns an {@link EncoderProfilesProxy} that filters out invalid resolutions. */
+    @Nullable
+    public EncoderProfilesProxy filterInvalidVideoResolution(
+            @Nullable EncoderProfilesProxy profiles) {
+        if (profiles == null) {
+            return null;
+        }
+
+        if (!hasQuirk()) {
+            return profiles;
+        }
+
+        List<VideoProfileProxy> validVideoProfiles = new ArrayList<>();
+        for (VideoProfileProxy videoProfile : profiles.getVideoProfiles()) {
+            Size videoSize = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+            if (mSupportedResolutions.contains(videoSize)) {
+                validVideoProfiles.add(videoProfile);
+            }
+        }
+
+        return validVideoProfiles.isEmpty() ? null : EncoderProfilesProxy.create(
+                profiles.getDefaultDurationSeconds(),
+                profiles.getRecommendedFileFormat(),
+                profiles.getAudioProfiles(),
+                validVideoProfiles
+        );
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java
new file mode 100644
index 0000000..df32c6d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java
@@ -0,0 +1,74 @@
+/*
+ * 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.impl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
+
+import java.util.List;
+
+/**
+ * An implementation that provides the {@link EncoderProfilesProxy} whose video resolutions are
+ * validated.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ResolutionValidatedEncoderProfilesProvider implements EncoderProfilesProvider {
+
+    private final EncoderProfilesProvider mProvider;
+    private final EncoderProfilesResolutionValidator mEncoderProfilesResolutionValidator;
+
+    public ResolutionValidatedEncoderProfilesProvider(@NonNull EncoderProfilesProvider provider,
+            @NonNull Quirks quirks) {
+        mProvider = provider;
+        List<ProfileResolutionQuirk> resolutionQuirks = quirks.getAll(ProfileResolutionQuirk.class);
+        mEncoderProfilesResolutionValidator = new EncoderProfilesResolutionValidator(
+                resolutionQuirks);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasProfile(int quality) {
+        if (!mProvider.hasProfile(quality)) {
+            return false;
+        }
+
+        if (mEncoderProfilesResolutionValidator.hasQuirk()) {
+            EncoderProfilesProxy profiles = mProvider.getAll(quality);
+            return mEncoderProfilesResolutionValidator.hasValidVideoResolution(profiles);
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable
+    @Override
+    public EncoderProfilesProxy getAll(int quality) {
+        if (!mProvider.hasProfile(quality)) {
+            return null;
+        }
+
+        EncoderProfilesProxy profiles = mProvider.getAll(quality);
+        if (mEncoderProfilesResolutionValidator.hasQuirk()) {
+            profiles = mEncoderProfilesResolutionValidator.filterInvalidVideoResolution(profiles);
+        }
+
+        return profiles;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java
new file mode 100644
index 0000000..cd50106
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java
@@ -0,0 +1,65 @@
+/*
+ * 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.impl.compat;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+
+/**
+ * Helper for accessing features of {@link EncoderProfiles} and {@link CamcorderProfile} in a
+ * backwards compatible fashion.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class EncoderProfilesProxyCompat {
+
+    /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+    @RequiresApi(31)
+    @NonNull
+    public static EncoderProfilesProxy from(@NonNull EncoderProfiles encoderProfiles) {
+        if (Build.VERSION.SDK_INT >= 33) {
+            return EncoderProfilesProxyCompatApi33Impl.from(encoderProfiles);
+        } else if (Build.VERSION.SDK_INT >= 31) {
+            return EncoderProfilesProxyCompatApi31Impl.from(encoderProfiles);
+        } else {
+            throw new RuntimeException(
+                    "Unable to call from(EncoderProfiles) on API " + Build.VERSION.SDK_INT
+                            + ". Version 31 or higher required.");
+        }
+    }
+
+    /** Creates an EncoderProfilesProxy instance from {@link CamcorderProfile}. */
+    @NonNull
+    public static EncoderProfilesProxy from(@NonNull CamcorderProfile camcorderProfile) {
+        if (Build.VERSION.SDK_INT >= 31) {
+            throw new RuntimeException(
+                    "Should not use from(CamcorderProfile) on API " + Build.VERSION.SDK_INT
+                            + ". CamcorderProfile is deprecated on API 31, use "
+                            + "from(EncoderProfiles) instead.");
+        } else {
+            return EncoderProfilesProxyCompatBaseImpl.from(camcorderProfile);
+        }
+    }
+
+    // Class should not be instantiated.
+    private EncoderProfilesProxyCompat() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java
new file mode 100644
index 0000000..6e63f4b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.impl.compat;
+
+import android.media.EncoderProfiles;
+import android.media.EncoderProfiles.AudioProfile;
+import android.media.EncoderProfiles.VideoProfile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(31)
+class EncoderProfilesProxyCompatApi31Impl {
+
+    /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+    @NonNull
+    public static EncoderProfilesProxy from(
+            @NonNull EncoderProfiles encoderProfiles) {
+        return EncoderProfilesProxy.create(
+                encoderProfiles.getDefaultDurationSeconds(),
+                encoderProfiles.getRecommendedFileFormat(),
+                fromAudioProfiles(encoderProfiles.getAudioProfiles()),
+                fromVideoProfiles(encoderProfiles.getVideoProfiles())
+        );
+    }
+
+    /** Creates VideoProfileProxy instances from a list of {@link VideoProfile}. */
+    @NonNull
+    private static List<VideoProfileProxy> fromVideoProfiles(
+            @NonNull List<VideoProfile> profiles) {
+        List<VideoProfileProxy> proxies = new ArrayList<>();
+        for (VideoProfile profile : profiles) {
+            proxies.add(VideoProfileProxy.create(
+                    profile.getCodec(),
+                    profile.getMediaType(),
+                    profile.getBitrate(),
+                    profile.getFrameRate(),
+                    profile.getWidth(),
+                    profile.getHeight(),
+                    profile.getProfile(),
+                    VideoProfileProxy.BIT_DEPTH_8,
+                    VideoProfile.YUV_420,
+                    VideoProfile.HDR_NONE
+            ));
+        }
+        return proxies;
+    }
+
+    /** Creates AudioProfileProxy instances from a list of {@link AudioProfile}. */
+    @NonNull
+    private static List<AudioProfileProxy> fromAudioProfiles(
+            @NonNull List<AudioProfile> profiles) {
+        List<AudioProfileProxy> proxies = new ArrayList<>();
+        for (AudioProfile profile : profiles) {
+            proxies.add(AudioProfileProxy.create(
+                    profile.getCodec(),
+                    profile.getMediaType(),
+                    profile.getBitrate(),
+                    profile.getSampleRate(),
+                    profile.getChannels(),
+                    profile.getProfile()
+            ));
+        }
+        return proxies;
+    }
+
+    // Class should not be instantiated.
+    private EncoderProfilesProxyCompatApi31Impl() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java
new file mode 100644
index 0000000..9dd5e2d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.impl.compat;
+
+import android.media.EncoderProfiles;
+import android.media.EncoderProfiles.AudioProfile;
+import android.media.EncoderProfiles.VideoProfile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(33)
+class EncoderProfilesProxyCompatApi33Impl {
+
+    /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+    @NonNull
+    public static EncoderProfilesProxy from(
+            @NonNull EncoderProfiles encoderProfiles) {
+        return EncoderProfilesProxy.create(
+                encoderProfiles.getDefaultDurationSeconds(),
+                encoderProfiles.getRecommendedFileFormat(),
+                fromAudioProfiles(encoderProfiles.getAudioProfiles()),
+                fromVideoProfiles(encoderProfiles.getVideoProfiles())
+        );
+    }
+
+    /** Creates VideoProfileProxy instances from a list of {@link VideoProfile}. */
+    @NonNull
+    private static List<VideoProfileProxy> fromVideoProfiles(
+            @NonNull List<VideoProfile> profiles) {
+        List<VideoProfileProxy> proxies = new ArrayList<>();
+        for (VideoProfile profile : profiles) {
+            proxies.add(VideoProfileProxy.create(
+                    profile.getCodec(),
+                    profile.getMediaType(),
+                    profile.getBitrate(),
+                    profile.getFrameRate(),
+                    profile.getWidth(),
+                    profile.getHeight(),
+                    profile.getProfile(),
+                    profile.getBitDepth(),
+                    profile.getChromaSubsampling(),
+                    profile.getHdrFormat()
+            ));
+        }
+        return proxies;
+    }
+
+    /** Creates AudioProfileProxy instances from a list of {@link AudioProfile}. */
+    @NonNull
+    private static List<AudioProfileProxy> fromAudioProfiles(
+            @NonNull List<AudioProfile> profiles) {
+        List<AudioProfileProxy> proxies = new ArrayList<>();
+        for (AudioProfile profile : profiles) {
+            proxies.add(AudioProfileProxy.create(
+                    profile.getCodec(),
+                    profile.getMediaType(),
+                    profile.getBitrate(),
+                    profile.getSampleRate(),
+                    profile.getChannels(),
+                    profile.getProfile()
+            ));
+        }
+        return proxies;
+    }
+
+    // Class should not be instantiated.
+    private EncoderProfilesProxyCompatApi33Impl() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java
new file mode 100644
index 0000000..cacc8d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java
@@ -0,0 +1,196 @@
+/*
+ * 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.impl.compat;
+
+import static android.media.MediaRecorder.AudioEncoder.AAC;
+import static android.media.MediaRecorder.AudioEncoder.AAC_ELD;
+import static android.media.MediaRecorder.AudioEncoder.AMR_NB;
+import static android.media.MediaRecorder.AudioEncoder.AMR_WB;
+import static android.media.MediaRecorder.AudioEncoder.HE_AAC;
+import static android.media.MediaRecorder.AudioEncoder.OPUS;
+import static android.media.MediaRecorder.AudioEncoder.VORBIS;
+import static android.media.MediaRecorder.VideoEncoder.AV1;
+import static android.media.MediaRecorder.VideoEncoder.DOLBY_VISION;
+import static android.media.MediaRecorder.VideoEncoder.H263;
+import static android.media.MediaRecorder.VideoEncoder.H264;
+import static android.media.MediaRecorder.VideoEncoder.HEVC;
+import static android.media.MediaRecorder.VideoEncoder.MPEG_4_SP;
+import static android.media.MediaRecorder.VideoEncoder.VP8;
+import static android.media.MediaRecorder.VideoEncoder.VP9;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class EncoderProfilesProxyCompatBaseImpl {
+
+    /** Creates an EncoderProfilesProxy instance from {@link CamcorderProfile}. */
+    @NonNull
+    public static EncoderProfilesProxy from(
+            @NonNull CamcorderProfile camcorderProfile) {
+        return EncoderProfilesProxy.create(
+                camcorderProfile.duration,
+                camcorderProfile.fileFormat,
+                toAudioProfiles(camcorderProfile),
+                toVideoProfiles(camcorderProfile)
+        );
+    }
+
+    /** Creates VideoProfileProxy instances from {@link CamcorderProfile}. */
+    @NonNull
+    private static List<VideoProfileProxy> toVideoProfiles(
+            @NonNull CamcorderProfile camcorderProfile) {
+        List<VideoProfileProxy> proxies = new ArrayList<>();
+        proxies.add(VideoProfileProxy.create(
+                camcorderProfile.videoCodec,
+                getVideoCodecMimeType(camcorderProfile.videoCodec),
+                camcorderProfile.videoBitRate,
+                camcorderProfile.videoFrameRate,
+                camcorderProfile.videoFrameWidth,
+                camcorderProfile.videoFrameHeight,
+                EncoderProfilesProxy.CODEC_PROFILE_NONE,
+                VideoProfileProxy.BIT_DEPTH_8,
+                EncoderProfiles.VideoProfile.YUV_420,
+                EncoderProfiles.VideoProfile.HDR_NONE
+        ));
+        return proxies;
+    }
+
+    /** Creates AudioProfileProxy instances from {@link CamcorderProfile}. */
+    @NonNull
+    private static List<AudioProfileProxy> toAudioProfiles(
+            @NonNull CamcorderProfile camcorderProfile) {
+        List<AudioProfileProxy> proxies = new ArrayList<>();
+        proxies.add(AudioProfileProxy.create(
+                camcorderProfile.audioCodec,
+                getAudioCodecMimeType(camcorderProfile.audioCodec),
+                camcorderProfile.audioBitRate,
+                camcorderProfile.audioSampleRate,
+                camcorderProfile.audioChannels,
+                getRequiredAudioProfile(camcorderProfile.audioCodec)
+        ));
+        return proxies;
+    }
+
+    /**
+     * Returns a mime-type string for the given video codec type.
+     *
+     * @return A mime-type string or {@link VideoProfileProxy#MEDIA_TYPE_NONE} if the codec type is
+     * {@link MediaRecorder.VideoEncoder#DEFAULT}, as this type is under-defined and cannot be
+     * resolved to a specific mime type without more information.
+     */
+    @NonNull
+    private static String getVideoCodecMimeType(
+            @VideoProfileProxy.VideoEncoder int codec) {
+        switch (codec) {
+            // Mime-type definitions taken from
+            // frameworks/av/media/libstagefright/foundation/MediaDefs.cpp
+            case H263:
+                return MediaFormat.MIMETYPE_VIDEO_H263;
+            case H264:
+                return MediaFormat.MIMETYPE_VIDEO_AVC;
+            case HEVC:
+                return MediaFormat.MIMETYPE_VIDEO_HEVC;
+            case VP8:
+                return MediaFormat.MIMETYPE_VIDEO_VP8;
+            case MPEG_4_SP:
+                return MediaFormat.MIMETYPE_VIDEO_MPEG4;
+            case VP9:
+                return MediaFormat.MIMETYPE_VIDEO_VP9;
+            case DOLBY_VISION:
+                return MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION;
+            case AV1:
+                return MediaFormat.MIMETYPE_VIDEO_AV1;
+            case MediaRecorder.VideoEncoder.DEFAULT:
+                break;
+        }
+
+        return VideoProfileProxy.MEDIA_TYPE_NONE;
+    }
+
+    /**
+     * Returns a mime-type string for the given audio codec type.
+     *
+     * @return A mime-type string or {@link AudioProfileProxy#MEDIA_TYPE_NONE} if the codec type is
+     * {@link android.media.MediaRecorder.AudioEncoder#DEFAULT}, as this type is under-defined
+     * and cannot be resolved to a specific mime type without more information.
+     */
+    @NonNull
+    private static String getAudioCodecMimeType(@AudioProfileProxy.AudioEncoder int codec) {
+        // Mime-type definitions taken from
+        // frameworks/av/media/libstagefright/foundation/MediaDefs.cpp
+        switch (codec) {
+            case AAC: // Should use aac-profile LC
+            case HE_AAC: // Should use aac-profile HE
+            case AAC_ELD: // Should use aac-profile ELD
+                return MediaFormat.MIMETYPE_AUDIO_AAC;
+            case AMR_NB:
+                return MediaFormat.MIMETYPE_AUDIO_AMR_NB;
+            case AMR_WB:
+                return MediaFormat.MIMETYPE_AUDIO_AMR_WB;
+            case OPUS:
+                return MediaFormat.MIMETYPE_AUDIO_OPUS;
+            case VORBIS:
+                return MediaFormat.MIMETYPE_AUDIO_VORBIS;
+            case MediaRecorder.AudioEncoder.DEFAULT:
+                break;
+        }
+
+        return AudioProfileProxy.MEDIA_TYPE_NONE;
+    }
+
+    /**
+     * Returns the required audio profile for the given audio encoder.
+     *
+     * <p>For example, this can be used to differentiate between AAC encoders
+     * {@link android.media.MediaRecorder.AudioEncoder#AAC},
+     * {@link android.media.MediaRecorder.AudioEncoder#AAC_ELD},
+     * and {@link android.media.MediaRecorder.AudioEncoder#HE_AAC}.
+     * Should be used with the {@link MediaCodecInfo.CodecProfileLevel#profile} field.
+     *
+     * @return The profile required by the audio codec. If no profile is required, returns
+     * {@link EncoderProfilesProxy#CODEC_PROFILE_NONE}.
+     */
+    private static int getRequiredAudioProfile(@AudioProfileProxy.AudioEncoder int codec) {
+        switch (codec) {
+            case AAC:
+                return MediaCodecInfo.CodecProfileLevel.AACObjectLC;
+            case AAC_ELD:
+                return MediaCodecInfo.CodecProfileLevel.AACObjectELD;
+            case HE_AAC:
+                return MediaCodecInfo.CodecProfileLevel.AACObjectHE;
+            default:
+                return EncoderProfilesProxy.CODEC_PROFILE_NONE;
+        }
+    }
+
+    // Class should not be instantiated.
+    private EncoderProfilesProxyCompatBaseImpl() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java
new file mode 100644
index 0000000..cdb5a87
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java
@@ -0,0 +1,41 @@
+/*
+ * 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.impl.quirk;
+
+import android.media.EncoderProfiles;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * A Quirk interface which denotes that CameraX should validate video resolutions returned from
+ * {@link EncoderProfiles} instead of using them directly.
+ *
+ * <p>Subclasses of this quirk should provide a list of supported resolutions for CameraX to
+ * verify.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface ProfileResolutionQuirk extends Quirk {
+
+    /** Returns a list of supported resolutions. */
+    @NonNull
+    List<Size> getSupportedResolutions();
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt
new file mode 100644
index 0000000..57b9338
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.impl
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+import androidx.camera.testing.EncoderProfilesUtil
+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
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class EncoderProfilesResolutionValidatorTest {
+
+    @Test
+    fun noQuirk_alwaysValid() {
+        val validator = EncoderProfilesResolutionValidator(null)
+
+        assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_2160P)).isTrue()
+        assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_720P)).isTrue()
+    }
+
+    @Test
+    fun hasQuirk_shouldCheckSupportedResolutions() {
+        val quirk = createFakeProfileResolutionQuirk(
+            supportedResolution = arrayOf(EncoderProfilesUtil.RESOLUTION_2160P)
+        )
+        val validator = EncoderProfilesResolutionValidator(listOf(quirk))
+
+        assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_2160P)).isTrue()
+        assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_720P)).isFalse()
+    }
+
+    @Test
+    fun nullProfile_notValid() {
+        val quirk = createFakeProfileResolutionQuirk(
+            supportedResolution = arrayOf(EncoderProfilesUtil.RESOLUTION_2160P)
+        )
+        val validator = EncoderProfilesResolutionValidator(listOf(quirk))
+
+        assertThat(validator.hasValidVideoResolution(null)).isFalse()
+    }
+
+    private fun createFakeProfileResolutionQuirk(
+        supportedResolution: Array<Size> = emptyArray()
+    ): ProfileResolutionQuirk {
+        return FakeQuirk(supportedResolution)
+    }
+
+    class FakeQuirk(private val supportedResolutions: Array<Size>) : ProfileResolutionQuirk {
+
+        override fun getSupportedResolutions(): MutableList<Size> {
+            return supportedResolutions.toMutableList()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..198bc4c
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.impl
+
+import android.media.CamcorderProfile.QUALITY_1080P
+import android.media.CamcorderProfile.QUALITY_2160P
+import android.media.CamcorderProfile.QUALITY_480P
+import android.media.CamcorderProfile.QUALITY_720P
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_1080P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_2160P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_480P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_720P
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_1080P
+import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
+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
+
+private val PAIR_2160 = Pair(QUALITY_2160P, PROFILES_2160P)
+private val PAIR_1080 = Pair(QUALITY_1080P, PROFILES_1080P)
+private val PAIR_720 = Pair(QUALITY_720P, PROFILES_720P)
+private val PAIR_480 = Pair(QUALITY_480P, PROFILES_480P)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionValidatedEncoderProfilesProviderTest {
+
+    private val defaultProvider = createFakeEncoderProfilesProvider(
+        arrayOf(PAIR_2160, PAIR_1080, PAIR_720, PAIR_480)
+    )
+
+    @Test
+    fun hasNoProfile_canNotGetProfiles() {
+        val quirks = createQuirksWithProfileResolutionQuirk(
+            supportedResolution = arrayOf(RESOLUTION_1080P)
+        )
+        val emptyProvider = createFakeEncoderProfilesProvider()
+        val provider = ResolutionValidatedEncoderProfilesProvider(emptyProvider, quirks)
+
+        assertThat(provider.hasProfile(QUALITY_2160P)).isFalse()
+        assertThat(provider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(provider.hasProfile(QUALITY_720P)).isFalse()
+        assertThat(provider.hasProfile(QUALITY_480P)).isFalse()
+        assertThat(provider.getAll(QUALITY_2160P)).isNull()
+        assertThat(provider.getAll(QUALITY_1080P)).isNull()
+        assertThat(provider.getAll(QUALITY_720P)).isNull()
+        assertThat(provider.getAll(QUALITY_480P)).isNull()
+    }
+
+    @Test
+    fun hasQuirk_canOnlyGetSupportedProfiles() {
+        val quirks = createQuirksWithProfileResolutionQuirk(
+            supportedResolution = arrayOf(RESOLUTION_1080P)
+        )
+        val provider = ResolutionValidatedEncoderProfilesProvider(defaultProvider, quirks)
+
+        assertThat(provider.hasProfile(QUALITY_2160P)).isFalse()
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        assertThat(provider.hasProfile(QUALITY_720P)).isFalse()
+        assertThat(provider.hasProfile(QUALITY_480P)).isFalse()
+        assertThat(provider.getAll(QUALITY_2160P)).isNull()
+        assertThat(provider.getAll(QUALITY_1080P)).isNotNull()
+        assertThat(provider.getAll(QUALITY_720P)).isNull()
+        assertThat(provider.getAll(QUALITY_480P)).isNull()
+    }
+
+    @Test
+    fun hasNoQuirk_canGetProfiles() {
+        val quirks = Quirks(emptyList())
+        val provider = ResolutionValidatedEncoderProfilesProvider(defaultProvider, quirks)
+
+        assertThat(provider.hasProfile(QUALITY_2160P)).isTrue()
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        assertThat(provider.hasProfile(QUALITY_720P)).isTrue()
+        assertThat(provider.hasProfile(QUALITY_480P)).isTrue()
+        assertThat(provider.getAll(QUALITY_2160P)).isNotNull()
+        assertThat(provider.getAll(QUALITY_1080P)).isNotNull()
+        assertThat(provider.getAll(QUALITY_720P)).isNotNull()
+        assertThat(provider.getAll(QUALITY_480P)).isNotNull()
+    }
+
+    private fun createFakeEncoderProfilesProvider(
+        qualityToProfilesPairs: Array<Pair<Int, EncoderProfilesProxy>> = emptyArray()
+    ): EncoderProfilesProvider {
+        return FakeEncoderProfilesProvider.Builder().also { builder ->
+            for (pair in qualityToProfilesPairs) {
+                builder.add(pair.first, pair.second)
+            }
+        }.build()
+    }
+
+    private fun createQuirksWithProfileResolutionQuirk(
+        supportedResolution: Array<Size> = emptyArray()
+    ): Quirks {
+        return Quirks(listOf(FakeQuirk(supportedResolution)))
+    }
+
+    class FakeQuirk(private val supportedResolutions: Array<Size>) : ProfileResolutionQuirk {
+
+        override fun getSupportedResolutions(): MutableList<Size> {
+            return supportedResolutions.toMutableList()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
new file mode 100644
index 0000000..f3d56b2
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
@@ -0,0 +1,189 @@
+/*
+ * 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.testing;
+
+import android.media.EncoderProfiles;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.Collections;
+
+/**
+ * Utility methods for testing {@link EncoderProfiles} related classes, including predefined
+ * resolutions, attributes and {@link EncoderProfilesProxy}, which can be used directly on the
+ * unit tests.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class EncoderProfilesUtil {
+
+    /** Resolution for QCIF. */
+    public static final Size RESOLUTION_QCIF = new Size(176, 144);
+    /** Resolution for QVGA. */
+    public static final Size RESOLUTION_QVGA = new Size(320, 240);
+    /** Resolution for CIF. */
+    public static final Size RESOLUTION_CIF = new Size(352, 288);
+    /** Resolution for VGA. */
+    public static final Size RESOLUTION_VGA = new Size(640, 480);
+    /** Resolution for 480P. */
+    public static final Size RESOLUTION_480P = new Size(720, 480); /* 640, 704 or 720 x 480 */
+    /** Resolution for 720P. */
+    public static final Size RESOLUTION_720P = new Size(1280, 720);
+    /** Resolution for 1080P. */
+    public static final Size RESOLUTION_1080P = new Size(1920, 1080); /* 1920 x 1080 or 1088 */
+    /** Resolution for 2K. */
+    public static final Size RESOLUTION_2K = new Size(2048, 1080);
+    /** Resolution for QHD. */
+    public static final Size RESOLUTION_QHD = new Size(2560, 1440);
+    /** Resolution for 2160P. */
+    public static final Size RESOLUTION_2160P = new Size(3840, 2160);
+    /** Resolution for 4KDCI. */
+    public static final Size RESOLUTION_4KDCI = new Size(4096, 2160);
+
+    /** Default duration. */
+    public static final int DEFAULT_DURATION = 30;
+    /** Default output format. */
+    public static final int DEFAULT_OUTPUT_FORMAT = MediaRecorder.OutputFormat.MPEG_4;
+    /** Default video codec. */
+    public static final int DEFAULT_VIDEO_CODEC = MediaRecorder.VideoEncoder.H264;
+    /** Default media type. */
+    public static final String DEFAULT_VIDEO_MEDIA_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+    /** Default video bitrate. */
+    public static final int DEFAULT_VIDEO_BITRATE = 8 * 1024 * 1024;
+    /** Default video frame rate. */
+    public static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+    /** Default video code profile. */
+    public static final int DEFAULT_VIDEO_PROFILE = EncoderProfilesProxy.CODEC_PROFILE_NONE;
+    /** Default bit depth. */
+    public static final int DEFAULT_VIDEO_BIT_DEPTH = VideoProfileProxy.BIT_DEPTH_8;
+    /** Default chroma subsampling. */
+    public static final int DEFAULT_VIDEO_CHROMA_SUBSAMPLING = EncoderProfiles.VideoProfile.YUV_420;
+    /** Default hdr format. */
+    public static final int DEFAULT_VIDEO_HDR_FORMAT = EncoderProfiles.VideoProfile.HDR_NONE;
+    /** Default audio codec. */
+    public static final int DEFAULT_AUDIO_CODEC = MediaRecorder.AudioEncoder.AAC;
+    /** Default media type. */
+    public static final String DEFAULT_AUDIO_MEDIA_TYPE = MediaFormat.MIMETYPE_AUDIO_AAC;
+    /** Default audio bitrate. */
+    public static final int DEFAULT_AUDIO_BITRATE = 192_000;
+    /** Default audio sample rate. */
+    public static final int DEFAULT_AUDIO_SAMPLE_RATE = 48_000;
+    /** Default channel count. */
+    public static final int DEFAULT_AUDIO_CHANNELS = 1;
+    /** Default audio code profile. */
+    public static final int DEFAULT_AUDIO_PROFILE = EncoderProfilesProxy.CODEC_PROFILE_NONE;
+
+    public static final EncoderProfilesProxy PROFILES_QCIF = createFakeEncoderProfilesProxy(
+            RESOLUTION_QCIF.getWidth(),
+            RESOLUTION_QCIF.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_QVGA = createFakeEncoderProfilesProxy(
+            RESOLUTION_QVGA.getWidth(),
+            RESOLUTION_QVGA.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_CIF = createFakeEncoderProfilesProxy(
+            RESOLUTION_CIF.getWidth(),
+            RESOLUTION_CIF.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_VGA = createFakeEncoderProfilesProxy(
+            RESOLUTION_VGA.getWidth(),
+            RESOLUTION_VGA.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_480P = createFakeEncoderProfilesProxy(
+            RESOLUTION_480P.getWidth(),
+            RESOLUTION_480P.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_720P = createFakeEncoderProfilesProxy(
+            RESOLUTION_720P.getWidth(),
+            RESOLUTION_720P.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_1080P = createFakeEncoderProfilesProxy(
+            RESOLUTION_1080P.getWidth(),
+            RESOLUTION_1080P.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_2K = createFakeEncoderProfilesProxy(
+            RESOLUTION_2K.getWidth(),
+            RESOLUTION_2K.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_QHD = createFakeEncoderProfilesProxy(
+            RESOLUTION_QHD.getWidth(),
+            RESOLUTION_QHD.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_2160P = createFakeEncoderProfilesProxy(
+            RESOLUTION_2160P.getWidth(),
+            RESOLUTION_2160P.getHeight()
+    );
+
+    public static final EncoderProfilesProxy PROFILES_4KDCI = createFakeEncoderProfilesProxy(
+            RESOLUTION_4KDCI.getWidth(),
+            RESOLUTION_4KDCI.getHeight()
+    );
+
+    /** A utility method to create an EncoderProfilesProxy with some default values. */
+    @NonNull
+    public static EncoderProfilesProxy createFakeEncoderProfilesProxy(
+            int videoFrameWidth,
+            int videoFrameHeight
+    ) {
+        VideoProfileProxy videoProfile = VideoProfileProxy.create(
+                DEFAULT_VIDEO_CODEC,
+                DEFAULT_VIDEO_MEDIA_TYPE,
+                DEFAULT_VIDEO_BITRATE,
+                DEFAULT_VIDEO_FRAME_RATE,
+                videoFrameWidth,
+                videoFrameHeight,
+                DEFAULT_VIDEO_PROFILE,
+                DEFAULT_VIDEO_BIT_DEPTH,
+                DEFAULT_VIDEO_CHROMA_SUBSAMPLING,
+                DEFAULT_VIDEO_HDR_FORMAT
+        );
+        AudioProfileProxy audioProfile = AudioProfileProxy.create(
+                DEFAULT_AUDIO_CODEC,
+                DEFAULT_AUDIO_MEDIA_TYPE,
+                DEFAULT_AUDIO_BITRATE,
+                DEFAULT_AUDIO_SAMPLE_RATE,
+                DEFAULT_AUDIO_CHANNELS,
+                DEFAULT_AUDIO_PROFILE
+        );
+
+        return EncoderProfilesProxy.create(
+                DEFAULT_DURATION,
+                DEFAULT_OUTPUT_FORMAT,
+                Collections.singletonList(audioProfile),
+                Collections.singletonList(videoProfile)
+        );
+    }
+
+    // This class is not instantiable.
+    private EncoderProfilesUtil() {
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java
new file mode 100644
index 0000000..a4d1e46
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java
@@ -0,0 +1,85 @@
+/*
+ * 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.testing.fakes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A fake implementation of the {@link EncoderProfilesProvider} and used for test.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class FakeEncoderProfilesProvider implements EncoderProfilesProvider {
+
+    private final Map<Integer, EncoderProfilesProxy> mQualityToProfileMap;
+
+    FakeEncoderProfilesProvider(@NonNull Map<Integer, EncoderProfilesProxy> qualityToProfileMap) {
+        mQualityToProfileMap = qualityToProfileMap;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasProfile(int quality) {
+        return mQualityToProfileMap.get(quality) != null;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable
+    @Override
+    public EncoderProfilesProxy getAll(int quality) {
+        return mQualityToProfileMap.get(quality);
+    }
+
+    /**
+     * The builder to create a FakeEncoderProfilesProvider instance.
+     */
+    @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+    public static class Builder {
+
+        private final Map<Integer, EncoderProfilesProxy> mQualityToProfileMap = new HashMap<>();
+
+        /**
+         * Adds a quality and its corresponding profiles.
+         */
+        @NonNull
+        public Builder add(int quality, @NonNull EncoderProfilesProxy profiles) {
+            mQualityToProfileMap.put(quality, profiles);
+            return this;
+        }
+
+        /**
+         * Adds qualities and their corresponding profiles.
+         */
+        @NonNull
+        public Builder addAll(@NonNull Map<Integer, EncoderProfilesProxy> qualityToProfileMap) {
+            mQualityToProfileMap.putAll(qualityToProfileMap);
+            return this;
+        }
+
+        /** Builds the FakeEncoderProfilesProvider instance. */
+        @NonNull
+        public FakeEncoderProfilesProvider build() {
+            return new FakeEncoderProfilesProvider(mQualityToProfileMap);
+        }
+    }
+}