Merge changes from topic "Extensions1_3_supportedCameraOps" into androidx-main

* changes:
  Fix the crash when invoking ImageCaptureExtenderImpl.getAvailableCaptureRequestKeys()
  [Restrict CameraControl] Apply supportedCameraOperations to the CameraControl and CameraInfo.
  [Restrict CameraControl] Add supportedCameraOperations to SessionProcessor
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt
index d0fa9e3..fc3bf34 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt
@@ -48,11 +48,12 @@
 import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.testing.VerifyResultListener
+import androidx.camera.camera2.pipe.testing.toCameraControlAdapter
+import androidx.camera.camera2.pipe.testing.toCameraInfoAdapter
 import androidx.camera.core.CameraControl
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -105,7 +106,7 @@
             CameraSelector.LENS_FACING_BACK
         ).build()
         camera = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-        cameraControl = camera.cameraControl as CameraControlAdapter
+        cameraControl = camera.cameraControl.toCameraControlAdapter()
         camera2CameraControl = cameraControl.camera2cameraControl
         comboListener = camera2CameraControl.requestListener
     }
@@ -438,7 +439,7 @@
                 Context.CAMERA_SERVICE
             ) as CameraManager
         val characteristics = cameraManager.getCameraCharacteristics(
-            (camera.cameraInfo as CameraInfoInternal).cameraId
+            camera.cameraInfo.toCameraInfoAdapter().cameraId
         )
 
         val maxDigitalZoom = characteristics.get(
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
index a072374..a4eb05c 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
@@ -61,6 +61,8 @@
 import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.testing.VerifyResultListener
+import androidx.camera.camera2.pipe.testing.toCameraControlAdapter
+import androidx.camera.camera2.pipe.testing.toCameraInfoAdapter
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraControl
 import androidx.camera.core.CameraSelector
@@ -70,7 +72,6 @@
 import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory
 import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.Quirks
 import androidx.camera.core.impl.SessionConfig
@@ -147,7 +148,7 @@
             CameraSelector.LENS_FACING_BACK
         ).build()
         camera = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-        cameraControl = camera.cameraControl as CameraControlAdapter
+        cameraControl = camera.cameraControl.toCameraControlAdapter()
         comboListener = cameraControl.camera2cameraControl.requestListener
 
         characteristics = CameraUtil.getCameraCharacteristics(
@@ -473,7 +474,7 @@
     }
 
     private fun Camera.getMaxSupportedZoomRatio(): Float {
-        return cameraInfo.zoomState.value!!.maxZoomRatio
+        return cameraInfo.toCameraInfoAdapter().zoomState.value!!.maxZoomRatio
     }
 
     private suspend fun verifyRequestOptions() {
@@ -506,7 +507,7 @@
             cameraSelector,
             *useCases,
         )
-        cameraControl = camera.cameraControl as CameraControlAdapter
+        cameraControl = camera.cameraControl.toCameraControlAdapter()
     }
 
     private fun createFakeRecordingUseCase(): FakeUseCase {
@@ -618,7 +619,7 @@
     }
 
     private fun Camera.getCameraQuirks(): Quirks {
-        return (cameraInfo as? CameraInfoInternal)?.cameraQuirks!!
+        return cameraInfo.toCameraInfoAdapter().cameraQuirks
     }
 
     private fun CameraCharacteristics.isAfModeSupported(
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
index 3f62b210..c8d8825a 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -24,6 +24,7 @@
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
+import androidx.camera.camera2.pipe.testing.toCameraControlAdapter
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.impl.CameraCaptureCallback
@@ -101,7 +102,7 @@
             }
         }
 
-        cameraControl = camera!!.cameraControl as CameraControlAdapter
+        cameraControl = camera!!.cameraControl.toCameraControlAdapter()
     }
 
     @After
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
index 092a7c6..1c7730d 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.testing.VerifyResultListener
+import androidx.camera.camera2.pipe.testing.toCameraControlAdapter
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
@@ -92,7 +93,7 @@
             CameraSelector.LENS_FACING_BACK
         ).build()
         camera = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-        cameraControl = camera.cameraControl as CameraControlAdapter
+        cameraControl = camera.cameraControl.toCameraControlAdapter()
 
         @OptIn(ExperimentalCamera2Interop::class)
         comboListener = cameraControl.camera2cameraControl.requestListener
@@ -267,6 +268,6 @@
                 }
             },
         )
-        cameraControl = camera.cameraControl as CameraControlAdapter
+        cameraControl = camera.cameraControl.toCameraControlAdapter()
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/TapToFocusDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/TapToFocusDeviceTest.kt
index 0366575..a1d918e 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/TapToFocusDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/TapToFocusDeviceTest.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.testing.VerifyResultListener
+import androidx.camera.camera2.pipe.testing.toCameraControlAdapter
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.ImageCapture
@@ -115,7 +116,7 @@
             imageCapture
         )
 
-        cameraControl = camera.cameraControl as CameraControlAdapter
+        cameraControl = camera.cameraControl.toCameraControlAdapter()
 
         @OptIn(ExperimentalCamera2Interop::class)
         comboListener = cameraControl.camera2cameraControl.requestListener
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUtil.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUtil.kt
new file mode 100644
index 0000000..9a7f3fc
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUtil.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.testing
+
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+fun CameraControl.toCameraControlAdapter(): CameraControlAdapter {
+    return ((this as CameraControlInternal).implementation) as CameraControlAdapter
+}
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+fun CameraInfo.toCameraInfoAdapter(): CameraInfoAdapter {
+    return ((this as CameraInfoInternal).implementation) as CameraInfoAdapter
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
index d692dbe..2ca7aeb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
@@ -27,6 +27,7 @@
 import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.core.CameraControl
+import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.core.util.Preconditions
 import com.google.common.util.concurrent.ListenableFuture
@@ -186,11 +187,12 @@
          */
         @JvmStatic
         fun from(cameraControl: CameraControl): Camera2CameraControl {
+            var cameraControlImpl = (cameraControl as CameraControlInternal).implementation
             Preconditions.checkArgument(
-                cameraControl is CameraControlAdapter,
+                cameraControlImpl is CameraControlAdapter,
                 "CameraControl doesn't contain Camera2 implementation."
             )
-            return (cameraControl as CameraControlAdapter).camera2cameraControl
+            return (cameraControlImpl as CameraControlAdapter).camera2cameraControl
         }
 
         /**
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt
index 2372dd3..3094fc4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt
@@ -23,6 +23,7 @@
 import androidx.camera.camera2.pipe.integration.compat.workaround.getSafely
 import androidx.camera.camera2.pipe.integration.impl.CameraProperties
 import androidx.camera.core.CameraInfo
+import androidx.camera.core.impl.CameraInfoInternal
 import androidx.core.util.Preconditions
 
 /**
@@ -88,11 +89,12 @@
          */
         @JvmStatic
         fun from(@Suppress("UNUSED_PARAMETER") cameraInfo: CameraInfo): Camera2CameraInfo {
+            var cameraInfoImpl = (cameraInfo as CameraInfoInternal).implementation
             Preconditions.checkArgument(
-                cameraInfo is CameraInfoAdapter,
+                cameraInfoImpl is CameraInfoAdapter,
                 "CameraInfo doesn't contain Camera2 implementation."
             )
-            return (cameraInfo as CameraInfoAdapter).camera2CameraInfo
+            return (cameraInfoImpl as CameraInfoAdapter).camera2CameraInfo
         }
 
         /**
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
index 2f3e542..0a53825 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
@@ -59,6 +59,7 @@
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
+import androidx.camera.camera2.internal.util.TestUtil;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
@@ -431,7 +432,7 @@
                 imageCapture);
 
         Camera2CameraControlImpl camera2CameraControlImpl =
-                (Camera2CameraControlImpl) mCamera.getCameraControl();
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
 
         CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
         CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
@@ -458,7 +459,7 @@
                 ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
                 imageAnalysis);
 
-        return (Camera2CameraControlImpl) mCamera.getCameraControl();
+        return TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
     }
 
     private <T> void assertArraySize(T[] array, int expectedSize) {
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
index 8143d94..3e6a382 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 
 import androidx.camera.camera2.Camera2Config;
+import androidx.camera.camera2.internal.util.TestUtil;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
 import androidx.camera.core.ImageAnalysis;
@@ -82,8 +83,8 @@
         // Make ImageAnalysis active.
         imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
         mCamera = CameraUtil.createCameraAndAttachUseCase(context, cameraSelector, imageAnalysis);
-        Camera2CameraControlImpl cameraControl = (Camera2CameraControlImpl)
-                mCamera.getCameraControl();
+        Camera2CameraControlImpl cameraControl =
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
 
         mTorchControl = cameraControl.getTorchControl();
     }
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/TestUtil.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/TestUtil.java
new file mode 100644
index 0000000..cd4f5ef
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/util/TestUtil.java
@@ -0,0 +1,48 @@
+/*
+ * 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.util;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.Camera2CameraControlImpl;
+import androidx.camera.camera2.internal.Camera2CameraInfoImpl;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.impl.CameraControlInternal;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class TestUtil {
+    public static Camera2CameraControlImpl getCamera2CameraControlImpl(
+            CameraControl cameraControl) {
+        if (cameraControl instanceof CameraControlInternal) {
+            CameraControlInternal impl =
+                    ((CameraControlInternal) cameraControl).getImplementation();
+            return (Camera2CameraControlImpl) impl;
+        }
+        throw new IllegalArgumentException(
+                "Can't get Camera2CameraControlImpl from the CameraControl");
+    }
+
+    public static Camera2CameraInfoImpl getCamera2CameraInfoImpl(CameraInfo cameraInfo) {
+        if (cameraInfo instanceof CameraInfoInternal) {
+            CameraInfoInternal impl = ((CameraInfoInternal) cameraInfo).getImplementation();
+            return (Camera2CameraInfoImpl) impl;
+        }
+        throw new IllegalArgumentException(
+                "Can't get Camera2CameraInfoImpl from the CameraInfo");
+    }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
index 7977243..2566caf 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
@@ -39,9 +39,9 @@
 import androidx.annotation.OptIn;
 import androidx.camera.camera2.Camera2Config;
 import androidx.camera.camera2.internal.Camera2CameraControlImpl;
+import androidx.camera.camera2.internal.util.TestUtil;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.camera.testing.CameraUtil;
@@ -94,7 +94,8 @@
         mCameraSelector = new CameraSelector.Builder().requireLensFacing(
                 CameraSelector.LENS_FACING_BACK).build();
         mCamera = CameraUtil.createCameraUseCaseAdapter(mContext, mCameraSelector);
-        mCamera2CameraControlImpl = (Camera2CameraControlImpl) mCamera.getCameraControl();
+        mCamera2CameraControlImpl =
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
         mCamera2CameraControl = mCamera2CameraControlImpl.getCamera2CameraControl();
         mMockCaptureCallback = mock(CameraCaptureSession.CaptureCallback.class);
     }
@@ -267,7 +268,7 @@
 
     private Rect getZoom2XCropRegion() throws Exception {
         AtomicReference<String> cameraIdRef = new AtomicReference<>();
-        String cameraId = ((CameraInfoInternal) mCamera.getCameraInfo()).getCameraId();
+        String cameraId = TestUtil.getCamera2CameraInfoImpl(mCamera.getCameraInfo()).getCameraId();
         cameraIdRef.set(cameraId);
 
         CameraManager cameraManager =
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java
index 6e9ed1d..860400e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java
@@ -20,12 +20,14 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
 
+import java.util.Collections;
 import java.util.Set;
 
 @RequiresApi(28)
 class CameraCharacteristicsApi28Impl extends CameraCharacteristicsBaseImpl{
-
+    private static final String TAG = "CameraCharacteristicsImpl";
     CameraCharacteristicsApi28Impl(@NonNull CameraCharacteristics cameraCharacteristics) {
         super(cameraCharacteristics);
     }
@@ -33,6 +35,14 @@
     @NonNull
     @Override
     public Set<String> getPhysicalCameraIds() {
-        return mCameraCharacteristics.getPhysicalCameraIds();
+        try {
+            return mCameraCharacteristics.getPhysicalCameraIds();
+        } catch (Exception e) {
+            // getPhysicalCameraIds could cause crash in Robolectric and there is no known
+            // workaround
+            Logger.e(TAG,
+                    "CameraCharacteristics.getPhysicalCameraIds throws an exception.", e);
+            return Collections.emptySet();
+        }
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
index 3591ef3..0b8f4b9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
@@ -25,6 +25,7 @@
 import androidx.camera.camera2.internal.Camera2CameraControlImpl;
 import androidx.camera.camera2.internal.annotation.CameraExecutor;
 import androidx.camera.core.CameraControl;
+import androidx.camera.core.impl.CameraControlInternal;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.TagBundle;
 import androidx.camera.core.impl.annotation.ExecutedBy;
@@ -125,9 +126,11 @@
      */
     @NonNull
     public static Camera2CameraControl from(@NonNull CameraControl cameraControl) {
-        Preconditions.checkArgument(cameraControl instanceof Camera2CameraControlImpl,
+        CameraControlInternal cameraControlImpl =
+                ((CameraControlInternal) cameraControl).getImplementation();
+        Preconditions.checkArgument(cameraControlImpl instanceof Camera2CameraControlImpl,
                 "CameraControl doesn't contain Camera2 implementation.");
-        return ((Camera2CameraControlImpl) cameraControl).getCamera2CameraControl();
+        return ((Camera2CameraControlImpl) cameraControlImpl).getCamera2CameraControl();
     }
 
     /**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
index 5e18f87..33522d2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
@@ -25,6 +25,7 @@
 import androidx.annotation.RestrictTo.Scope;
 import androidx.camera.camera2.internal.Camera2CameraInfoImpl;
 import androidx.camera.core.CameraInfo;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.core.util.Preconditions;
 
 import java.util.Map;
@@ -58,9 +59,11 @@
      */
     @NonNull
     public static Camera2CameraInfo from(@NonNull CameraInfo cameraInfo) {
-        Preconditions.checkArgument(cameraInfo instanceof Camera2CameraInfoImpl,
+        CameraInfoInternal cameraInfoImpl =
+                ((CameraInfoInternal) cameraInfo).getImplementation();
+        Preconditions.checkArgument(cameraInfoImpl instanceof Camera2CameraInfoImpl,
                 "CameraInfo doesn't contain Camera2 implementation.");
-        return ((Camera2CameraInfoImpl) cameraInfo).getCamera2CameraInfo();
+        return ((Camera2CameraInfoImpl) cameraInfoImpl).getCamera2CameraInfo();
     }
 
     /**
@@ -119,9 +122,10 @@
     @NonNull
     public static CameraCharacteristics extractCameraCharacteristics(
             @NonNull CameraInfo cameraInfo) {
-        Preconditions.checkState(cameraInfo instanceof Camera2CameraInfoImpl, "CameraInfo does "
-                + "not contain any Camera2 information.");
-        Camera2CameraInfoImpl impl = (Camera2CameraInfoImpl) cameraInfo;
+        CameraInfoInternal cameraInfoImpl = ((CameraInfoInternal) cameraInfo).getImplementation();
+        Preconditions.checkState(cameraInfoImpl instanceof Camera2CameraInfoImpl,
+                "CameraInfo does not contain any Camera2 information.");
+        Camera2CameraInfoImpl impl = (Camera2CameraInfoImpl) cameraInfoImpl;
         return impl.getCameraCharacteristicsCompat().toCameraCharacteristics();
     }
 
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java
index 2276320..8ad3050 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java
@@ -83,6 +83,7 @@
         Camera2CameraInfo camera2CameraInfo = mock(Camera2CameraInfo.class);
         Camera2CameraInfoImpl cameraInfoImpl = mock(Camera2CameraInfoImpl.class);
         when(cameraInfoImpl.getCamera2CameraInfo()).thenAnswer(ignored -> camera2CameraInfo);
+        when(cameraInfoImpl.getImplementation()).thenAnswer(ignored -> cameraInfoImpl);
         Camera2CameraInfo resultCamera2CameraInfo = Camera2CameraInfo.from(cameraInfoImpl);
 
         assertThat(resultCamera2CameraInfo).isEqualTo(camera2CameraInfo);
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 56a6ebb..c789d6d 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -47,6 +47,8 @@
     testImplementation(libs.truth)
     testImplementation(libs.robolectric)
     testImplementation(libs.mockitoCore4)
+    testImplementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+    testImplementation(project(":internal-testutils-truth"))
     testImplementation(project(":camera:camera-testing"), {
         exclude group: "androidx.camera", module: "camera-core"
     })
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index a385c37..32462ac 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -42,6 +42,7 @@
 import androidx.camera.core.concurrent.CameraCoordinator;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureMetaData;
+import androidx.camera.core.impl.CameraControlInternal;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.ImageCaptureConfig;
 import androidx.camera.core.impl.StreamSpec;
@@ -142,7 +143,7 @@
         ImageCapture.OnImageCapturedCallback callback = mock(
                 ImageCapture.OnImageCapturedCallback.class);
         FakeCameraControl fakeCameraControl =
-                ((FakeCameraControl) mCameraUseCaseAdapter.getCameraControl());
+                getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
 
         fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
             // Notify the cancel after the capture request has been successfully submitted
@@ -173,7 +174,7 @@
         ImageCapture.OnImageCapturedCallback callback = mock(
                 ImageCapture.OnImageCapturedCallback.class);
         FakeCameraControl fakeCameraControl =
-                ((FakeCameraControl) mCameraUseCaseAdapter.getCameraControl());
+                getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
             // Notify the failure after the capture request has been successfully submitted
             fakeCameraControl.notifyAllRequestsOnCaptureFailed();
@@ -309,6 +310,11 @@
         assertThat(hasJpegQuality(captureConfigs, jpegQuality)).isTrue();
     }
 
+    private FakeCameraControl getCameraControlImplementation(CameraControl cameraControl) {
+        CameraControlInternal impl = ((CameraControlInternal) cameraControl).getImplementation();
+        return (FakeCameraControl) impl;
+    }
+
     @NonNull
     private List<CaptureConfig> captureImage(@NonNull ImageCapture imageCapture,
             @NonNull Class<?> callbackClass) {
@@ -342,7 +348,7 @@
         }
 
         FakeCameraControl fakeCameraControl =
-                ((FakeCameraControl) mCameraUseCaseAdapter.getCameraControl());
+                getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         FakeCameraControl.OnNewCaptureRequestListener mockCaptureRequestListener =
                 mock(FakeCameraControl.OnNewCaptureRequestListener.class);
         fakeCameraControl.setOnNewCaptureRequestListener(mockCaptureRequestListener);
@@ -510,7 +516,7 @@
         ImageCapture.OnImageCapturedCallback callback = mock(
                 ImageCapture.OnImageCapturedCallback.class);
         FakeCameraControl fakeCameraControl =
-                ((FakeCameraControl) mCameraUseCaseAdapter.getCameraControl());
+                getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         CountDownLatch latch = new CountDownLatch(1);
         fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
             latch.countDown();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/FocusMeteringAction.java b/camera/camera-core/src/main/java/androidx/camera/core/FocusMeteringAction.java
index 30f3edb..2faa545 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/FocusMeteringAction.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/FocusMeteringAction.java
@@ -186,6 +186,17 @@
         }
 
         /**
+         * Create a Builder from a {@link FocusMeteringAction}.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public Builder(@NonNull FocusMeteringAction focusMeteringAction) {
+            mMeteringPointsAf.addAll(focusMeteringAction.getMeteringPointsAf());
+            mMeteringPointsAe.addAll(focusMeteringAction.getMeteringPointsAe());
+            mMeteringPointsAwb.addAll(focusMeteringAction.getMeteringPointsAwb());
+            mAutoCancelDurationInMillis = focusMeteringAction.getAutoCancelDurationInMillis();
+        }
+
+        /**
          * Adds another {@link MeteringPoint} with default metering mode {@link #FLAG_AF} |
          * {@link #FLAG_AE} | {@link #FLAG_AWB}.
          *
@@ -271,6 +282,27 @@
         }
 
         /**
+         *
+         * Remove all points of the given meteringMode.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @NonNull
+        public Builder removePoints(@MeteringMode int meteringMode) {
+            if ((meteringMode & FLAG_AF) != 0) {
+                mMeteringPointsAf.clear();
+            }
+
+            if ((meteringMode & FLAG_AE) != 0) {
+                mMeteringPointsAe.clear();
+            }
+
+            if ((meteringMode & FLAG_AWB) != 0) {
+                mMeteringPointsAwb.clear();
+            }
+            return this;
+        }
+
+        /**
          * Builds the {@link FocusMeteringAction} instance.
          */
         @NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
index 33688e9..c89bb2f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
@@ -39,9 +39,13 @@
 /**
  * The CameraControlInternal Interface.
  *
+ *
  * <p>CameraControlInternal is used for global camera operations like zoom, focus, flash and
- * triggering
- * AF/AE.
+ * triggering AF/AE as well as some internal operations.
+ *
+ * <p>{@link #getImplementation()} returns a {@link CameraControlInternal} instance
+ * that contains the actual implementation and can be cast to an implementation specific class.
+ * If the instance itself is the implementation instance, then it should return <code>this</code>.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface CameraControlInternal extends CameraControl {
@@ -136,6 +140,16 @@
     @NonNull
     Config getInteropConfig();
 
+    /**
+     * Gets the underlying implementation instance which could be cast into an implementation
+     * specific class for further use in implementation module. Returns <code>this</code> if this
+     * instance is the implementation instance.
+     */
+    @NonNull
+    default CameraControlInternal getImplementation() {
+        return this;
+    }
+
     CameraControlInternal DEFAULT_EMPTY_INSTANCE = new CameraControlInternal() {
         @FlashMode
         @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index 791efe0..e6a1e9b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -36,6 +36,10 @@
  * An interface for retrieving camera information.
  *
  * <p>Contains methods for retrieving characteristics for a specific camera.
+ *
+ * <p>{@link #getImplementation()} returns a {@link CameraInfoInternal} instance
+ * that contains the actual implementation and can be cast to an implementation specific class.
+ * If the instance itself is the implementation instance, then it should return <code>this</code>.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public interface CameraInfoInternal extends CameraInfo {
@@ -101,6 +105,17 @@
     @NonNull
     Set<DynamicRange> getSupportedDynamicRanges();
 
+    /**
+     * Gets the underlying implementation instance which could be cast into an implementation
+     * specific class for further use in implementation module. Returns <code>this</code> if this
+     * instance is the implementation instance.
+     */
+    @NonNull
+    default CameraInfoInternal getImplementation() {
+        return this;
+    }
+
+
     /** {@inheritDoc} */
     @NonNull
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
new file mode 100644
index 0000000..ea73135
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
@@ -0,0 +1,155 @@
+/*
+ * 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.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.FocusMeteringResult;
+import androidx.camera.core.ImageCapture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/**
+ * A {@link CameraControlInternal} that forwards all the calls into the given
+ * {@link CameraControlInternal}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ForwardingCameraControl implements CameraControlInternal {
+    private final CameraControlInternal mCameraControlInternal;
+
+    /**
+     * Create an instance that will forward all calls to the supplied {@link CameraControlInternal}
+     * instance.
+     */
+    public ForwardingCameraControl(@NonNull CameraControlInternal cameraControlInternal) {
+        mCameraControlInternal = cameraControlInternal;
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> enableTorch(boolean torch) {
+        return mCameraControlInternal.enableTorch(torch);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<FocusMeteringResult> startFocusAndMetering(
+            @NonNull FocusMeteringAction action) {
+        return mCameraControlInternal.startFocusAndMetering(action);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> cancelFocusAndMetering() {
+        return mCameraControlInternal.cancelFocusAndMetering();
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> setZoomRatio(float ratio) {
+        return mCameraControlInternal.setZoomRatio(ratio);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> setLinearZoom(float linearZoom) {
+        return mCameraControlInternal.setLinearZoom(linearZoom);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Integer> setExposureCompensationIndex(int value) {
+        return mCameraControlInternal.setExposureCompensationIndex(value);
+    }
+
+    @Override
+    @ImageCapture.FlashMode
+    public int getFlashMode() {
+        return mCameraControlInternal.getFlashMode();
+    }
+
+    @Override
+    public void setFlashMode(@ImageCapture.FlashMode int flashMode) {
+        mCameraControlInternal.setFlashMode(flashMode);
+    }
+
+    @Override
+    public void addZslConfig(@NonNull SessionConfig.Builder sessionConfigBuilder) {
+        mCameraControlInternal.addZslConfig(sessionConfigBuilder);
+    }
+
+    @Override
+    public void setZslDisabledByUserCaseConfig(boolean disabled) {
+        mCameraControlInternal.setZslDisabledByUserCaseConfig(disabled);
+    }
+
+    @Override
+    public boolean isZslDisabledByByUserCaseConfig() {
+        return mCameraControlInternal.isZslDisabledByByUserCaseConfig();
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<List<Void>> submitStillCaptureRequests(
+            @NonNull List<CaptureConfig> captureConfigs,
+            @ImageCapture.CaptureMode int captureMode,
+            @ImageCapture.FlashType int flashType) {
+        return mCameraControlInternal.submitStillCaptureRequests(
+                captureConfigs,
+                captureMode,
+                flashType);
+    }
+
+    @NonNull
+    @Override
+    public SessionConfig getSessionConfig() {
+        return mCameraControlInternal.getSessionConfig();
+    }
+
+    @NonNull
+    @Override
+    public Rect getSensorRect() {
+        return mCameraControlInternal.getSensorRect();
+    }
+
+    @Override
+    public void addInteropConfig(@NonNull Config config) {
+        mCameraControlInternal.addInteropConfig(config);
+    }
+
+    @Override
+    public void clearInteropConfig() {
+        mCameraControlInternal.clearInteropConfig();
+    }
+
+    @NonNull
+    @Override
+    public Config getInteropConfig() {
+        return mCameraControlInternal.getInteropConfig();
+    }
+
+    @NonNull
+    @Override
+    public CameraControlInternal getImplementation() {
+        return mCameraControlInternal.getImplementation();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
new file mode 100644
index 0000000..c389a1d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -0,0 +1,195 @@
+/*
+ * 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.util.Range;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraState;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.ExperimentalZeroShutterLag;
+import androidx.camera.core.ExposureState;
+import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.ZoomState;
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link CameraInfoInternal} that forwards all the calls into the given
+ * {@link CameraInfoInternal}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ForwardingCameraInfo implements CameraInfoInternal {
+
+    private final CameraInfoInternal mCameraInfoInternal;
+
+    /**
+     * Create an instance that will forward all calls to the supplied {@link CameraInfoInternal}
+     * instance.
+     */
+    public ForwardingCameraInfo(@NonNull CameraInfoInternal cameraInfoInternal) {
+        mCameraInfoInternal = cameraInfoInternal;
+    }
+
+    @Override
+    public int getSensorRotationDegrees() {
+        return mCameraInfoInternal.getSensorRotationDegrees();
+    }
+
+    @Override
+    public int getSensorRotationDegrees(int relativeRotation) {
+        return mCameraInfoInternal.getSensorRotationDegrees(relativeRotation);
+    }
+
+    @Override
+    public boolean hasFlashUnit() {
+        return mCameraInfoInternal.hasFlashUnit();
+    }
+
+    @NonNull
+    @Override
+    public LiveData<Integer> getTorchState() {
+        return mCameraInfoInternal.getTorchState();
+    }
+
+    @NonNull
+    @Override
+    public LiveData<ZoomState> getZoomState() {
+        return mCameraInfoInternal.getZoomState();
+    }
+
+    @NonNull
+    @Override
+    public ExposureState getExposureState() {
+        return mCameraInfoInternal.getExposureState();
+    }
+
+    @NonNull
+    @Override
+    public LiveData<CameraState> getCameraState() {
+        return mCameraInfoInternal.getCameraState();
+    }
+
+    @NonNull
+    @Override
+    public String getImplementationType() {
+        return mCameraInfoInternal.getImplementationType();
+    }
+
+    @Override
+    public int getLensFacing() {
+        return mCameraInfoInternal.getLensFacing();
+    }
+
+    @Override
+    public float getIntrinsicZoomRatio() {
+        return mCameraInfoInternal.getIntrinsicZoomRatio();
+    }
+
+    @Override
+    public boolean isFocusMeteringSupported(@NonNull FocusMeteringAction action) {
+        return mCameraInfoInternal.isFocusMeteringSupported(action);
+    }
+
+    @Override
+    @ExperimentalZeroShutterLag
+    public boolean isZslSupported() {
+        return mCameraInfoInternal.isZslSupported();
+    }
+
+    @NonNull
+    @Override
+    public Set<Range<Integer>> getSupportedFrameRateRanges() {
+        return mCameraInfoInternal.getSupportedFrameRateRanges();
+    }
+
+    @Override
+    public boolean isPrivateReprocessingSupported() {
+        return mCameraInfoInternal.isPrivateReprocessingSupported();
+    }
+
+    @NonNull
+    @Override
+    public String getCameraId() {
+        return mCameraInfoInternal.getCameraId();
+    }
+
+    @Override
+    public void addSessionCaptureCallback(@NonNull Executor executor,
+            @NonNull CameraCaptureCallback callback) {
+        mCameraInfoInternal.addSessionCaptureCallback(executor, callback);
+    }
+
+    @Override
+    public void removeSessionCaptureCallback(@NonNull CameraCaptureCallback callback) {
+        mCameraInfoInternal.removeSessionCaptureCallback(callback);
+    }
+
+    @NonNull
+    @Override
+    public Quirks getCameraQuirks() {
+        return mCameraInfoInternal.getCameraQuirks();
+    }
+
+    @NonNull
+    @Override
+    public EncoderProfilesProvider getEncoderProfilesProvider() {
+        return mCameraInfoInternal.getEncoderProfilesProvider();
+    }
+
+    @NonNull
+    @Override
+    public Timebase getTimebase() {
+        return mCameraInfoInternal.getTimebase();
+    }
+
+    @NonNull
+    @Override
+    public List<Size> getSupportedResolutions(int format) {
+        return mCameraInfoInternal.getSupportedResolutions(format);
+    }
+
+    @NonNull
+    @Override
+    public List<Size> getSupportedHighResolutions(int format) {
+        return mCameraInfoInternal.getSupportedHighResolutions(format);
+    }
+
+    @NonNull
+    @Override
+    public Set<DynamicRange> getSupportedDynamicRanges() {
+        return mCameraInfoInternal.getSupportedDynamicRanges();
+    }
+
+    @NonNull
+    @Override
+    public CameraInfoInternal getImplementation() {
+        return mCameraInfoInternal.getImplementation();
+    }
+
+    @NonNull
+    @Override
+    public CameraSelector getCameraSelector() {
+        return mCameraInfoInternal.getCameraSelector();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/RestrictedCameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/RestrictedCameraControl.java
new file mode 100644
index 0000000..6fc61f8
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/RestrictedCameraControl.java
@@ -0,0 +1,199 @@
+/*
+ * 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.FocusMeteringAction;
+import androidx.camera.core.FocusMeteringResult;
+import androidx.camera.core.impl.utils.futures.Futures;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A {@link CameraControlInternal} whose capabilities can be restricted via
+ * {@link #enableRestrictedOperations(boolean, Set)}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class RestrictedCameraControl extends ForwardingCameraControl {
+    /**
+     * Defines the list of supported camera operations.
+     */
+    public static final int ZOOM = 0;
+    public static final int AUTO_FOCUS = 1;
+    public static final int AF_REGION = 2;
+    public static final int AE_REGION = 3;
+    public static final int AWB_REGION = 4;
+    public static final int FLASH = 5;
+    public static final int TORCH = 6;
+    public static final int EXPOSURE_COMPENSATION = 7;
+
+    public @interface CameraOperation {
+    }
+
+    private final CameraControlInternal mCameraControl;
+    private volatile boolean mUseRestrictedCameraOperations = false;
+    @Nullable
+    private volatile @CameraOperation Set<Integer> mRestrictedCameraOperations;
+
+    /**
+     * Creates the restricted version of the given {@link CameraControlInternal}.
+     */
+    public RestrictedCameraControl(@NonNull CameraControlInternal cameraControl) {
+        super(cameraControl);
+        mCameraControl = cameraControl;
+    }
+
+    /**
+     * Enable or disable the restricted operations. If disabled, it works just like the origin
+     * CameraControlInternal instance.
+     */
+    public void enableRestrictedOperations(boolean enable,
+            @Nullable @CameraOperation Set<Integer> restrictedOperations) {
+        mUseRestrictedCameraOperations = enable;
+        mRestrictedCameraOperations = restrictedOperations;
+    }
+
+    /**
+     * Returns implementation instance.
+     */
+    @NonNull
+    @Override
+    public CameraControlInternal getImplementation() {
+        return mCameraControl;
+    }
+
+    boolean isOperationSupported(
+            @NonNull @CameraOperation int... operations) {
+        if (!mUseRestrictedCameraOperations || mRestrictedCameraOperations == null) {
+            return true;
+        }
+
+        // Arrays.asList doesn't work for int array.
+        List<Integer> operationList = new ArrayList<>(operations.length);
+        for (int operation : operations) {
+            operationList.add(operation);
+        }
+
+        return mRestrictedCameraOperations.containsAll(operationList);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> enableTorch(boolean torch) {
+        if (!isOperationSupported(TORCH)) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("Torch is not supported"));
+        }
+        return mCameraControl.enableTorch(torch);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<FocusMeteringResult> startFocusAndMetering(
+            @NonNull FocusMeteringAction action) {
+        FocusMeteringAction modifiedAction = getModifiedFocusMeteringAction(action);
+        if (modifiedAction == null) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("FocusMetering is not supported"));
+        }
+
+        return mCameraControl.startFocusAndMetering(modifiedAction);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> cancelFocusAndMetering() {
+        return mCameraControl.cancelFocusAndMetering();
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> setZoomRatio(float ratio) {
+        if (!isOperationSupported(ZOOM)) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("Zoom is not supported"));
+        }
+        return mCameraControl.setZoomRatio(ratio);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> setLinearZoom(float linearZoom) {
+        if (!isOperationSupported(ZOOM)) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("Zoom is not supported"));
+        }
+        return mCameraControl.setLinearZoom(linearZoom);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Integer> setExposureCompensationIndex(int value) {
+        if (!isOperationSupported(EXPOSURE_COMPENSATION)) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("ExposureCompensation is not supported"));
+        }
+        return mCameraControl.setExposureCompensationIndex(value);
+    }
+
+    /**
+     * Returns the modified {@link FocusMeteringAction} that filters out unsupported AE/AF/AWB
+     * regions. Returns null if none of AF/AE/AWB regions can be supported after the filtering.
+     */
+    @Nullable
+    FocusMeteringAction getModifiedFocusMeteringAction(@NonNull FocusMeteringAction action) {
+        boolean shouldModify = false;
+        FocusMeteringAction.Builder builder = new FocusMeteringAction.Builder(action);
+        if (!action.getMeteringPointsAf().isEmpty()
+                && !isOperationSupported(AUTO_FOCUS, AF_REGION)) {
+            shouldModify = true;
+            builder.removePoints(FocusMeteringAction.FLAG_AF);
+        }
+
+        if (!action.getMeteringPointsAe().isEmpty()
+                && !isOperationSupported(AE_REGION)) {
+            shouldModify = true;
+            builder.removePoints(FocusMeteringAction.FLAG_AE);
+        }
+
+        if (!action.getMeteringPointsAwb().isEmpty()
+                && !isOperationSupported(AWB_REGION)) {
+            shouldModify = true;
+            builder.removePoints(FocusMeteringAction.FLAG_AWB);
+        }
+
+        // Returns origin action if no need to modify.
+        if (!shouldModify) {
+            return action;
+        }
+
+        FocusMeteringAction modifyAction = builder.build();
+        if (modifyAction.getMeteringPointsAf().isEmpty()
+                && modifyAction.getMeteringPointsAe().isEmpty()
+                && modifyAction.getMeteringPointsAwb().isEmpty()) {
+            // All regions are not allowed, return null.
+            return null;
+        }
+        return builder.build();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/RestrictedCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/RestrictedCameraInfo.java
new file mode 100644
index 0000000..b2148f5
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/RestrictedCameraInfo.java
@@ -0,0 +1,123 @@
+/*
+ * 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.util.Range;
+import android.util.Rational;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ExposureState;
+import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.TorchState;
+import androidx.camera.core.ZoomState;
+import androidx.camera.core.internal.ImmutableZoomState;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+/**
+ * A {@link CameraInfoInternal} that returns disabled state if the corresponding operation in the
+ * given {@link RestrictedCameraControl} is disabled.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class RestrictedCameraInfo extends ForwardingCameraInfo {
+    private final CameraInfoInternal mCameraInfo;
+    private final RestrictedCameraControl mRestrictedCameraControl;
+
+    public RestrictedCameraInfo(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull RestrictedCameraControl restrictedCameraControl) {
+        super(cameraInfo);
+        mCameraInfo = cameraInfo;
+        mRestrictedCameraControl = restrictedCameraControl;
+    }
+
+    @NonNull
+    @Override
+    public CameraInfoInternal getImplementation() {
+        return mCameraInfo;
+    }
+
+    @Override
+    public boolean hasFlashUnit() {
+        if (!mRestrictedCameraControl.isOperationSupported(RestrictedCameraControl.FLASH)) {
+            return false;
+        }
+
+        return mCameraInfo.hasFlashUnit();
+    }
+
+    @NonNull
+    @Override
+    public LiveData<Integer> getTorchState() {
+        if (!mRestrictedCameraControl.isOperationSupported(RestrictedCameraControl.TORCH)) {
+            return new MutableLiveData<>(TorchState.OFF);
+        }
+
+        return mCameraInfo.getTorchState();
+    }
+
+    @NonNull
+    @Override
+    public LiveData<ZoomState> getZoomState() {
+        if (!mRestrictedCameraControl.isOperationSupported(RestrictedCameraControl.ZOOM)) {
+            return new MutableLiveData<>(ImmutableZoomState.create(
+                    /* zoomRatio */1f, /* maxZoomRatio */ 1f,
+                    /* minZoomRatio */ 1f, /* linearZoom*/ 0f));
+        }
+        return mCameraInfo.getZoomState();
+    }
+
+    @NonNull
+    @Override
+    public ExposureState getExposureState() {
+        if (!mRestrictedCameraControl.isOperationSupported(
+                RestrictedCameraControl.EXPOSURE_COMPENSATION)) {
+            return new ExposureState() {
+                @Override
+                public int getExposureCompensationIndex() {
+                    return 0;
+                }
+
+                @NonNull
+                @Override
+                public Range<Integer> getExposureCompensationRange() {
+                    return new Range<>(0, 0);
+                }
+
+                @NonNull
+                @Override
+                public Rational getExposureCompensationStep() {
+                    return Rational.ZERO;
+                }
+
+                @Override
+                public boolean isExposureCompensationSupported() {
+                    return false;
+                }
+            };
+        }
+        return mCameraInfo.getExposureState();
+    }
+
+    @Override
+    public boolean isFocusMeteringSupported(@NonNull FocusMeteringAction action) {
+        if (mRestrictedCameraControl.getModifiedFocusMeteringAction(action) == null) {
+            return false;
+        }
+        return mCameraInfo.isFocusMeteringSupported(action);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index f9858f5..70b5c2b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -24,7 +24,9 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraInfo;
 
+import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A processor for (1) transforming the surfaces used in Preview/ImageCapture/ImageAnalysis
@@ -127,6 +129,14 @@
     }
 
     /**
+     * Returns the supported camera operations when the SessionProcessor is enabled.
+     */
+    @NonNull
+    default @RestrictedCameraControl.CameraOperation Set<Integer> getSupportedCameraOperations() {
+        return Collections.emptySet();
+    }
+
+    /**
      * Callback for {@link #startRepeating} and {@link #startCapture}.
      */
     interface CaptureCallback {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 9d3021c..4e74b5e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -59,6 +59,10 @@
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraMode;
 import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.RestrictedCameraControl;
+import androidx.camera.core.impl.RestrictedCameraControl.CameraOperation;
+import androidx.camera.core.impl.RestrictedCameraInfo;
+import androidx.camera.core.impl.SessionProcessor;
 import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
@@ -143,6 +147,12 @@
     @Nullable
     private StreamSharing mStreamSharing;
 
+    @NonNull
+    private final RestrictedCameraControl mRestrictedCameraControl;
+    @NonNull
+    private final RestrictedCameraInfo mRestrictedCameraInfo;
+
+
     /**
      * Create a new {@link CameraUseCaseAdapter} instance.
      *
@@ -167,6 +177,12 @@
         mCameraCoordinator = cameraCoordinator;
         mCameraDeviceSurfaceManager = cameraDeviceSurfaceManager;
         mUseCaseConfigFactory = useCaseConfigFactory;
+        // TODO(b/279996499): bind the same restricted CameraControl and CameraInfo to use cases.
+        mRestrictedCameraControl =
+                new RestrictedCameraControl(mCameraInternal.getCameraControlInternal());
+        mRestrictedCameraInfo =
+                new RestrictedCameraInfo(mCameraInternal.getCameraInfoInternal(),
+                        mRestrictedCameraControl);
     }
 
     /**
@@ -603,14 +619,14 @@
             Map<UseCaseConfig<?>, List<Size>> configToSupportedSizesMap = new HashMap<>();
             Rect sensorRect;
             try {
-                sensorRect = ((CameraControlInternal) getCameraControl()).getSensorRect();
+                sensorRect = mCameraInternal.getCameraControlInternal().getSensorRect();
             } catch (NullPointerException e) {
                 // TODO(b/274531208): Remove the unnecessary SENSOR_INFO_ACTIVE_ARRAY_SIZE NPE
                 //  check related code only which is used for robolectric tests
                 sensorRect = null;
             }
             SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
-                    (CameraInfoInternal) getCameraInfo(),
+                    cameraInfoInternal,
                     sensorRect != null ? rectToSize(sensorRect) : null);
             for (UseCase useCase : newUseCases) {
                 ConfigPair configPair = configPairMap.get(useCase);
@@ -809,13 +825,13 @@
     @NonNull
     @Override
     public CameraControl getCameraControl() {
-        return mCameraInternal.getCameraControlInternal();
+        return mRestrictedCameraControl;
     }
 
     @NonNull
     @Override
     public CameraInfo getCameraInfo() {
-        return mCameraInternal.getCameraInfoInternal();
+        return mRestrictedCameraInfo;
     }
 
     @NonNull
@@ -846,6 +862,14 @@
             }
 
             mCameraConfig = cameraConfig;
+            SessionProcessor sessionProcessor = mCameraConfig.getSessionProcessor(null);
+            if (sessionProcessor != null) {
+                @CameraOperation Set<Integer> supportedOps =
+                        sessionProcessor.getSupportedCameraOperations();
+                mRestrictedCameraControl.enableRestrictedOperations(true, supportedOps);
+            } else {
+                mRestrictedCameraControl.enableRestrictedOperations(false, null);
+            }
 
             //Configure the CameraInternal as well so that it can get SessionProcessor.
             mCameraInternal.setExtendedConfig(mCameraConfig);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraControl.java
index 1ca8ace..294173d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraControl.java
@@ -20,18 +20,14 @@
 import static java.util.Collections.singletonList;
 import static java.util.Objects.requireNonNull;
 
-import android.graphics.Rect;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.camera.core.FocusMeteringAction;
-import androidx.camera.core.FocusMeteringResult;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.impl.CameraControlInternal;
 import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.ForwardingCameraControl;
 import androidx.camera.core.impl.utils.futures.Futures;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -42,83 +38,20 @@
  * A {@link CameraControlInternal} that is used to control the virtual camera.
  */
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class VirtualCameraControl implements CameraControlInternal {
+public class VirtualCameraControl extends ForwardingCameraControl {
 
     private static final int DEFAULT_JPEG_QUALITY = 100;
 
-    private final CameraControlInternal mParent;
     private final StreamSharing.Control mStreamSharingControl;
 
     VirtualCameraControl(@NonNull CameraControlInternal parent,
             @NonNull StreamSharing.Control streamSharingControl) {
-        mParent = parent;
+        super(parent);
         mStreamSharingControl = streamSharingControl;
     }
 
     @NonNull
     @Override
-    public ListenableFuture<Void> enableTorch(boolean torch) {
-        return mParent.enableTorch(torch);
-    }
-
-    @NonNull
-    @Override
-    public ListenableFuture<FocusMeteringResult> startFocusAndMetering(
-            @NonNull FocusMeteringAction action) {
-        return mParent.startFocusAndMetering(action);
-    }
-
-    @NonNull
-    @Override
-    public ListenableFuture<Void> cancelFocusAndMetering() {
-        return mParent.cancelFocusAndMetering();
-    }
-
-    @NonNull
-    @Override
-    public ListenableFuture<Void> setZoomRatio(float ratio) {
-        return mParent.setZoomRatio(ratio);
-    }
-
-    @NonNull
-    @Override
-    public ListenableFuture<Void> setLinearZoom(float linearZoom) {
-        return mParent.setLinearZoom(linearZoom);
-    }
-
-    @NonNull
-    @Override
-    public ListenableFuture<Integer> setExposureCompensationIndex(int value) {
-        return mParent.setExposureCompensationIndex(value);
-    }
-
-    @Override
-    public int getFlashMode() {
-        return mParent.getFlashMode();
-    }
-
-    @Override
-    public void setFlashMode(int flashMode) {
-        mParent.setFlashMode(flashMode);
-    }
-
-    @Override
-    public void addZslConfig(@NonNull SessionConfig.Builder sessionConfigBuilder) {
-        mParent.addZslConfig(sessionConfigBuilder);
-    }
-
-    @Override
-    public void setZslDisabledByUserCaseConfig(boolean disabled) {
-        mParent.setZslDisabledByUserCaseConfig(disabled);
-    }
-
-    @Override
-    public boolean isZslDisabledByByUserCaseConfig() {
-        return mParent.isZslDisabledByByUserCaseConfig();
-    }
-
-    @NonNull
-    @Override
     public ListenableFuture<List<Void>> submitStillCaptureRequests(
             @NonNull List<CaptureConfig> captureConfigs,
             @ImageCapture.CaptureMode int captureMode,
@@ -132,32 +65,4 @@
         return requireNonNull(captureConfig.getImplementationOptions().retrieveOption(
                 CaptureConfig.OPTION_JPEG_QUALITY, DEFAULT_JPEG_QUALITY));
     }
-
-    @NonNull
-    @Override
-    public SessionConfig getSessionConfig() {
-        return mParent.getSessionConfig();
-    }
-
-    @NonNull
-    @Override
-    public Rect getSensorRect() {
-        return mParent.getSensorRect();
-    }
-
-    @Override
-    public void addInteropConfig(@NonNull Config config) {
-        mParent.addInteropConfig(config);
-    }
-
-    @Override
-    public void clearInteropConfig() {
-        mParent.clearInteropConfig();
-    }
-
-    @NonNull
-    @Override
-    public Config getInteropConfig() {
-        return mParent.getInteropConfig();
-    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/FocusMeteringActionTest.java b/camera/camera-core/src/test/java/androidx/camera/core/FocusMeteringActionTest.java
index c20d759..1f7200f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/FocusMeteringActionTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/FocusMeteringActionTest.java
@@ -23,6 +23,7 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
+import java.util.Arrays;
 import java.util.concurrent.TimeUnit;
 
 @RunWith(RobolectricTestRunner.class)
@@ -311,13 +312,53 @@
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void builderWithNullPoint() {
-        new FocusMeteringAction.Builder(null).build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
     public void builderWithNullPoint2() {
         new FocusMeteringAction.Builder(null, FocusMeteringAction.FLAG_AF).build();
     }
 
+    @Test
+    public void copyBuilder() {
+        // 1. Arrange
+        FocusMeteringAction action1 = new FocusMeteringAction.Builder(mPoint1)
+                .addPoint(mPoint2, FocusMeteringAction.FLAG_AE)
+                .setAutoCancelDuration(8000, TimeUnit.MILLISECONDS)
+                .build();
+
+        // 2. Act
+        FocusMeteringAction action2 = new FocusMeteringAction.Builder(action1).build();
+
+        // 3. Assert
+        assertThat(action1.getMeteringPointsAf()).containsExactlyElementsIn(
+                action2.getMeteringPointsAf()
+        );
+        assertThat(action1.getMeteringPointsAe()).containsExactlyElementsIn(
+                action2.getMeteringPointsAe()
+        );
+        assertThat(action1.getMeteringPointsAwb()).containsExactlyElementsIn(
+                action2.getMeteringPointsAwb()
+        );
+        assertThat(action1.getAutoCancelDurationInMillis())
+                .isEqualTo(action2.getAutoCancelDurationInMillis());
+        assertThat(action1.isAutoCancelEnabled()).isEqualTo(action2.isAutoCancelEnabled());
+    }
+
+    @Test
+    public void removePoints() {
+        // 1. Arrange
+        FocusMeteringAction.Builder builder = new FocusMeteringAction.Builder(mPoint1)
+                .addPoint(mPoint2, FocusMeteringAction.FLAG_AE);
+
+        // 2. Act
+        FocusMeteringAction action = builder.removePoints(FocusMeteringAction.FLAG_AE).build();
+
+        // 3. Assert
+        assertThat(action.getMeteringPointsAe()).isEmpty();
+        assertThat(action.getMeteringPointsAf()).containsExactlyElementsIn(
+                Arrays.asList(mPoint1)
+        );
+        assertThat(action.getMeteringPointsAwb()).containsExactlyElementsIn(
+                Arrays.asList(mPoint1)
+        );
+    }
+
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index cabf0cc..670ef68 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -20,15 +20,22 @@
 import android.graphics.Matrix
 import android.graphics.Rect
 import android.os.Build
+import android.util.Range
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
 import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.FocusMeteringAction.FLAG_AE
+import androidx.camera.core.FocusMeteringAction.FLAG_AF
+import androidx.camera.core.FocusMeteringAction.FLAG_AWB
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceOrientedMeteringPointFactory
+import androidx.camera.core.TorchState
 import androidx.camera.core.UseCase
 import androidx.camera.core.ViewPort
 import androidx.camera.core.concurrent.CameraCoordinator
@@ -38,6 +45,8 @@
 import androidx.camera.core.impl.Identifier
 import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.RestrictedCameraControl
+import androidx.camera.core.impl.SessionProcessor
 import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
@@ -45,17 +54,23 @@
 import androidx.camera.core.processing.DefaultSurfaceProcessor
 import androidx.camera.core.streamsharing.StreamSharing
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
 import androidx.camera.testing.fakes.FakeCameraCoordinator
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeSessionProcessor
 import androidx.camera.testing.fakes.FakeSurfaceEffect
 import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.camera.testing.fakes.FakeUseCaseConfig
 import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
 import androidx.camera.testing.fakes.GrayscaleImageEffect
+import androidx.concurrent.futures.await
+import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
+import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -81,7 +96,6 @@
     instrumentedPackages = ["androidx.camera.core"]
 )
 class CameraUseCaseAdapterTest {
-
     private lateinit var effects: List<CameraEffect>
     private lateinit var executor: ExecutorService
 
@@ -93,6 +107,8 @@
     private lateinit var sharedEffect: FakeSurfaceEffect
     private lateinit var cameraCoordinator: CameraCoordinator
     private lateinit var surfaceProcessorInternal: FakeSurfaceProcessorInternal
+    private lateinit var fakeCameraControl: FakeCameraControl
+    private lateinit var fakeCameraInfo: FakeCameraInfoInternal
     private val fakeCameraSet = LinkedHashSet<CameraInternal>()
     private val imageEffect = GrayscaleImageEffect()
     private val preview = Preview.Builder().build()
@@ -106,7 +122,9 @@
     @Before
     fun setUp() {
         fakeCameraDeviceSurfaceManager = FakeCameraDeviceSurfaceManager()
-        fakeCamera = FakeCamera(CAMERA_ID)
+        fakeCameraControl = FakeCameraControl()
+        fakeCameraInfo = FakeCameraInfoInternal()
+        fakeCamera = FakeCamera(CAMERA_ID, fakeCameraControl, fakeCameraInfo)
         cameraCoordinator = FakeCameraCoordinator()
         useCaseConfigFactory = FakeUseCaseConfigFactory()
         fakeCameraSet.add(fakeCamera)
@@ -209,7 +227,7 @@
     fun invalidUseCaseComboCantBeFixedByStreamSharing_throwsException() {
         // Arrange: create a camera that only support one JPEG stream.
         fakeCameraDeviceSurfaceManager.setValidSurfaceCombos(setOf(listOf(JPEG)))
-        // Act: add PRIV and JPEG streams.
+        // Act: add PRIVATE and JPEG streams.
         adapter.addUseCases(setOf(preview, image))
     }
 
@@ -909,6 +927,316 @@
         assertThat(video.effect).isNull()
     }
 
+    private fun createAdapterWithSupportedCameraOperations(
+        @RestrictedCameraControl.CameraOperation supportedOps: Set<Int>
+    ): CameraUseCaseAdapter {
+        val cameraUseCaseAdapter = CameraUseCaseAdapter(
+            fakeCameraSet,
+            cameraCoordinator,
+            fakeCameraDeviceSurfaceManager,
+            useCaseConfigFactory
+        )
+
+        val fakeSessionProcessor = FakeSessionProcessor()
+        // no camera operations are supported.
+        fakeSessionProcessor.restrictedCameraOperations = supportedOps
+        val cameraConfig: CameraConfig = FakeCameraConfig(fakeSessionProcessor)
+        cameraUseCaseAdapter.setExtendedConfig(cameraConfig)
+        return cameraUseCaseAdapter
+    }
+
+    @Test
+    fun cameraControlFailed_whenNoCameraOperationsSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(supportedOps = emptySet())
+
+        // 2. Act && Assert
+        assertThrows<IllegalStateException> {
+            cameraUseCaseAdapter.cameraControl.setZoomRatio(1.0f).await()
+        }
+        assertThrows<IllegalStateException> {
+            cameraUseCaseAdapter.cameraControl.setLinearZoom(1.0f).await()
+        }
+        assertThrows<IllegalStateException> {
+            cameraUseCaseAdapter.cameraControl.enableTorch(true).await()
+        }
+        assertThrows<IllegalStateException> {
+            cameraUseCaseAdapter.cameraControl.startFocusAndMetering(
+                getFocusMeteringAction()
+            ).await()
+        }
+        assertThrows<IllegalStateException> {
+            cameraUseCaseAdapter.cameraControl.setExposureCompensationIndex(0).await()
+        }
+    }
+
+    private fun getFocusMeteringAction(
+        meteringMode: Int = FLAG_AF or FLAG_AE or FLAG_AWB
+    ): FocusMeteringAction {
+        val pointFactory = SurfaceOrientedMeteringPointFactory(1f, 1f)
+        return FocusMeteringAction.Builder(
+            pointFactory.createPoint(0.5f, 0.5f), meteringMode)
+            .build()
+    }
+
+    @Test
+    fun zoomEnabled_whenZoomOperationsSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(RestrictedCameraControl.ZOOM))
+
+        // 2. Act && Assert
+        cameraUseCaseAdapter.cameraControl.setZoomRatio(2.0f).await()
+        assertThat(fakeCameraControl.zoomRatio).isEqualTo(2.0f)
+        cameraUseCaseAdapter.cameraControl.setLinearZoom(1.0f).await()
+        assertThat(fakeCameraControl.linearZoom).isEqualTo(1.0f)
+    }
+
+    @Test
+    fun torchEnabled_whenTorchOperationSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(RestrictedCameraControl.TORCH))
+
+        // 2. Act
+        cameraUseCaseAdapter.cameraControl.enableTorch(true).await()
+
+        // 3. Assert
+        assertThat(fakeCameraControl.torchEnabled).isEqualTo(true)
+    }
+
+    @Test
+    fun focusMetering_afEnabled_whenAfOperationSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(
+                    RestrictedCameraControl.AUTO_FOCUS,
+                    RestrictedCameraControl.AF_REGION,
+                    ))
+
+        // 2. Act
+        cameraUseCaseAdapter.cameraControl.startFocusAndMetering(
+            getFocusMeteringAction()
+        ).await()
+
+        // 3. Assert
+        // Only AF point remains, AE/AWB points removed.
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAf?.size)
+            .isEqualTo(1)
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAe)
+            .isEmpty()
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAwb)
+            .isEmpty()
+    }
+
+    @Test
+    fun focusMetering_aeEnabled_whenAeOperationsSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(
+                    RestrictedCameraControl.AE_REGION,
+                ))
+
+        // 2. Act
+        cameraUseCaseAdapter.cameraControl.startFocusAndMetering(
+            getFocusMeteringAction()
+        ).await()
+
+        // 3. Assert
+        // Only AE point remains, AF/AWB points removed.
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAe?.size)
+            .isEqualTo(1)
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAf)
+            .isEmpty()
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAwb)
+            .isEmpty()
+    }
+
+    @Test
+    fun focusMetering_awbEnabled_whenAwbOperationsSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(
+                    RestrictedCameraControl.AWB_REGION,
+                ))
+
+        // 2. Act
+        cameraUseCaseAdapter.cameraControl.startFocusAndMetering(
+            getFocusMeteringAction()
+        ).await()
+
+        // 3. Assert
+        // Only AWB point remains, AF/AE points removed.
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAwb?.size)
+            .isEqualTo(1)
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAf)
+            .isEmpty()
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction?.meteringPointsAe)
+            .isEmpty()
+    }
+
+    @Test
+    fun focusMetering_disabled_whenNoneIsSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(
+                    RestrictedCameraControl.AE_REGION,
+                ))
+
+        // 2. Act && Assert
+        assertThrows<IllegalStateException> {
+            cameraUseCaseAdapter.cameraControl.startFocusAndMetering(
+                getFocusMeteringAction(FLAG_AF or FLAG_AWB)
+            ).await()
+        }
+        assertThat(fakeCameraControl.lastSubmittedFocusMeteringAction).isNull()
+    }
+
+    @Test
+    fun exposureEnabled_whenExposureOperationSupported(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(RestrictedCameraControl.EXPOSURE_COMPENSATION))
+
+        // 2. Act
+        cameraUseCaseAdapter.cameraControl.setExposureCompensationIndex(0).await()
+
+        // 3. Assert
+        assertThat(fakeCameraControl.exposureCompensationIndex).isEqualTo(0)
+    }
+
+    @Test
+    fun cameraInfo_returnsDisabledState_AllOpsDisabled(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = emptySet())
+
+        // 2. Act && Assert
+        // Zoom is disabled
+        val zoomState = cameraUseCaseAdapter.cameraInfo.zoomState.value!!
+        assertThat(zoomState.minZoomRatio).isEqualTo(1f)
+        assertThat(zoomState.maxZoomRatio).isEqualTo(1f)
+        assertThat(zoomState.zoomRatio).isEqualTo(1f)
+        assertThat(zoomState.linearZoom).isEqualTo(0f)
+
+        // Flash is disabled
+        assertThat(cameraUseCaseAdapter.cameraInfo.hasFlashUnit()).isFalse()
+
+        // Torch is disabled.
+        assertThat(cameraUseCaseAdapter.cameraInfo.torchState.value).isEqualTo(TorchState.OFF)
+
+        // FocusMetering is disabled.
+        assertThat(cameraUseCaseAdapter.cameraInfo
+            .isFocusMeteringSupported(getFocusMeteringAction()))
+            .isFalse()
+
+        // ExposureCompensation is disabled.
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.isExposureCompensationSupported)
+            .isFalse()
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.exposureCompensationRange)
+            .isEqualTo(Range(0, 0))
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.exposureCompensationStep)
+            .isEqualTo(Rational.ZERO)
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.exposureCompensationIndex)
+            .isEqualTo(0)
+    }
+
+    @Test
+    fun cameraInfo_zoomEnabled(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(RestrictedCameraControl.ZOOM)
+            )
+        fakeCameraInfo.setZoom(10f, 0.6f, 10f, 1f)
+
+        // 2. Act
+        val zoomState = cameraUseCaseAdapter.cameraInfo.zoomState.value!!
+
+        // 3. Assert
+        val fakeZoomState = fakeCameraInfo.zoomState.value!!
+        assertThat(zoomState.zoomRatio).isEqualTo(fakeZoomState.zoomRatio)
+        assertThat(zoomState.minZoomRatio).isEqualTo(fakeZoomState.minZoomRatio)
+        assertThat(zoomState.maxZoomRatio).isEqualTo(fakeZoomState.maxZoomRatio)
+        assertThat(zoomState.linearZoom).isEqualTo(fakeZoomState.linearZoom)
+    }
+
+    @Test
+    fun cameraInfo_torchEnabled(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(RestrictedCameraControl.TORCH)
+            )
+        fakeCameraInfo.setTorch(TorchState.ON)
+
+        // 2. Act && Assert
+        assertThat(cameraUseCaseAdapter.cameraInfo.torchState.value)
+            .isEqualTo(fakeCameraInfo.torchState.value)
+    }
+
+    @Test
+    fun cameraInfo_afEnabled(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(
+                    RestrictedCameraControl.AUTO_FOCUS,
+                    RestrictedCameraControl.AF_REGION
+                )
+            )
+        fakeCameraInfo.setIsFocusMeteringSupported(true)
+
+        // 2. Act && Assert
+        assertThat(cameraUseCaseAdapter.cameraInfo.isFocusMeteringSupported(
+            getFocusMeteringAction()
+        )).isTrue()
+    }
+
+    @Test
+    fun cameraInfo_exposureExposureEnabled(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(
+                    RestrictedCameraControl.EXPOSURE_COMPENSATION,
+                )
+            )
+        fakeCameraInfo.setExposureState(2, Range.create(0, 10), Rational(1, 1), true)
+
+        // 2. Act && Assert
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.exposureCompensationIndex)
+            .isEqualTo(fakeCameraInfo.exposureState.exposureCompensationIndex)
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.exposureCompensationRange)
+            .isEqualTo(fakeCameraInfo.exposureState.exposureCompensationRange)
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.exposureCompensationStep)
+            .isEqualTo(fakeCameraInfo.exposureState.exposureCompensationStep)
+        assertThat(cameraUseCaseAdapter.cameraInfo.exposureState.isExposureCompensationSupported)
+            .isEqualTo(fakeCameraInfo.exposureState.isExposureCompensationSupported)
+    }
+
+    @Test
+    fun cameraInfo_flashEnabled(): Unit = runBlocking {
+        // 1. Arrange
+        val cameraUseCaseAdapter =
+            createAdapterWithSupportedCameraOperations(
+                supportedOps = setOf(RestrictedCameraControl.FLASH)
+            )
+
+        // 2. Act && Assert
+        assertThat(cameraUseCaseAdapter.cameraInfo.hasFlashUnit())
+            .isEqualTo(fakeCameraInfo.hasFlashUnit())
+    }
+
     private fun createCoexistingRequiredRuleCameraConfig(): CameraConfig {
         return object : CameraConfig {
             private val mUseCaseConfigFactory =
@@ -950,7 +1278,9 @@
         return false
     }
 
-    private class FakeCameraConfig : CameraConfig {
+    private class FakeCameraConfig(
+        val sessionProcessor: FakeSessionProcessor? = null
+    ) : CameraConfig {
         private val mUseCaseConfigFactory =
             UseCaseConfigFactory { _, _ -> null }
         private val mIdentifier = Identifier.create(Any())
@@ -965,5 +1295,13 @@
         override fun getConfig(): Config {
             return OptionsBundle.emptyBundle()
         }
+
+        override fun getSessionProcessor(valueIfMissing: SessionProcessor?): SessionProcessor? {
+            return sessionProcessor ?: valueIfMissing
+        }
+
+        override fun getSessionProcessor(): SessionProcessor {
+            return sessionProcessor!!
+        }
     }
 }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index 0e76994..cfa06c8 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -204,7 +204,8 @@
     @Test
     fun canInvokeStartTrigger() = runBlocking {
         val fakeSessionProcessImpl = FakeSessionProcessImpl()
-        val advancedSessionProcessor = AdvancedSessionProcessor(fakeSessionProcessImpl, context)
+        val advancedSessionProcessor = AdvancedSessionProcessor(
+            fakeSessionProcessImpl, emptyList(), context)
 
         val parametersMap: MutableMap<CaptureRequest.Key<*>, Any> = mutableMapOf(
             CaptureRequest.CONTROL_AF_MODE to CaptureRequest.CONTROL_AF_MODE_AUTO,
@@ -375,7 +376,8 @@
         imageCapture: ImageCapture,
         imageAnalysis: ImageAnalysis? = null
     ) {
-        val advancedSessionProcessor = AdvancedSessionProcessor(fakeSessionProcessImpl, context)
+        val advancedSessionProcessor = AdvancedSessionProcessor(fakeSessionProcessImpl,
+            emptyList(), context)
         val latchPreviewFrame = CountDownLatch(1)
         val latchAnalysis = CountDownLatch(1)
         val deferCapturedImage = CompletableDeferred<ImageProxy>()
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index f9e1b5a..288929c 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -154,7 +154,7 @@
         fakePreviewExtenderImpl = FakePreviewExtenderImpl(previewProcessorType)
         fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(hasCaptureProcessor)
         basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
-            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, emptyList(), context
         )
     }
 
@@ -205,7 +205,7 @@
             hasCaptureProcessor, throwErrorOnProcess = true
         )
         basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
-            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, emptyList(), context
         )
         val preview = Preview.Builder().build()
         val imageCapture = ImageCapture.Builder().build()
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
index 17a7a78..5722a6b 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
 import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
@@ -27,9 +28,11 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraInfo;
+import androidx.camera.core.Logger;
 import androidx.camera.core.impl.SessionProcessor;
 import androidx.camera.extensions.ExtensionMode;
 import androidx.camera.extensions.impl.advanced.AdvancedExtenderImpl;
@@ -52,6 +55,7 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class AdvancedVendorExtender implements VendorExtender {
+    private static final String TAG = "AdvancedVendorExtender";
     private final ExtensionDisabledValidator mExtensionDisabledValidator =
             new ExtensionDisabledValidator();
     private final AdvancedExtenderImpl mAdvancedExtenderImpl;
@@ -84,6 +88,11 @@
         }
     }
 
+    @VisibleForTesting
+    AdvancedVendorExtender(AdvancedExtenderImpl advancedExtenderImpl) {
+        mAdvancedExtenderImpl = advancedExtenderImpl;
+    }
+
     @OptIn(markerClass = ExperimentalCamera2Interop.class)
     @Override
     public void init(@NonNull CameraInfo cameraInfo) {
@@ -151,11 +160,28 @@
         return yuvList == null ? new Size[0] : yuvList.toArray(new Size[0]);
     }
 
+    @NonNull
+    private List<CaptureRequest.Key> getSupportedParameterKeys() {
+        List<CaptureRequest.Key> keys = Collections.emptyList();
+        if (ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_3) >= 0) {
+            try {
+                keys = Collections.unmodifiableList(
+                        mAdvancedExtenderImpl.getAvailableCaptureRequestKeys());
+            } catch (Exception e) {
+                Logger.e(TAG, "AdvancedExtenderImpl.getAvailableCaptureRequestKeys "
+                        + "throws exceptions", e);
+            }
+        }
+        return keys;
+    }
+
     @Nullable
     @Override
     public SessionProcessor createSessionProcessor(@NonNull Context context) {
         Preconditions.checkNotNull(mCameraId, "VendorExtender#init() must be called first");
         return new AdvancedSessionProcessor(
-                mAdvancedExtenderImpl.createSessionProcessor(), context);
+                mAdvancedExtenderImpl.createSessionProcessor(),
+                getSupportedParameterKeys(),
+                context);
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index 2a7ed76..61f0550 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -19,7 +19,9 @@
 import android.content.Context;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
@@ -28,6 +30,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraInfo;
@@ -46,14 +49,14 @@
 import androidx.camera.extensions.impl.NightImageCaptureExtenderImpl;
 import androidx.camera.extensions.impl.NightPreviewExtenderImpl;
 import androidx.camera.extensions.impl.PreviewExtenderImpl;
+import androidx.camera.extensions.internal.compat.workaround.AvailableKeysRetriever;
 import androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator;
 import androidx.camera.extensions.internal.sessionprocessor.BasicExtenderSessionProcessor;
 import androidx.core.util.Preconditions;
 
-import org.jetbrains.annotations.TestOnly;
-
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -69,6 +72,27 @@
     private PreviewExtenderImpl mPreviewExtenderImpl = null;
     private ImageCaptureExtenderImpl mImageCaptureExtenderImpl = null;
     private CameraInfo mCameraInfo;
+    private String mCameraId;
+    private CameraCharacteristics mCameraCharacteristics;
+    private AvailableKeysRetriever mAvailableKeysRetriever = new AvailableKeysRetriever();
+
+    static final List<CaptureRequest.Key> sBaseSupportedKeys = new ArrayList<>(Arrays.asList(
+            CaptureRequest.SCALER_CROP_REGION,
+            CaptureRequest.CONTROL_AF_MODE,
+            CaptureRequest.CONTROL_AF_TRIGGER,
+            CaptureRequest.CONTROL_AF_REGIONS,
+            CaptureRequest.CONTROL_AE_REGIONS,
+            CaptureRequest.CONTROL_AWB_REGIONS,
+            CaptureRequest.CONTROL_AE_MODE,
+            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+            CaptureRequest.FLASH_MODE,
+            CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION
+    ));
+    static {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            sBaseSupportedKeys.add(CaptureRequest.CONTROL_ZOOM_RATIO);
+        }
+    }
 
     public BasicVendorExtender(@ExtensionMode.Mode int mode) {
         try {
@@ -102,7 +126,7 @@
         }
     }
 
-    @TestOnly
+    @VisibleForTesting
     BasicVendorExtender(ImageCaptureExtenderImpl imageCaptureExtenderImpl,
             PreviewExtenderImpl previewExtenderImpl) {
         mPreviewExtenderImpl = previewExtenderImpl;
@@ -136,11 +160,11 @@
             return;
         }
 
-        String cameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
-        CameraCharacteristics cameraCharacteristics =
+        mCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
+        mCameraCharacteristics =
                 Camera2CameraInfo.extractCameraCharacteristics(cameraInfo);
-        mPreviewExtenderImpl.init(cameraId, cameraCharacteristics);
-        mImageCaptureExtenderImpl.init(cameraId, cameraCharacteristics);
+        mPreviewExtenderImpl.init(mCameraId, mCameraCharacteristics);
+        mImageCaptureExtenderImpl.init(mCameraId, mCameraCharacteristics);
 
         Logger.d(TAG, "PreviewExtender processorType= " + mPreviewExtenderImpl.getProcessorType());
         Logger.d(TAG, "ImageCaptureExtender processor= "
@@ -285,11 +309,41 @@
         return getOutputSizes(ImageFormat.YUV_420_888);
     }
 
+    @NonNull
+    private List<CaptureRequest.Key> getSupportedParameterKeys(Context context) {
+        if (ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_3) >= 0) {
+            try {
+                List<CaptureRequest.Key> keys =
+                        Collections.unmodifiableList(
+                                mAvailableKeysRetriever.getAvailableCaptureRequestKeys(
+                                        mImageCaptureExtenderImpl,
+                                        mCameraId,
+                                        mCameraCharacteristics,
+                                        context));
+                if (keys == null) {
+                    keys = Collections.emptyList();
+                }
+                return keys;
+            } catch (Exception e) {
+                // it could crash on some OEMs.
+                Logger.e(TAG, "ImageCaptureExtenderImpl.getAvailableCaptureRequestKeys "
+                        + "throws exceptions", e);
+                return Collections.emptyList();
+            }
+        } else {
+            // For Basic Extender implementing v1.2 or below, we assume zoom/tap-to-focus/flash/EC
+            // are supported for compatibility reason.
+            return Collections.unmodifiableList(sBaseSupportedKeys);
+        }
+    }
+
     @Nullable
     @Override
     public SessionProcessor createSessionProcessor(@NonNull Context context) {
         Preconditions.checkNotNull(mCameraInfo, "VendorExtender#init() must be called first");
-        return new BasicExtenderSessionProcessor(mPreviewExtenderImpl, mImageCaptureExtenderImpl,
+        return new BasicExtenderSessionProcessor(
+                mPreviewExtenderImpl, mImageCaptureExtenderImpl,
+                getSupportedParameterKeys(context),
                 context);
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java
index ec3f327..52af0b8 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java
@@ -22,6 +22,8 @@
 import androidx.camera.core.Logger;
 import androidx.camera.extensions.impl.ExtensionVersionImpl;
 
+import org.jetbrains.annotations.TestOnly;
+
 /**
  * Provides interfaces to check the extension version.
  */
@@ -31,6 +33,15 @@
 
     private static volatile ExtensionVersion sExtensionVersion;
 
+    /**
+     * For testing only. Inject a fake {@link ExtensionVersion}. Set it to {@code null} to unset
+     * it.
+     */
+    @TestOnly
+    public static void injectInstance(@Nullable ExtensionVersion extensionVersion) {
+        sExtensionVersion = extensionVersion;
+    }
+
     private static ExtensionVersion getInstance() {
         if (sExtensionVersion != null) {
             return sExtensionVersion;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
index 82bb2e7..532e1e8 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
@@ -48,6 +48,10 @@
             quirks.add(new CrashWhenOnDisableTooSoon());
         }
 
+        if (GetAvailableKeysNeedsOnInit.load()) {
+            quirks.add(new GetAvailableKeysNeedsOnInit());
+        }
+
         return quirks;
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/GetAvailableKeysNeedsOnInit.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/GetAvailableKeysNeedsOnInit.java
new file mode 100644
index 0000000..57f0e76
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/GetAvailableKeysNeedsOnInit.java
@@ -0,0 +1,38 @@
+/*
+ * 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.extensions.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: b/279541627,
+ * Description: ImageCaptureExtenderImpl.getAvailableCaptureRequestKeys and
+ * getAvailableCaptureResultKeys incorrectly expect onInit() to be invoked to supply the
+ * CameraCharacteristics. It causes a {@link NullPointerException} if onInit() is not invoked in
+ * prior to getAvailableCaptureRequestKeys or getAvailableCaptureResultKeys.
+ * Device(s): All Samsung devices
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class GetAvailableKeysNeedsOnInit implements Quirk {
+    static boolean load() {
+        return Build.BRAND.equalsIgnoreCase("SAMSUNG");
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/AvailableKeysRetriever.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/AvailableKeysRetriever.java
new file mode 100644
index 0000000..605719e
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/AvailableKeysRetriever.java
@@ -0,0 +1,68 @@
+/*
+ * 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.extensions.internal.compat.workaround;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
+import androidx.camera.extensions.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.extensions.internal.compat.quirk.GetAvailableKeysNeedsOnInit;
+
+import java.util.List;
+
+/**
+ * A workaround for getting the available CaptureRequest keys safely.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class AvailableKeysRetriever {
+    boolean mShouldInvokeOnInit;
+
+    /**
+     * Default constructor.
+     */
+    public AvailableKeysRetriever() {
+        mShouldInvokeOnInit = DeviceQuirks.get(GetAvailableKeysNeedsOnInit.class) != null;
+    }
+
+    /**
+     * Get available CaptureRequest keys from the given {@link ImageCaptureExtenderImpl}. The
+     * cameraId, cameraCharacteristics and the context is needed for invoking onInit whenever
+     * necessary.
+     */
+    @NonNull
+    public List<CaptureRequest.Key> getAvailableCaptureRequestKeys(
+            @NonNull ImageCaptureExtenderImpl imageCaptureExtender,
+            @NonNull String cameraId,
+            @NonNull CameraCharacteristics cameraCharacteristics,
+            @NonNull Context context) {
+        if (mShouldInvokeOnInit) {
+            imageCaptureExtender.onInit(cameraId, cameraCharacteristics, context);
+        }
+
+        try {
+            return imageCaptureExtender.getAvailableCaptureRequestKeys();
+        } finally {
+            if (mShouldInvokeOnInit) {
+                imageCaptureExtender.onDeInit();
+            }
+        }
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
index ac7b358..ab42134 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
@@ -62,7 +62,10 @@
     private final SessionProcessorImpl mImpl;
     private final Context mContext;
 
-    public AdvancedSessionProcessor(@NonNull SessionProcessorImpl impl, @NonNull Context context) {
+    public AdvancedSessionProcessor(@NonNull SessionProcessorImpl impl,
+            @NonNull List<CaptureRequest.Key> supportedKeys,
+            @NonNull Context context) {
+        super(supportedKeys);
         mImpl = impl;
         mContext = context;
     }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index 5452a8a..4a282ab 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -96,7 +96,9 @@
 
     public BasicExtenderSessionProcessor(@NonNull PreviewExtenderImpl previewExtenderImpl,
             @NonNull ImageCaptureExtenderImpl imageCaptureExtenderImpl,
+            @NonNull List<CaptureRequest.Key> supportedKeys,
             @NonNull Context context) {
+        super(supportedKeys);
         mPreviewExtenderImpl = previewExtenderImpl;
         mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
         mContext = context;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SessionProcessorBase.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SessionProcessorBase.java
index a10ddcb..f02f5fa 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SessionProcessorBase.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SessionProcessorBase.java
@@ -20,6 +20,7 @@
 import android.hardware.camera2.CaptureRequest;
 import android.media.Image;
 import android.media.ImageReader;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 
@@ -36,15 +37,20 @@
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.OutputSurface;
+import androidx.camera.core.impl.RestrictedCameraControl;
+import androidx.camera.core.impl.RestrictedCameraControl.CameraOperation;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.SessionProcessor;
 import androidx.camera.core.impl.SessionProcessorSurface;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Base class for SessionProcessor implementation. It is responsible for creating image readers and
@@ -67,6 +73,67 @@
     private String mCameraId;
 
     @NonNull
+
+    private final @CameraOperation Set<Integer> mSupportedCameraOperations;
+
+    SessionProcessorBase(@NonNull List<CaptureRequest.Key> supportedParameterKeys) {
+        mSupportedCameraOperations = getSupportedCameraOperations(supportedParameterKeys);
+    }
+
+    private @CameraOperation Set<Integer> getSupportedCameraOperations(
+            @NonNull List<CaptureRequest.Key> supportedParameterKeys) {
+        @CameraOperation Set<Integer> operations = new HashSet<>();
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            if (supportedParameterKeys.contains(CaptureRequest.CONTROL_ZOOM_RATIO)
+                    || supportedParameterKeys.contains(CaptureRequest.SCALER_CROP_REGION)) {
+                operations.add(RestrictedCameraControl.ZOOM);
+            }
+        } else {
+            if (supportedParameterKeys.contains(CaptureRequest.SCALER_CROP_REGION)) {
+                operations.add(RestrictedCameraControl.ZOOM);
+            }
+        }
+
+        if (supportedParameterKeys.containsAll(
+                Arrays.asList(
+                        CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_MODE))) {
+            operations.add(RestrictedCameraControl.AUTO_FOCUS);
+        }
+
+        if (supportedParameterKeys.contains(CaptureRequest.CONTROL_AF_REGIONS)) {
+            operations.add(RestrictedCameraControl.AF_REGION);
+        }
+
+        if (supportedParameterKeys.contains(CaptureRequest.CONTROL_AE_REGIONS)) {
+            operations.add(RestrictedCameraControl.AE_REGION);
+        }
+
+        if (supportedParameterKeys.contains(CaptureRequest.CONTROL_AWB_REGIONS)) {
+            operations.add(RestrictedCameraControl.AWB_REGION);
+        }
+
+        if (supportedParameterKeys.containsAll(
+                Arrays.asList(
+                        CaptureRequest.CONTROL_AE_MODE,
+                        CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER))) {
+            operations.add(RestrictedCameraControl.FLASH);
+        }
+
+        if (supportedParameterKeys.containsAll(
+                Arrays.asList(
+                        CaptureRequest.CONTROL_AE_MODE,
+                        CaptureRequest.FLASH_MODE))) {
+            operations.add(RestrictedCameraControl.TORCH);
+        }
+
+        if (supportedParameterKeys.contains(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION)) {
+            operations.add(RestrictedCameraControl.EXPOSURE_COMPENSATION);
+        }
+        return operations;
+    }
+
+    @NonNull
     private static SessionProcessorSurface createOutputConfigSurface(
             @NonNull Camera2OutputConfig outputConfig, Map<Integer, ImageReader> imageReaderMap) {
         if (outputConfig instanceof SurfaceOutputConfig) {
@@ -97,6 +164,7 @@
         }
         throw new UnsupportedOperationException("Unsupported Camera2OutputConfig:" + outputConfig);
     }
+
     @NonNull
     @Override
     @OptIn(markerClass = ExperimentalCamera2Interop.class)
@@ -163,6 +231,12 @@
     }
 
     @NonNull
+    @Override
+    public @CameraOperation Set<Integer> getSupportedCameraOperations() {
+        return mSupportedCameraOperations;
+    }
+
+    @NonNull
     protected abstract Camera2SessionConfig initSessionInternal(
             @NonNull String cameraId,
             @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/SupportedCameraOperationsTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/SupportedCameraOperationsTest.kt
new file mode 100644
index 0000000..0efc40a
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/SupportedCameraOperationsTest.kt
@@ -0,0 +1,457 @@
+/*
+ * 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.extensions.internal
+
+import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.params.SessionConfiguration
+import android.os.Build
+import android.util.Pair
+import android.util.Range
+import android.util.Size
+import androidx.camera.camera2.internal.Camera2CameraInfoImpl
+import androidx.camera.camera2.internal.compat.CameraManagerCompat
+import androidx.camera.core.impl.RestrictedCameraControl
+import androidx.camera.extensions.impl.CaptureStageImpl
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
+import androidx.camera.extensions.impl.advanced.AdvancedExtenderImpl
+import androidx.camera.extensions.impl.advanced.Camera2SessionConfigImpl
+import androidx.camera.extensions.impl.advanced.OutputSurfaceConfigurationImpl
+import androidx.camera.extensions.impl.advanced.OutputSurfaceImpl
+import androidx.camera.extensions.impl.advanced.RequestProcessorImpl
+import androidx.camera.extensions.impl.advanced.SessionProcessorImpl
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowCameraManager
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP,
+    instrumentedPackages = arrayOf("androidx.camera.extensions.internal")
+)
+class SupportedCameraOperationsTest(
+    private val extenderType: String
+) {
+    val context = RuntimeEnvironment.getApplication()
+
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+        fun createTestSet(): List<String> {
+            return listOf("basic", "advanced")
+        }
+    }
+    private fun setCameraXExtensionsVersion(version: String) {
+        val field = VersionName::class.java.getDeclaredField("CURRENT")
+        field.isAccessible = true
+        field[null] = VersionName(version)
+    }
+
+    private fun setExtensionRuntimeVersion(version: String) {
+        ExtensionVersion.injectInstance(object : ExtensionVersion() {
+            override fun isAdvancedExtenderSupportedInternal(): Boolean {
+                return false
+            }
+
+            override fun getVersionObject(): Version {
+                return Version.parse(version)!!
+            }
+        })
+    }
+
+    @Before
+    fun setUp() {
+        setupCameraCharacteristics()
+        setCameraXExtensionsVersion("1.3.0")
+        setExtensionRuntimeVersion("1.3.0")
+    }
+
+    private fun setupCameraCharacteristics() {
+        val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+        val shadowCharacteristics = Shadow.extract<ShadowCameraCharacteristics>(characteristics)
+        shadowCharacteristics.set(
+            CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK
+        )
+        shadowCharacteristics.set(
+            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, arrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE,
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
+            )
+        )
+        val cameraManager = ApplicationProvider.getApplicationContext<Context>()
+            .getSystemService(Context.CAMERA_SERVICE) as CameraManager
+        (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
+            .addCamera("0", characteristics)
+    }
+
+    private fun testSupportedCameraOperation(
+        supportedCaptureRequestKeys: List<CaptureRequest.Key<out Any>>,
+        @RestrictedCameraControl.CameraOperation expectSupportedOperations: Set<Int>
+    ) {
+        var vendorExtender: VendorExtender? = null
+        if (extenderType == "basic") {
+            val fakeImageCaptureExtenderImpl = FakeImageCaptureExtenderImpl(
+                supportedRequestKeys = supportedCaptureRequestKeys
+            )
+            vendorExtender = BasicVendorExtender(fakeImageCaptureExtenderImpl, null)
+        } else if (extenderType == "advanced") {
+            val fakeAdvancedExtenderImpl = FakeAdvancedVendorExtenderImpl(
+                supportedRequestKeys = supportedCaptureRequestKeys
+            )
+            vendorExtender = AdvancedVendorExtender(fakeAdvancedExtenderImpl)
+        }
+
+        val cameraInfo = Camera2CameraInfoImpl("0", CameraManagerCompat.from(context))
+        vendorExtender!!.init(cameraInfo)
+        val sessionProcessor = vendorExtender.createSessionProcessor(context)!!
+        assertThat(sessionProcessor.supportedCameraOperations)
+            .containsExactlyElementsIn(expectSupportedOperations)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.R)
+    @Test
+    fun supportedCameraOperations_zoomIsEnabled_androidR() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_ZOOM_RATIO
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.ZOOM
+            )
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.R)
+    @Test
+    fun supportedCameraOperations_cropregion_zoomIsEnabled_androidR() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.SCALER_CROP_REGION
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.ZOOM
+            )
+        )
+    }
+
+    @Config(maxSdk = Build.VERSION_CODES.Q)
+    @Test
+    fun supportedCameraOperations_zoomIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.SCALER_CROP_REGION
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.ZOOM
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_autoFocusIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AF_MODE,
+                CaptureRequest.CONTROL_AF_TRIGGER
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.AUTO_FOCUS
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_afRegionIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AF_REGIONS,
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.AF_REGION
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_aeRegionIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AE_REGIONS,
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.AE_REGION
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_awbRegionIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AWB_REGIONS,
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.AWB_REGION
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_torchIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AE_MODE,
+                CaptureRequest.FLASH_MODE
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.TORCH
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_flashIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AE_MODE,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.FLASH
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_exposureCompensationIsEnabled() {
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION,
+            ),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.EXPOSURE_COMPENSATION
+            )
+        )
+    }
+
+    // For Basic extender under 1.3.0, ensures all operations are supported
+    @Test
+    fun supportedCameraOperations_allOperationsEnabled_basic1_2_and_below() {
+        assumeTrue(extenderType == "basic")
+        setExtensionRuntimeVersion("1.2.0")
+        setCameraXExtensionsVersion("1.3.0")
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = emptyList(),
+            expectSupportedOperations = setOf(
+                RestrictedCameraControl.ZOOM,
+                RestrictedCameraControl.AUTO_FOCUS,
+                RestrictedCameraControl.TORCH,
+                RestrictedCameraControl.AF_REGION,
+                RestrictedCameraControl.AE_REGION,
+                RestrictedCameraControl.AWB_REGION,
+                RestrictedCameraControl.EXPOSURE_COMPENSATION,
+                RestrictedCameraControl.FLASH,
+            )
+        )
+    }
+
+    @Test
+    fun supportedCameraOperations_allOperationsDisabled_advanced1_2_and_below() {
+        assumeTrue(extenderType == "advanced")
+        setExtensionRuntimeVersion("1.2.0")
+        setCameraXExtensionsVersion("1.3.0")
+        testSupportedCameraOperation(
+            supportedCaptureRequestKeys = listOf(
+                CaptureRequest.SCALER_CROP_REGION,
+                CaptureRequest.CONTROL_AF_MODE,
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_REGIONS,
+                CaptureRequest.CONTROL_AE_REGIONS,
+                CaptureRequest.CONTROL_AWB_REGIONS,
+                CaptureRequest.CONTROL_AE_MODE,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.FLASH_MODE,
+                CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION
+            ),
+            expectSupportedOperations = emptySet() // No ops should be supported.
+        )
+    }
+
+    private class FakeImageCaptureExtenderImpl(
+        val supportedRequestKeys: List<CaptureRequest.Key<out Any>>
+    ) : ImageCaptureExtenderImpl {
+        override fun isExtensionAvailable(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics
+        ): Boolean = true
+        override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {
+        }
+        override fun getCaptureProcessor() = null
+        override fun getCaptureStages(): List<CaptureStageImpl> = emptyList()
+        override fun getMaxCaptureStage() = 2
+        override fun getSupportedResolutions() = null
+        override fun getEstimatedCaptureLatencyRange(size: Size?) = null
+        override fun getAvailableCaptureRequestKeys(): List<CaptureRequest.Key<out Any>> {
+            return supportedRequestKeys
+        }
+
+        override fun getAvailableCaptureResultKeys(): List<CaptureResult.Key<Any>> {
+            return mutableListOf()
+        }
+
+        override fun getSupportedPostviewResolutions(
+            captureSize: Size
+        ): MutableList<Pair<Int, Array<Size>>>? = null
+
+        override fun isCaptureProcessProgressAvailable() = false
+
+        override fun getRealtimeCaptureLatency(): Pair<Long, Long>? = null
+        override fun isPostviewAvailable() = false
+        override fun onInit(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics,
+            context: Context
+        ) {}
+
+        override fun onDeInit() {}
+        override fun onPresetSession(): CaptureStageImpl? = null
+
+        override fun onEnableSession(): CaptureStageImpl? = null
+
+        override fun onDisableSession(): CaptureStageImpl? = null
+        override fun onSessionType(): Int = SessionConfiguration.SESSION_REGULAR
+    }
+
+    private class FakeAdvancedVendorExtenderImpl(
+        val supportedRequestKeys: List<CaptureRequest.Key<out Any>>
+    ) : AdvancedExtenderImpl {
+        override fun isExtensionAvailable(
+            cameraId: String,
+            characteristicsMap: MutableMap<String, CameraCharacteristics>
+        ): Boolean = true
+
+        override fun init(
+            cameraId: String,
+            characteristicsMap: MutableMap<String, CameraCharacteristics>
+        ) {}
+        override fun getEstimatedCaptureLatencyRange(
+            cameraId: String,
+            captureOutputSize: Size?,
+            imageFormat: Int
+        ): Range<Long>? = null
+        override fun getSupportedPreviewOutputResolutions(
+            cameraId: String
+        ): Map<Int, MutableList<Size>> = emptyMap()
+        override fun getSupportedCaptureOutputResolutions(
+            cameraId: String
+        ): Map<Int, MutableList<Size>> = emptyMap()
+
+        override fun getSupportedPostviewResolutions(
+            captureSize: Size
+        ): Map<Int, MutableList<Size>> = emptyMap()
+        override fun getSupportedYuvAnalysisResolutions(cameraId: String) = null
+        override fun createSessionProcessor(): SessionProcessorImpl = DummySessionProcessorImpl()
+        override fun getAvailableCaptureRequestKeys():
+            List<CaptureRequest.Key<out Any>> = supportedRequestKeys
+
+        override fun getAvailableCaptureResultKeys(): List<CaptureResult.Key<Any>> = emptyList()
+        override fun isCaptureProcessProgressAvailable() = false
+        override fun isPostviewAvailable() = false
+    }
+
+    private class DummySessionProcessorImpl : SessionProcessorImpl {
+        override fun initSession(
+            cameraId: String,
+            cameraCharacteristicsMap: MutableMap<String, CameraCharacteristics>,
+            context: Context,
+            surfaceConfigs: OutputSurfaceConfigurationImpl
+        ): Camera2SessionConfigImpl {
+            throw UnsupportedOperationException("Not supported")
+        }
+        override fun initSession(
+            cameraId: String,
+            cameraCharacteristicsMap: MutableMap<String, CameraCharacteristics>,
+            context: Context,
+            previewSurfaceConfig: OutputSurfaceImpl,
+            imageCaptureSurfaceConfig: OutputSurfaceImpl,
+            imageAnalysisSurfaceConfig: OutputSurfaceImpl?
+        ): Camera2SessionConfigImpl {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun deInitSession() {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun setParameters(parameters: MutableMap<CaptureRequest.Key<*>, Any>) {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun startTrigger(
+            triggers: MutableMap<CaptureRequest.Key<*>, Any>,
+            callback: SessionProcessorImpl.CaptureCallback
+        ): Int {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun onCaptureSessionStart(requestProcessor: RequestProcessorImpl) {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun onCaptureSessionEnd() {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun startRepeating(callback: SessionProcessorImpl.CaptureCallback): Int {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun stopRepeating() {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun startCapture(callback: SessionProcessorImpl.CaptureCallback): Int {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun startCaptureWithPostview(callback: SessionProcessorImpl.CaptureCallback): Int {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun abortCapture(captureSequenceId: Int) {
+            throw UnsupportedOperationException("Not supported")
+        }
+
+        override fun getRealtimeCaptureLatency(): Pair<Long, Long>? {
+            throw UnsupportedOperationException("Not supported")
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/AvailableKeysRetrieverTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/AvailableKeysRetrieverTest.kt
new file mode 100644
index 0000000..816aaf4
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/AvailableKeysRetrieverTest.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.extensions.internal.compat.workaround
+
+import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.params.SessionConfiguration
+import android.os.Build
+import android.util.Pair
+import android.util.Size
+import androidx.camera.extensions.impl.CaptureStageImpl
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP,
+    instrumentedPackages = arrayOf("androidx.camera.extensions.internal")
+)
+class AvailableKeysRetrieverTest {
+    private val context: Context = RuntimeEnvironment.getApplication()
+    private val availableKeys = listOf<CaptureRequest.Key<out Any>>(
+        CaptureRequest.CONTROL_AE_REGIONS, CaptureRequest.CONTROL_AF_MODE
+    )
+    private val fakeImageCaptureExtenderImpl = FakeImageCaptureExtenderImpl(availableKeys)
+    private val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+
+    @Test
+    fun shouldInvokeOnInit() {
+        // 1. Arrange
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "SAMSUNG")
+        val retriever = AvailableKeysRetriever()
+
+        // 2. Act
+        val resultKeys = retriever.getAvailableCaptureRequestKeys(
+            fakeImageCaptureExtenderImpl, "0", characteristics, context)
+
+        // 3. Assert
+        assertThat(resultKeys).containsExactlyElementsIn(availableKeys)
+        assertThat(fakeImageCaptureExtenderImpl.invokeList).containsExactly(
+            "onInit", "getAvailableCaptureRequestKeys", "onDeInit"
+        )
+    }
+
+    @Test
+    fun shouldNotInvokeOnInit() {
+        // 1. Arrange
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "OTHER")
+        val retriever = AvailableKeysRetriever()
+
+        // 2. Act
+        val resultKeys = retriever.getAvailableCaptureRequestKeys(
+            fakeImageCaptureExtenderImpl, "0", characteristics, context)
+
+        // 3. Assert
+        assertThat(resultKeys).containsExactlyElementsIn(availableKeys)
+        assertThat(fakeImageCaptureExtenderImpl.invokeList).containsExactly(
+            "getAvailableCaptureRequestKeys"
+        )
+    }
+
+    class FakeImageCaptureExtenderImpl(
+        private var availableRequestKeys: List<CaptureRequest.Key<out Any>>
+    ) : ImageCaptureExtenderImpl {
+        val invokeList = mutableListOf<String>()
+        override fun isExtensionAvailable(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics
+        ): Boolean = true
+        override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {
+            invokeList.add("init")
+        }
+        override fun getCaptureProcessor() = null
+        override fun getCaptureStages(): List<CaptureStageImpl> = emptyList()
+        override fun getMaxCaptureStage() = 2
+        override fun getSupportedResolutions() = null
+        override fun getEstimatedCaptureLatencyRange(size: Size?) = null
+        override fun getAvailableCaptureRequestKeys(): List<CaptureRequest.Key<out Any>> {
+            invokeList.add("getAvailableCaptureRequestKeys")
+
+            return availableRequestKeys
+        }
+
+        override fun getAvailableCaptureResultKeys(): List<CaptureResult.Key<Any>> {
+            return mutableListOf()
+        }
+
+        override fun getSupportedPostviewResolutions(
+            captureSize: Size
+        ): MutableList<Pair<Int, Array<Size>>>? = null
+
+        override fun isCaptureProcessProgressAvailable() = false
+
+        override fun getRealtimeCaptureLatency(): Pair<Long, Long>? = null
+        override fun isPostviewAvailable() = false
+        override fun onInit(
+            cameraId: String,
+            cameraCharacteristics: CameraCharacteristics,
+            context: Context
+        ) {
+            invokeList.add("onInit")
+        }
+
+        override fun onDeInit() {
+            invokeList.add("onDeInit")
+        }
+        override fun onPresetSession(): CaptureStageImpl? = null
+        override fun onEnableSession(): CaptureStageImpl? = null
+        override fun onDisableSession(): CaptureStageImpl? = null
+        override fun onSessionType(): Int = SessionConfiguration.SESSION_REGULAR
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index c032e08..73c441f 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -23,6 +23,7 @@
 import android.util.Size;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.FocusMeteringResult;
@@ -80,6 +81,10 @@
     private float mZoomRatio = -1;
     private float mLinearZoom = -1;
     private boolean mTorchEnabled = false;
+    private int mExposureCompensation = -1;
+
+    @Nullable
+    private FocusMeteringAction mLastSubmittedFocusMeteringAction = null;
 
     public FakeCameraControl() {
         this(NO_OP_CALLBACK);
@@ -189,9 +194,14 @@
     @NonNull
     @Override
     public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
+        mExposureCompensation = exposure;
         return Futures.immediateFuture(null);
     }
 
+    public int getExposureCompensationIndex() {
+        return mExposureCompensation;
+    }
+
     @NonNull
     @Override
     public ListenableFuture<List<Void>> submitStillCaptureRequests(
@@ -229,6 +239,7 @@
     @Override
     public ListenableFuture<FocusMeteringResult> startFocusAndMetering(
             @NonNull FocusMeteringAction action) {
+        mLastSubmittedFocusMeteringAction = action;
         return Futures.immediateFuture(FocusMeteringResult.emptyInstance());
     }
 
@@ -265,6 +276,11 @@
         return mLinearZoom;
     }
 
+    @Nullable
+    public FocusMeteringAction getLastSubmittedFocusMeteringAction() {
+        return mLastSubmittedFocusMeteringAction;
+    }
+
     @Override
     public void addInteropConfig(@NonNull Config config) {
         for (Config.Option<?> option : config.listOptions()) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 6fb7552..7a4b53a 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -92,6 +92,9 @@
     private boolean mIsPrivateReprocessingSupported = false;
     private float mIntrinsicZoomRatio = 1.0F;
 
+    private boolean mIsFocusMeteringSupported = false;
+
+    private ExposureState mExposureState = new FakeExposureState();
     @NonNull
     private final List<Quirk> mCameraQuirks = new ArrayList<>();
 
@@ -117,6 +120,37 @@
         mZoomLiveData = new MutableLiveData<>(ImmutableZoomState.create(1.0f, 4.0f, 1.0f, 0.0f));
     }
 
+    /**
+     * Sets the zoom parameter.
+     */
+    public void setZoom(float zoomRatio, float minZoomRatio, float maxZoomRatio, float linearZoom) {
+        mZoomLiveData.postValue(ImmutableZoomState.create(
+                zoomRatio, maxZoomRatio, minZoomRatio, linearZoom
+        ));
+    }
+
+    /**
+     * Sets the exposure compensation parameters.
+     */
+    public void setExposureState(int index, @NonNull Range<Integer> range,
+            @NonNull Rational step, boolean isSupported) {
+        mExposureState = new FakeExposureState(index, range, step, isSupported);
+    }
+
+    /**
+     * Sets the torch state.
+     */
+    public void setTorch(int torchState) {
+        mTorchState.postValue(torchState);
+    }
+
+    /**
+     * Sets the return value for {@link #isFocusMeteringSupported(FocusMeteringAction)}.
+     */
+    public void setIsFocusMeteringSupported(boolean supported) {
+        mIsFocusMeteringSupported = supported;
+    }
+
     @Override
     public int getLensFacing() {
         return mLensFacing;
@@ -169,7 +203,7 @@
     @NonNull
     @Override
     public ExposureState getExposureState() {
-        return new FakeExposureState();
+        return mExposureState;
     }
 
     @NonNull
@@ -246,7 +280,7 @@
 
     @Override
     public boolean isFocusMeteringSupported(@NonNull FocusMeteringAction action) {
-        return false;
+        return mIsFocusMeteringSupported;
     }
 
     @Override
@@ -317,26 +351,41 @@
 
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     static final class FakeExposureState implements ExposureState {
+        private int mIndex = 0;
+        private Range<Integer> mRange = new Range<>(0, 0);
+        private Rational mStep = Rational.ZERO;
+        private boolean mIsSupported = true;
+
+        FakeExposureState() {
+        }
+        FakeExposureState(int index, Range<Integer> range,
+                Rational step, boolean isSupported) {
+            mIndex = index;
+            mRange = range;
+            mStep = step;
+            mIsSupported = isSupported;
+        }
+
         @Override
         public int getExposureCompensationIndex() {
-            return 0;
+            return mIndex;
         }
 
         @NonNull
         @Override
         public Range<Integer> getExposureCompensationRange() {
-            return Range.create(0, 0);
+            return mRange;
         }
 
         @NonNull
         @Override
         public Rational getExposureCompensationStep() {
-            return Rational.ZERO;
+            return mStep;
         }
 
         @Override
         public boolean isExposureCompensationSupported() {
-            return true;
+            return mIsSupported;
         }
     }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
index e08f64e..ee51c7d 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
@@ -33,6 +33,7 @@
 import androidx.camera.core.impl.OptionsBundle
 import androidx.camera.core.impl.OutputSurface
 import androidx.camera.core.impl.RequestProcessor
+import androidx.camera.core.impl.RestrictedCameraControl
 import androidx.camera.core.impl.SessionConfig
 import androidx.camera.core.impl.SessionProcessor
 import androidx.camera.core.impl.SessionProcessorSurface
@@ -46,8 +47,8 @@
 
 @RequiresApi(28) // writing to PRIVATE surface requires API 28+
 class FakeSessionProcessor(
-    val inputFormatPreview: Int?,
-    val inputFormatCapture: Int?
+    val inputFormatPreview: Int? = null,
+    val inputFormatCapture: Int? = null
 ) : SessionProcessor {
     private lateinit var previewProcessorSurface: DeferrableSurface
     private lateinit var captureProcessorSurface: DeferrableSurface
@@ -77,6 +78,9 @@
     private var rotationDegrees = 0
     private var jpegQuality = 100
 
+    @RestrictedCameraControl.CameraOperation
+    var restrictedCameraOperations: Set<Int> = emptySet()
+
     fun releaseSurfaces() {
         intermediaPreviewImageReader?.close()
         intermediaCaptureImageReader?.close()
@@ -215,6 +219,11 @@
         return latestParameters
     }
 
+    @RestrictedCameraControl.CameraOperation
+    override fun getSupportedCameraOperations(): Set<Int> {
+        return restrictedCameraOperations
+    }
+
     override fun startRepeating(callback: SessionProcessor.CaptureCallback): Int {
         startRepeatingCalled.complete(SystemClock.elapsedRealtimeNanos())
         val builder = RequestProcessorRequest.Builder().apply {