Merge "[Multi-Cam] Add setMirrorMode() for Preview" into androidx-main
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 8703b53..162e92b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.OutputConfiguration
 import android.hardware.camera2.params.SessionConfiguration.SESSION_HIGH_SPEED
 import android.hardware.camera2.params.SessionConfiguration.SESSION_REGULAR
 import android.media.MediaCodec
@@ -54,6 +55,7 @@
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.DynamicRange
+import androidx.camera.core.MirrorMode
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.CameraInfoInternal
@@ -702,6 +704,7 @@
                     val deferrableSurface = outputConfig.surface
                     val physicalCameraId =
                         physicalCameraIdForAllStreams ?: outputConfig.physicalCameraId
+                    val mirrorMode = outputConfig.mirrorMode
                     val outputStreamConfig = OutputStream.Config.create(
                         size = deferrableSurface.prescribedSize,
                         format = StreamFormat(deferrableSurface.prescribedStreamFormat),
@@ -710,6 +713,15 @@
                         } else {
                             CameraId.fromCamera2Id(physicalCameraId)
                         },
+                        // No need to map MIRROR_MODE_ON_FRONT_ONLY to MIRROR_MODE_AUTO
+                        // since its default value in framework
+                        mirrorMode = when (mirrorMode) {
+                            MirrorMode.MIRROR_MODE_OFF -> OutputStream.MirrorMode(
+                                OutputConfiguration.MIRROR_MODE_NONE)
+                            MirrorMode.MIRROR_MODE_ON -> OutputStream.MirrorMode(
+                                OutputConfiguration.MIRROR_MODE_H)
+                            else -> null
+                        },
                         streamUseCase = getStreamUseCase(
                             deferrableSurface,
                             sessionConfigAdapter.surfaceToStreamUseCaseMap
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 5be543a..b0e3164 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -23,6 +23,7 @@
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
 import android.hardware.camera2.params.DynamicRangeProfiles;
+import android.hardware.camera2.params.OutputConfiguration;
 import android.os.Build;
 import android.view.Surface;
 
@@ -44,6 +45,7 @@
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.DynamicRange;
 import androidx.camera.core.Logger;
+import androidx.camera.core.MirrorMode;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.DeferrableSurface;
@@ -391,6 +393,14 @@
                     outputConfig.getPhysicalCameraId());
         }
 
+        // No need to map MIRROR_MODE_ON_FRONT_ONLY to MIRROR_MODE_AUTO
+        // since its default value in framework
+        if (outputConfig.getMirrorMode() == MirrorMode.MIRROR_MODE_OFF) {
+            outputConfiguration.setMirrorMode(OutputConfiguration.MIRROR_MODE_NONE);
+        } else if (outputConfig.getMirrorMode() == MirrorMode.MIRROR_MODE_ON) {
+            outputConfiguration.setMirrorMode(OutputConfiguration.MIRROR_MODE_H);
+        }
+
         if (!outputConfig.getSharedSurfaces().isEmpty()) {
             outputConfiguration.enableSurfaceSharing();
             for (DeferrableSurface sharedDeferSurface : outputConfig.getSharedSurfaces()) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompat.java
index 9503797..a445d3f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompat.java
@@ -200,6 +200,24 @@
     }
 
     /**
+     * Returns mirror mode of {@link OutputConfiguration}.
+     * @return {@link OutputConfiguration#getMirrorMode()}
+     * @see OutputConfiguration#getMirrorMode()
+     */
+    public int getMirrorMode() {
+        return mImpl.getMirrorMode();
+    }
+
+    /**
+     * Sets mirror mode of {@link OutputConfiguration}.
+     * @param mirrorMode mirror mode to set for {@link OutputConfiguration}.
+     * @see OutputConfiguration#setMirrorMode(int)
+     */
+    public void setMirrorMode(int mirrorMode) {
+        mImpl.setMirrorMode(mirrorMode);
+    }
+
+    /**
      * Retrieve the physical camera ID set by {@link #setPhysicalCameraId(String)}.
      *
      */
@@ -488,6 +506,10 @@
     interface OutputConfigurationCompatImpl {
         void enableSurfaceSharing();
 
+        int getMirrorMode();
+
+        void setMirrorMode(int mirrorMode);
+
         @Nullable
         String getPhysicalCameraId();
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatApi33Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatApi33Impl.java
index a2d905b..bc8829f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatApi33Impl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatApi33Impl.java
@@ -48,6 +48,16 @@
     }
 
     @Override
+    public int getMirrorMode() {
+        return ((OutputConfiguration) getOutputConfiguration()).getMirrorMode();
+    }
+
+    @Override
+    public void setMirrorMode(int mirrorMode) {
+        ((OutputConfiguration) getOutputConfiguration()).setMirrorMode(mirrorMode);
+    }
+
+    @Override
     public long getDynamicRangeProfile() {
         return ((OutputConfiguration) getOutputConfiguration()).getDynamicRangeProfile();
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatBaseImpl.java
index ef70b78..07ee1df 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatBaseImpl.java
@@ -19,6 +19,7 @@
 import android.annotation.SuppressLint;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.params.DynamicRangeProfiles;
+import android.hardware.camera2.params.OutputConfiguration;
 import android.os.Build;
 import android.util.Size;
 import android.view.Surface;
@@ -82,6 +83,16 @@
         return ((OutputConfigurationParamsApi21) mObject).mPhysicalCameraId;
     }
 
+    @Override
+    public void setMirrorMode(int mirrorMode) {
+        //No-op
+    }
+
+    @Override
+    public int getMirrorMode() {
+        return OutputConfiguration.MIRROR_MODE_AUTO;
+    }
+
     /**
      * Set stream use case for this OutputConfiguration.
      */
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatTest.java
index 7b2ad2a..4dca498 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/OutputConfigurationCompatTest.java
@@ -16,6 +16,9 @@
 
 package androidx.camera.camera2.internal.compat.params;
 
+import static android.hardware.camera2.params.OutputConfiguration.MIRROR_MODE_H;
+import static android.hardware.camera2.params.OutputConfiguration.MIRROR_MODE_NONE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
@@ -240,6 +243,22 @@
     }
 
     @Test
+    @Config(minSdk = 33)
+    public void canSetMirrorMode() {
+        OutputConfiguration outputConfig = mock(OutputConfiguration.class);
+
+        OutputConfigurationCompat outputConfigCompat = OutputConfigurationCompat.wrap(outputConfig);
+
+        outputConfigCompat.setMirrorMode(MIRROR_MODE_NONE);
+
+        verify(outputConfig, times(1)).setMirrorMode(MIRROR_MODE_NONE);
+
+        outputConfigCompat.setMirrorMode(MIRROR_MODE_H);
+
+        verify(outputConfig, times(1)).setMirrorMode(MIRROR_MODE_H);
+    }
+
+    @Test
     public void canSetDynamicRangeProfile() {
         OutputConfigurationCompat outputConfigCompat =
                 new OutputConfigurationCompat(mock(Surface.class));
diff --git a/camera/camera-core/api/1.4.0-beta01.txt b/camera/camera-core/api/1.4.0-beta01.txt
index c84401f..058ffa8 100644
--- a/camera/camera-core/api/1.4.0-beta01.txt
+++ b/camera/camera-core/api/1.4.0-beta01.txt
@@ -212,6 +212,9 @@
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalMirrorMode {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
   }
 
@@ -507,6 +510,7 @@
     ctor public Preview.Builder();
     method public androidx.camera.core.Preview build();
     method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalMirrorMode public androidx.camera.core.Preview.Builder setMirrorMode(int);
     method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
     method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
     method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index c84401f..058ffa8 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -212,6 +212,9 @@
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalMirrorMode {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
   }
 
@@ -507,6 +510,7 @@
     ctor public Preview.Builder();
     method public androidx.camera.core.Preview build();
     method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalMirrorMode public androidx.camera.core.Preview.Builder setMirrorMode(int);
     method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
     method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
     method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
diff --git a/camera/camera-core/api/restricted_1.4.0-beta01.txt b/camera/camera-core/api/restricted_1.4.0-beta01.txt
index c84401f..058ffa8 100644
--- a/camera/camera-core/api/restricted_1.4.0-beta01.txt
+++ b/camera/camera-core/api/restricted_1.4.0-beta01.txt
@@ -212,6 +212,9 @@
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalMirrorMode {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
   }
 
@@ -507,6 +510,7 @@
     ctor public Preview.Builder();
     method public androidx.camera.core.Preview build();
     method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalMirrorMode public androidx.camera.core.Preview.Builder setMirrorMode(int);
     method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
     method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
     method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index c84401f..058ffa8 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -212,6 +212,9 @@
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalMirrorMode {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
   }
 
@@ -507,6 +510,7 @@
     ctor public Preview.Builder();
     method public androidx.camera.core.Preview build();
     method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+    method @SuppressCompatibility @androidx.camera.core.ExperimentalMirrorMode public androidx.camera.core.Preview.Builder setMirrorMode(int);
     method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
     method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
     method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index f288885..e8a9867 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -25,6 +25,7 @@
 import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
+import androidx.camera.core.MirrorMode.MIRROR_MODE_UNSPECIFIED
 import androidx.camera.core.UseCase.snapToSurfaceRotation
 import androidx.camera.core.concurrent.CameraCoordinator
 import androidx.camera.core.impl.Config
@@ -265,7 +266,7 @@
     @Test
     fun defaultMirrorModeIsOff() {
         val fakeUseCase = createFakeUseCase()
-        assertThat(fakeUseCase.mirrorModeInternal).isEqualTo(MIRROR_MODE_OFF)
+        assertThat(fakeUseCase.mirrorModeInternal).isEqualTo(MIRROR_MODE_UNSPECIFIED)
     }
 
     @Test
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalMirrorMode.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalMirrorMode.java
new file mode 100644
index 0000000..5e841b7
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalMirrorMode.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.RequiresOptIn;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Denotes that the annotated API is designed to be experimental for {@link MirrorMode}
+ */
+@Retention(CLASS)
+@RequiresOptIn
+public @interface ExperimentalMirrorMode {
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 798ba62..2cf9086 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -423,7 +423,10 @@
 
         sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
 
-        sessionConfigBuilder.addSurface(mDeferrableSurface, streamSpec.getDynamicRange(), null);
+        sessionConfigBuilder.addSurface(mDeferrableSurface,
+                streamSpec.getDynamicRange(),
+                null,
+                MirrorMode.MIRROR_MODE_UNSPECIFIED);
 
         sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
             clearPipeline();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
index 49af138..cb2e328 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
@@ -42,12 +42,16 @@
      */
     public static final int MIRROR_MODE_ON_FRONT_ONLY = 2;
 
+    /** The mirror mode is not specified by the user **/
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final int MIRROR_MODE_UNSPECIFIED = -1;
+
     private MirrorMode() {
     }
 
     /**
      */
-    @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_ON_FRONT_ONLY})
+    @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_ON_FRONT_ONLY, MIRROR_MODE_UNSPECIFIED})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public @interface Mirror {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 2163ecb..04f330d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.CameraEffect.PREVIEW;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_UNSPECIFIED;
 import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
@@ -54,6 +55,7 @@
 import android.graphics.SurfaceTexture;
 import android.media.ImageReader;
 import android.media.MediaCodec;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
@@ -332,7 +334,8 @@
         if (mSurfaceProvider != null) {
             sessionConfigBuilder.addSurface(mSessionDeferrableSurface,
                     streamSpec.getDynamicRange(),
-                    getPhysicalCameraId());
+                    getPhysicalCameraId(),
+                    getMirrorModeInternal());
         }
 
         sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
@@ -846,7 +849,11 @@
 
             setCaptureType(UseCaseConfigFactory.CaptureType.PREVIEW);
             setTargetClass(Preview.class);
-            mutableConfig.insertOption(OPTION_MIRROR_MODE, Defaults.DEFAULT_MIRROR_MODE);
+
+            if (mutableConfig.retrieveOption(
+                    OPTION_MIRROR_MODE, MIRROR_MODE_UNSPECIFIED) == MIRROR_MODE_UNSPECIFIED) {
+                mutableConfig.insertOption(OPTION_MIRROR_MODE, Defaults.DEFAULT_MIRROR_MODE);
+            }
         }
 
         /**
@@ -1024,13 +1031,30 @@
         }
 
         /**
-         * setMirrorMode is not supported on Preview.
+         * Sets the mirror mode.
+         *
+         * <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
+         * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY}.
+         * If not set, it defaults to {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY}.
+         *
+         * <p>For API 33 and above, it will change the mirroring behavior for Preview use case.
+         * It is calling
+         * {@link android.hardware.camera2.params.OutputConfiguration#setMirrorMode(int)}.
+         *
+         * <p> For API 32 and below, it will be no-op.
+         *
+         * @param mirrorMode The mirror mode of the intended target.
+         * @return The current Builder.
+         * @see android.hardware.camera2.params.OutputConfiguration#setMirrorMode(int)
          */
-        @RestrictTo(Scope.LIBRARY_GROUP)
+        @ExperimentalMirrorMode
         @NonNull
         @Override
         public Builder setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
-            throw new UnsupportedOperationException("setMirrorMode is not supported.");
+            if (Build.VERSION.SDK_INT >= 33) {
+                getMutableConfig().insertOption(OPTION_MIRROR_MODE, mirrorMode);
+            }
+            return this;
         }
 
         /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index eb703aa..5918040 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -19,6 +19,7 @@
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_UNSPECIFIED;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
@@ -442,7 +443,7 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @MirrorMode.Mirror
     protected int getMirrorModeInternal() {
-        return ((ImageOutputConfig) mCurrentConfig).getMirrorMode(MIRROR_MODE_OFF);
+        return ((ImageOutputConfig) mCurrentConfig).getMirrorMode(MIRROR_MODE_UNSPECIFIED);
     }
 
     /**
@@ -453,6 +454,7 @@
     public boolean isMirroringRequired(@NonNull CameraInternal camera) {
         int mirrorMode = getMirrorModeInternal();
         switch (mirrorMode) {
+            case MIRROR_MODE_UNSPECIFIED:
             case MIRROR_MODE_OFF:
                 return false;
             case MIRROR_MODE_ON:
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
index 414ddb0..6ea58a0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
@@ -30,6 +30,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.DynamicRange;
 import androidx.camera.core.Logger;
+import androidx.camera.core.MirrorMode;
 import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.compat.workaround.SurfaceSorter;
 
@@ -109,6 +110,14 @@
         public abstract String getPhysicalCameraId();
 
         /**
+         * Returns the mirror mode.
+         *
+         * @return {@link MirrorMode}
+         */
+        @MirrorMode.Mirror
+        public abstract int getMirrorMode();
+
+        /**
          * Returns the surface group ID. Default value is {@link #SURFACE_GROUP_ID_NONE} meaning
          * it doesn't belong to any surface group. A surface group ID is used to identify which
          * surface group this output surface belongs to. Output streams with the same
@@ -137,6 +146,7 @@
                     .setSurface(surface)
                     .setSharedSurfaces(Collections.emptyList())
                     .setPhysicalCameraId(null)
+                    .setMirrorMode(MirrorMode.MIRROR_MODE_UNSPECIFIED)
                     .setSurfaceGroupId(SURFACE_GROUP_ID_NONE)
                     .setDynamicRange(DynamicRange.SDR);
         }
@@ -168,6 +178,14 @@
             public abstract Builder setPhysicalCameraId(@Nullable String cameraId);
 
             /**
+             * Sets the mirror mode. It specifies mirroring mode for
+             * {@link android.hardware.camera2.params.OutputConfiguration}.
+             * @see android.hardware.camera2.params.OutputConfiguration#setMirrorMode(int)
+             */
+            @NonNull
+            public abstract Builder setMirrorMode(@MirrorMode.Mirror int mirrorMode);
+
+            /**
              * Sets the surface group ID. A surface group ID is used to identify which surface group
              * this output surface belongs to. Output streams with the same non-negative group ID
              * won't receive the camera output simultaneously therefore it could be used to reduce
@@ -643,11 +661,13 @@
          * Add a surface to the set that the session repeatedly writes data to.
          *
          * <p>The dynamic range of this surface will default to {@link DynamicRange#SDR}. To
-         * manually set the dynamic range, use {@link #addSurface(DeferrableSurface, DynamicRange)}.
+         * manually set the dynamic range, use
+         * {@link #addSurface(DeferrableSurface, DynamicRange, String, int)}.
          */
         @NonNull
         public Builder addSurface(@NonNull DeferrableSurface surface) {
-            return addSurface(surface, DynamicRange.SDR, null);
+            return addSurface(surface, DynamicRange.SDR, null,
+                    MirrorMode.MIRROR_MODE_UNSPECIFIED);
         }
 
         /**
@@ -657,10 +677,12 @@
         @NonNull
         public Builder addSurface(@NonNull DeferrableSurface surface,
                 @NonNull DynamicRange dynamicRange,
-                @Nullable String physicalCameraId) {
+                @Nullable String physicalCameraId,
+                @MirrorMode.Mirror int mirrorMode) {
             OutputConfig outputConfig = OutputConfig.builder(surface)
                     .setPhysicalCameraId(physicalCameraId)
                     .setDynamicRange(dynamicRange)
+                    .setMirrorMode(mirrorMode)
                     .build();
             mOutputConfigs.add(outputConfig);
             mCaptureConfigBuilder.addSurface(surface);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index d690404..b98833a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -45,6 +45,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.CameraEffect;
 import androidx.camera.core.ImageCapture;
+import androidx.camera.core.MirrorMode;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
@@ -277,7 +278,10 @@
 
         propagateChildrenCamera2Interop(streamSpec.getResolution(), builder);
 
-        builder.addSurface(mCameraEdge.getDeferrableSurface(), streamSpec.getDynamicRange(), null);
+        builder.addSurface(mCameraEdge.getDeferrableSurface(),
+                streamSpec.getDynamicRange(),
+                null,
+                MirrorMode.MIRROR_MODE_UNSPECIFIED);
         builder.addRepeatingCameraCaptureCallback(
                 mVirtualCameraAdapter.getParentMetadataCallback());
         if (streamSpec.getImplementationOptions() != null) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 0469f62..380be3b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -16,8 +16,8 @@
 
 package androidx.camera.core;
 
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_UNSPECIFIED;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -172,7 +172,7 @@
     @Test
     public void defaultMirrorModeIsOff() {
         ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
-        assertThat(imageAnalysis.getMirrorModeInternal()).isEqualTo(MIRROR_MODE_OFF);
+        assertThat(imageAnalysis.getMirrorModeInternal()).isEqualTo(MIRROR_MODE_UNSPECIFIED);
     }
 
     @Test(expected = UnsupportedOperationException.class)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index bc1b60a..affae0b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -33,8 +33,8 @@
 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG
 import androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR
-import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
+import androidx.camera.core.MirrorMode.MIRROR_MODE_UNSPECIFIED
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.ImageCaptureConfig
@@ -211,7 +211,7 @@
     @Test
     fun defaultMirrorModeIsOff() {
         val imageCapture = ImageCapture.Builder().build()
-        assertThat(imageCapture.mirrorModeInternal).isEqualTo(MIRROR_MODE_OFF)
+        assertThat(imageCapture.mirrorModeInternal).isEqualTo(MIRROR_MODE_UNSPECIFIED)
     }
 
     @Test(expected = UnsupportedOperationException::class)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 7ab1e17..f4e6f6b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -34,6 +34,8 @@
 import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
+import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
 import androidx.camera.core.Preview.SurfaceProvider
 import androidx.camera.core.SurfaceRequest.TransformationInfo
@@ -279,9 +281,34 @@
         assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
     }
 
-    @Test(expected = UnsupportedOperationException::class)
-    fun setMirrorMode_throwException() {
-        Preview.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
+    @Config(minSdk = 33)
+    @Test
+    fun setMirrorMode_OnFrontOnly() {
+        val preview = createPreview()
+        assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
+
+        val sessionConfig = preview.sessionConfig
+        assertThat(sessionConfig.outputConfigs[0].mirrorMode).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
+    }
+
+    @Config(minSdk = 33)
+    @Test
+    fun setMirrorMode_On() {
+        val preview = createPreview(mirrorMode = MIRROR_MODE_ON)
+        assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_ON)
+
+        val sessionConfig = preview.sessionConfig
+        assertThat(sessionConfig.outputConfigs[0].mirrorMode).isEqualTo(MIRROR_MODE_ON)
+    }
+
+    @Config(minSdk = 33)
+    @Test
+    fun setMirrorMode_Off() {
+        val preview = createPreview(mirrorMode = MIRROR_MODE_OFF)
+        assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_OFF)
+
+        val sessionConfig = preview.sessionConfig
+        assertThat(sessionConfig.outputConfigs[0].mirrorMode).isEqualTo(MIRROR_MODE_OFF)
     }
 
     @Test
@@ -833,9 +860,11 @@
         camera: FakeCamera = backCamera,
         targetRotation: Int = ROTATION_90,
         surfaceProvider: SurfaceProvider = SurfaceProvider {
-        }
+        },
+        mirrorMode: Int = MirrorMode.MIRROR_MODE_UNSPECIFIED
     ): Preview {
         previewToDetach = Preview.Builder()
+            .setMirrorMode(mirrorMode)
             .setTargetRotation(targetRotation)
             .build()
         previewToDetach.effect = effect
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 95971f2..dcdc96a 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -356,7 +356,11 @@
      */
     @MirrorMode.Mirror
     public int getMirrorMode() {
-        return getMirrorModeInternal();
+        int mirrorMode = getMirrorModeInternal();
+        if (mirrorMode == MirrorMode.MIRROR_MODE_UNSPECIFIED) {
+            return MirrorMode.MIRROR_MODE_OFF;
+        }
+        return mirrorMode;
     }
 
     @SuppressWarnings("unchecked")
@@ -903,7 +907,10 @@
         DynamicRange dynamicRange = streamSpec.getDynamicRange();
         if (!isStreamError && mDeferrableSurface != null) {
             if (isStreamActive) {
-                sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange, null);
+                sessionConfigBuilder.addSurface(mDeferrableSurface,
+                        dynamicRange,
+                        null,
+                        MirrorMode.MIRROR_MODE_UNSPECIFIED);
             } else {
                 sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface, dynamicRange);
             }
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
index e64e886..5421e2f 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
@@ -46,8 +46,10 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ConcurrentCamera;
 import androidx.camera.core.ConcurrentCamera.SingleCameraConfig;
+import androidx.camera.core.ExperimentalMirrorMode;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.MeteringPoint;
+import androidx.camera.core.MirrorMode;
 import androidx.camera.core.Preview;
 import androidx.camera.core.UseCaseGroup;
 import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration;
@@ -274,7 +276,7 @@
     }
 
     @SuppressLint("NullAnnotationGroup")
-    @OptIn(markerClass = {ExperimentalCamera2Interop.class,
+    @OptIn(markerClass = {ExperimentalCamera2Interop.class, ExperimentalMirrorMode.class,
             androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class})
     private void bindToLifecycleForConcurrentCamera(
             @NonNull ProcessCameraProvider cameraProvider,
@@ -329,6 +331,7 @@
                             .build(),
                     lifecycleOwner);
             Preview previewBack = new Preview.Builder()
+                    .setMirrorMode(MirrorMode.MIRROR_MODE_OFF)
                     .build();
             previewBack.setSurfaceProvider(backPreviewView.getSurfaceProvider());
             SingleCameraConfig secondary = new SingleCameraConfig(