Merge changes I7bc72709,I41b2b909,I38f520c7,I5bd6ae14,Ic80cd9e0 into androidx-main

* changes:
  Disable flash in Camera2CapturePipeline when low-light boost is on
  Implement CameraInfo#isLowLightBoostSupported and getLowLightBoostState API in camera-camera2
  Implement CameraControl#enableLowLightBoost API in camera-camera2
  Add Low Light Boost related API in CameraControl and CameraInfo interfaces
  Upgrade compileSdk as 35 for LLB feature development
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index bb710ed..eccd134 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -85,6 +85,8 @@
 }
 
 android {
+    compileSdk 35
+
     lintOptions {
         enable("CameraXQuirksClassDetector")
     }
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index cf174ef..45bc114 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -81,6 +81,8 @@
     androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
 }
 android {
+    compileSdk 35
+
     buildTypes.configureEach {
         consumerProguardFiles("proguard-rules.pro")
     }
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 d43e3f6..96d36ea 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
@@ -21,6 +21,7 @@
 import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
 import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
 import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_EXTERNAL_FLASH;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
 import static android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_AUTO;
 import static android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE;
 import static android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_OFF;
@@ -397,20 +398,8 @@
     public void enableTorch_aeModeSetAndRequestUpdated() throws InterruptedException {
         assumeTrue(mHasFlashUnit);
         mCamera2CameraControlImpl.enableTorch(true);
-
         HandlerUtil.waitForLooperToIdle(mHandler);
-
-        verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
-        SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
-        Camera2ImplConfig camera2Config = new Camera2ImplConfig(
-                sessionConfig.getImplementationOptions());
-
-        assertAeMode(camera2Config, CONTROL_AE_MODE_ON);
-
-        assertThat(
-                camera2Config.getCaptureRequestOption(
-                        CaptureRequest.FLASH_MODE, FLASH_MODE_OFF))
-                .isEqualTo(FLASH_MODE_TORCH);
+        verifyControlAeModeAndFlashMode(CONTROL_AE_MODE_ON, FLASH_MODE_TORCH);
     }
 
     @Test
@@ -442,6 +431,47 @@
 
     }
 
+    @SdkSuppress(minSdkVersion = 35)
+    @Test
+    public void enableLowLightBoost_aeModeSetAndRequestUpdated() throws InterruptedException {
+        assumeTrue(mCamera2CameraControlImpl.getLowLightBoostControl().isLowLightBoostSupported());
+        mCamera2CameraControlImpl.enableLowLightBoostAsync(true);
+        HandlerUtil.waitForLooperToIdle(mHandler);
+        verifyControlAeModeAndFlashMode(CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY,
+                FLASH_MODE_OFF);
+    }
+
+    @SdkSuppress(minSdkVersion = 35)
+    @Test
+    public void enableLowLightBoostCanOverrideTorch_aeModeSetAndRequestUpdated()
+            throws InterruptedException {
+        assumeTrue(mCamera2CameraControlImpl.getLowLightBoostControl().isLowLightBoostSupported());
+        assumeTrue(mHasFlashUnit);
+
+        mCamera2CameraControlImpl.enableTorch(true);
+        HandlerUtil.waitForLooperToIdle(mHandler);
+        verifyControlAeModeAndFlashMode(CONTROL_AE_MODE_ON, FLASH_MODE_TORCH);
+
+        mCamera2CameraControlImpl.enableTorch(true);
+        HandlerUtil.waitForLooperToIdle(mHandler);
+        verifyControlAeModeAndFlashMode(CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY,
+                FLASH_MODE_OFF);
+    }
+
+    private void verifyControlAeModeAndFlashMode(int expectedAeMode, int expectedFlashMode) {
+        verify(mControlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
+        SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
+        Camera2ImplConfig camera2Config = new Camera2ImplConfig(
+                sessionConfig.getImplementationOptions());
+
+        assertAeMode(camera2Config, expectedAeMode);
+
+        assertThat(
+                camera2Config.getCaptureRequestOption(
+                        CaptureRequest.FLASH_MODE, FLASH_MODE_OFF))
+                .isEqualTo(expectedFlashMode);
+    }
+
     @Test
     @LargeTest
     public void triggerAf_futureSucceeds() throws Exception {
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/LowLightBoostControlDeviceTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/LowLightBoostControlDeviceTest.kt
new file mode 100644
index 0000000..b0a8f1d
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/LowLightBoostControlDeviceTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.camera2.internal
+
+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.Preview
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.impl.CameraXUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val DEFAULT_CAMERA_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 35)
+class LowLightBoostControlDeviceTest {
+
+    @get:Rule
+    val cameraRule =
+        CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+            PreTestCameraIdList(Camera2Config.defaultConfig())
+        )
+
+    val context = ApplicationProvider.getApplicationContext() as Context
+
+    private lateinit var camera: CameraUseCaseAdapter
+
+    @Before
+    fun setUp() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_CAMERA_SELECTOR.lensFacing!!))
+        // Init CameraX
+        val config = Camera2Config.defaultConfig()
+        CameraXUtil.initialize(context, config)
+
+        // Prepare LowLightBoostControl
+        // To get a functional Camera2CameraControlImpl, it needs to bind an active UseCase and the
+        // UseCase must have repeating surface. Create and bind Preview as repeating surface.
+        camera =
+            CameraUtil.createCameraAndAttachUseCase(
+                context,
+                DEFAULT_CAMERA_SELECTOR,
+                Preview.Builder().build()
+            )
+    }
+
+    @After
+    fun tearDown() {
+        if (::camera.isInitialized) {
+            InstrumentationRegistry.getInstrumentation().runOnMainSync {
+                // TODO: The removeUseCases() call might be removed after clarifying the
+                // abortCaptures() issue in b/162314023.
+                camera.removeUseCases(camera.getUseCases())
+            }
+        }
+
+        CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
+    }
+
+    @Test
+    fun enableDisableLowLightBoost_futureWillCompleteSuccessfully_whenLlbIsSupported() {
+        assumeTrue(camera.cameraInfo.isLowLightBoostSupported)
+        TestUtil.getCamera2CameraControlImpl(camera.cameraControl).lowLightBoostControl.apply {
+            // Future should return with no exception
+            enableLowLightBoost(true).get(3, TimeUnit.SECONDS)
+
+            // Future should return with no exception
+            enableLowLightBoost(false).get(3, TimeUnit.SECONDS)
+        }
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 2b9b8a2..ef38ab5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.camera2.internal;
 
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
+
 import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
@@ -124,6 +126,7 @@
     private final FocusMeteringControl mFocusMeteringControl;
     private final ZoomControl mZoomControl;
     private final TorchControl mTorchControl;
+    private final LowLightBoostControl mLowLightBoostControl;
     private final ExposureControl mExposureControl;
     @VisibleForTesting
     ZslControl mZslControl;
@@ -137,6 +140,7 @@
 
     // use volatile modifier to make these variables in sync in all threads.
     private volatile boolean mIsTorchOn = false;
+    private volatile boolean mIsLowLightBoostOn = false;
     @ImageCapture.FlashMode
     private volatile int mFlashMode = FLASH_MODE_OFF;
 
@@ -201,6 +205,7 @@
                 this, scheduler, mExecutor, cameraQuirks);
         mZoomControl = new ZoomControl(this, mCameraCharacteristics, mExecutor);
         mTorchControl = new TorchControl(this, mCameraCharacteristics, mExecutor);
+        mLowLightBoostControl = new LowLightBoostControl(this, mCameraCharacteristics, mExecutor);
         if (Build.VERSION.SDK_INT >= 23) {
             mZslControl = new ZslControlImpl(mCameraCharacteristics, mExecutor);
         } else {
@@ -262,6 +267,10 @@
         return mTorchControl;
     }
 
+    public @NonNull LowLightBoostControl getLowLightBoostControl() {
+        return mLowLightBoostControl;
+    }
+
     public @NonNull ExposureControl getExposureControl() {
         return mExposureControl;
     }
@@ -307,6 +316,7 @@
         Logger.d(TAG, "setActive: isActive = " + isActive);
         mFocusMeteringControl.setActive(isActive);
         mZoomControl.setActive(isActive);
+        mLowLightBoostControl.setActive(isActive);
         mTorchControl.setActive(isActive);
         mExposureControl.setActive(isActive);
         mCamera2CameraControl.setActive(isActive);
@@ -424,6 +434,17 @@
         return Futures.nonCancellationPropagating(mTorchControl.enableTorch(torch));
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public @NonNull ListenableFuture<Void> enableLowLightBoostAsync(final boolean lowLightBoost) {
+        if (!isControlInUse()) {
+            return Futures.immediateFailedFuture(
+                    new OperationCanceledException("Camera is not active."));
+        }
+        return Futures.nonCancellationPropagating(
+                mLowLightBoostControl.enableLowLightBoost(lowLightBoost));
+    }
+
     @ExecutedBy("mExecutor")
     private @NonNull ListenableFuture<Void> waitForSessionUpdateId(long sessionUpdateIdToWait) {
         return CallbackToFutureAdapter.getFuture(completer -> {
@@ -623,30 +644,66 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mExecutor")
     void enableTorchInternal(boolean torch) {
+        // When low-light boost is on, any torch related operations will be ignored.
+        if (mIsLowLightBoostOn) {
+            return;
+        }
+
         mIsTorchOn = torch;
         if (!torch) {
-            // Send capture request with AE_MODE_ON + FLASH_MODE_OFF to turn off torch.
-            CaptureConfig.Builder singleRequestBuilder = new CaptureConfig.Builder();
-            singleRequestBuilder.setTemplateType(mTemplate);
-            singleRequestBuilder.setUseRepeatingSurface(true);
-            Camera2ImplConfig.Builder configBuilder = new Camera2ImplConfig.Builder();
-            configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
-                    getSupportedAeMode(CaptureRequest.CONTROL_AE_MODE_ON));
-            configBuilder.setCaptureRequestOption(CaptureRequest.FLASH_MODE,
-                    CaptureRequest.FLASH_MODE_OFF);
-            singleRequestBuilder.addImplementationOptions(configBuilder.build());
-            submitCaptureRequestsInternal(
-                    Collections.singletonList(singleRequestBuilder.build()));
+            // On some devices, needs to reset the AE/flash state to ensure that the torch can be
+            // turned off.
+            resetAeFlashState();
         }
         updateSessionConfigSynchronous();
     }
 
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mExecutor")
+    void enableLowLightBoostInternal(boolean lowLightBoost) {
+        if (mIsLowLightBoostOn == lowLightBoost) {
+            return;
+        }
+
+        // Forces turn off torch before enabling low-light boost.
+        if (lowLightBoost && mIsTorchOn) {
+            // On some devices, needs to reset the AE/flash state to ensure that the torch can be
+            // turned off.
+            resetAeFlashState();
+            mIsTorchOn = false;
+            mTorchControl.forceUpdateTorchStateToOff();
+        }
+
+        mIsLowLightBoostOn = lowLightBoost;
+        updateSessionConfigSynchronous();
+    }
+
+    private void resetAeFlashState() {
+        // Send capture request with AE_MODE_ON + FLASH_MODE_OFF to reset the AE/flash state.
+        CaptureConfig.Builder singleRequestBuilder = new CaptureConfig.Builder();
+        singleRequestBuilder.setTemplateType(mTemplate);
+        singleRequestBuilder.setUseRepeatingSurface(true);
+        Camera2ImplConfig.Builder configBuilder = new Camera2ImplConfig.Builder();
+        configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
+                getSupportedAeMode(CaptureRequest.CONTROL_AE_MODE_ON));
+        configBuilder.setCaptureRequestOption(CaptureRequest.FLASH_MODE,
+                CaptureRequest.FLASH_MODE_OFF);
+        singleRequestBuilder.addImplementationOptions(configBuilder.build());
+        submitCaptureRequestsInternal(
+                Collections.singletonList(singleRequestBuilder.build()));
+    }
+
     @ExecutedBy("mExecutor")
     boolean isTorchOn() {
         return mIsTorchOn;
     }
 
     @ExecutedBy("mExecutor")
+    boolean isLowLightBoostOn() {
+        return mIsLowLightBoostOn;
+    }
+
+    @ExecutedBy("mExecutor")
     void submitCaptureRequestsInternal(final List<CaptureConfig> captureConfigs) {
         mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs);
     }
@@ -676,7 +733,9 @@
             aeMode = CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH;
         }
 
-        if (mIsTorchOn) {
+        if (mIsLowLightBoostOn) {
+            aeMode = CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
+        } else if (mIsTorchOn) {
             builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_MODE,
                     CaptureRequest.FLASH_MODE_TORCH, Config.OptionPriority.REQUIRED);
         } else {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 65ed945..81a5d90 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -110,6 +110,8 @@
     @GuardedBy("mLock")
     private @Nullable RedirectableLiveData<Integer> mRedirectTorchStateLiveData = null;
     @GuardedBy("mLock")
+    private @Nullable RedirectableLiveData<Integer> mRedirectLowLightBoostStateLiveData = null;
+    @GuardedBy("mLock")
     private @Nullable RedirectableLiveData<ZoomState> mRedirectZoomStateLiveData = null;
     private final @NonNull RedirectableLiveData<CameraState> mCameraStateLiveData;
     @GuardedBy("mLock")
@@ -159,6 +161,11 @@
                         mCamera2CameraControlImpl.getTorchControl().getTorchState());
             }
 
+            if (mRedirectLowLightBoostStateLiveData != null) {
+                mRedirectLowLightBoostStateLiveData.redirectTo(mCamera2CameraControlImpl
+                        .getLowLightBoostControl().getLowLightBoostState());
+            }
+
             if (mCameraCaptureCallbacks != null) {
                 for (Pair<CameraCaptureCallback, Executor> pair :
                         mCameraCaptureCallbacks) {
@@ -291,6 +298,31 @@
     }
 
     @Override
+    public boolean isLowLightBoostSupported() {
+        return LowLightBoostControl.checkLowLightBoostAvailability(mCameraCharacteristicsCompat);
+    }
+
+    @Override
+    public @NonNull LiveData<Integer> getLowLightBoostState() {
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mRedirectLowLightBoostStateLiveData == null) {
+                    mRedirectLowLightBoostStateLiveData =
+                            new RedirectableLiveData<>(LowLightBoostControl.DEFAULT_LLB_STATE);
+                }
+                return mRedirectLowLightBoostStateLiveData;
+            }
+
+            // if RedirectableLiveData exists,  use it directly.
+            if (mRedirectLowLightBoostStateLiveData != null) {
+                return mRedirectLowLightBoostStateLiveData;
+            }
+
+            return mCamera2CameraControlImpl.getLowLightBoostControl().getLowLightBoostState();
+        }
+    }
+
+    @Override
     public @NonNull LiveData<ZoomState> getZoomState() {
         synchronized (mLock) {
             if (mCamera2CameraControlImpl == null) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index d000354..42a38eb 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -326,7 +326,8 @@
                                 null) : Futures.immediateFuture(null);
 
                 preCapture = FutureChain.from(getResult).transformAsync(captureResult -> {
-                    if (isFlashRequired(flashMode, captureResult)) {
+                    if (!mCameraControl.isLowLightBoostOn() && isFlashRequired(flashMode,
+                            captureResult)) {
                         setTimeout3A(CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS);
                     }
                     return mPipelineSubTask.preCapture(captureResult);
@@ -625,7 +626,9 @@
             Logger.d(TAG, "TorchTask#preCapture: isFlashRequired = " + isFlashRequired);
 
             if (isFlashRequired(mFlashMode, captureResult)) {
-                if (mCameraControl.isTorchOn()) {
+                if (mCameraControl.isLowLightBoostOn()) {
+                    Logger.d(TAG, "Low-light boost already on, not turn on");
+                } else if (mCameraControl.isTorchOn()) {
                     Logger.d(TAG, "Torch already on, not turn on");
                 } else {
                     Logger.d(TAG, "Turn on torch");
@@ -694,7 +697,7 @@
         @Override
         public @NonNull ListenableFuture<Boolean> preCapture(
                 @Nullable TotalCaptureResult captureResult) {
-            if (isFlashRequired(mFlashMode, captureResult)) {
+            if (!mCameraControl.isLowLightBoostOn() && isFlashRequired(mFlashMode, captureResult)) {
                 Logger.d(TAG, "Trigger AE");
                 mIsExecuted = true;
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/LowLightBoostControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/LowLightBoostControl.java
new file mode 100644
index 0000000..78bcb35
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/LowLightBoostControl.java
@@ -0,0 +1,266 @@
+/*
+ * 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.camera2.internal;
+
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
+import static android.hardware.camera2.CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.annotation.CameraExecutor;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.Logger;
+import androidx.camera.core.LowLightBoostState;
+import androidx.camera.core.impl.annotation.ExecutedBy;
+import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of low-light boost control used within CameraControl and CameraInfo.
+ *
+ * It is used to control the low-light boost mode of camera device that
+ * {@link Camera2CameraControlImpl} operates. The low-light boost control must be activated via
+ * {@link #setActive(boolean)} when the camera device is ready to do low-light boost operations
+ * and be deactivated when the camera device is closing or closed.
+ */
+final class LowLightBoostControl {
+    private static final String TAG = "LowLightBoostControl";
+    static final int DEFAULT_LLB_STATE = LowLightBoostState.OFF;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    private final Camera2CameraControlImpl mCamera2CameraControlImpl;
+    private final MutableLiveData<Integer> mLowLightBoostState;
+    private final boolean mIsLowLightBoostSupported;
+    @CameraExecutor
+    private final Executor mExecutor;
+
+    private boolean mIsActive;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+            CallbackToFutureAdapter.Completer<Void> mEnableLlbCompleter;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+            boolean mTargetLlbEnabled;
+
+    @VisibleForTesting
+    final Camera2CameraControlImpl.CaptureResultListener mCaptureResultListener;
+
+    /**
+     * Constructs a LowLightBoostControl.
+     *
+     * @param camera2CameraControlImpl the camera control this LowLightBoostControl belongs.
+     * @param cameraCharacteristics    the characteristics for the camera being controlled.
+     * @param executor                 the camera executor used to run camera task.
+     */
+    LowLightBoostControl(@NonNull Camera2CameraControlImpl camera2CameraControlImpl,
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics,
+            @CameraExecutor @NonNull Executor executor) {
+        mCamera2CameraControlImpl = camera2CameraControlImpl;
+        mExecutor = executor;
+
+        mIsLowLightBoostSupported = checkLowLightBoostAvailability(cameraCharacteristics);
+        mLowLightBoostState = new MutableLiveData<>(DEFAULT_LLB_STATE);
+
+        mCaptureResultListener = captureResult -> {
+            if (mEnableLlbCompleter != null) {
+                CaptureRequest captureRequest = captureResult.getRequest();
+                Integer aeMode = captureRequest.get(CaptureRequest.CONTROL_AE_MODE);
+
+                // Skips the check if capture result doesn't contain AE mode related info.
+                if (aeMode == null) {
+                    return false;
+                }
+
+                // mTargetLlbEnabled might be either true or false.
+                // - When mTargetLlbEnabled is true: complete the completer when
+                // AE mode becomes CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY.
+                // - When mTargetLlbEnabled is false: complete the completer when
+                // AE mode becomes non-CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY.
+                boolean llbEnabled =
+                        aeMode == CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
+                if (llbEnabled == mTargetLlbEnabled) {
+                    mEnableLlbCompleter.set(null);
+                    mEnableLlbCompleter = null;
+                } else {
+                    return false;
+                }
+            }
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+                    && mTargetLlbEnabled) {
+                Integer currentState = captureResult.get(CONTROL_LOW_LIGHT_BOOST_STATE);
+                if (currentState != null) {
+                    setLiveDataValue(mLowLightBoostState, currentState);
+                }
+            }
+
+            // Return false to keep getting captureResult.
+            return false;
+        };
+
+        if (mIsLowLightBoostSupported) {
+            mCamera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
+        }
+    }
+
+    /**
+     * Set current active state. Set active if it is ready to do low-light boost operations.
+     *
+     * @param isActive true to activate or false otherwise.
+     */
+    @ExecutedBy("mExecutor")
+    void setActive(boolean isActive) {
+        if (mIsActive == isActive) {
+            return;
+        }
+
+        mIsActive = isActive;
+
+        if (!isActive) {
+            if (mTargetLlbEnabled) {
+                mTargetLlbEnabled = false;
+                mCamera2CameraControlImpl.enableLowLightBoostInternal(false);
+                setLiveDataValue(mLowLightBoostState, LowLightBoostState.OFF);
+            }
+
+            if (mEnableLlbCompleter != null) {
+                mEnableLlbCompleter.setException(
+                        new CameraControl.OperationCanceledException("Camera is not active."));
+                mEnableLlbCompleter = null;
+            }
+        }
+    }
+
+    /**
+     * Enable or disable the low-light boost.
+     *
+     * <p>The returned {@link ListenableFuture} will succeed when the request is sent to camera
+     * device. But it may get an {@link CameraControl.OperationCanceledException} result when:
+     * <ol>
+     * <li>There are multiple {@code enableLowLightBoost(boolean)} requests in the same time, the
+     * older and incomplete futures will get cancelled.
+     * <li>When the LowLightBoostControl is set to inactive.
+     * </ol>
+     *
+     * <p>The returned {@link ListenableFuture} will fail immediately when:
+     * <ol>
+     * <li>The LowLightBoostControl is not in active state.
+     * <li>The camera doesn't support low-light boost mode.
+     * </ol>
+     *
+     * @param enabled true to open the low-light boost, false to close it.
+     * @return A {@link ListenableFuture} which is successful when the low-light boost was changed
+     * to the value specified. It fails when it is unable to change the low-light boost state.
+     */
+    ListenableFuture<Void> enableLowLightBoost(boolean enabled) {
+        if (!mIsLowLightBoostSupported) {
+            Logger.d(TAG, "Unable to enable low-light boost due to it is not supported.");
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("Low-light boost is not supported"));
+        }
+
+        setLiveDataValue(mLowLightBoostState,
+                enabled ? LowLightBoostState.INACTIVE : LowLightBoostState.OFF);
+
+        return CallbackToFutureAdapter.getFuture(completer -> {
+            mExecutor.execute(() -> enableLowLightBoostInternal(completer, enabled));
+            return "enableLowLightBoost: " + enabled;
+        });
+    }
+
+    /**
+     * Returns a {@link LiveData} of current {@link LowLightBoostState}.
+     *
+     * <p>The low-light boost state can be enabled or disabled via
+     * {@link #enableLowLightBoost(boolean)} which will trigger the change event to the returned
+     * {@link LiveData}.
+     *
+     * @return a {@link LiveData} containing current low-light boost state.
+     */
+    @NonNull
+    LiveData<Integer> getLowLightBoostState() {
+        return mLowLightBoostState;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mExecutor")
+    void enableLowLightBoostInternal(@Nullable Completer<Void> completer, boolean enabled) {
+        if (!mIsActive) {
+            setLiveDataValue(mLowLightBoostState, LowLightBoostState.OFF);
+            if (completer != null) {
+                completer.setException(
+                        new CameraControl.OperationCanceledException("Camera is not active."));
+            }
+            return;
+        }
+
+        mTargetLlbEnabled = enabled;
+        mCamera2CameraControlImpl.enableLowLightBoostInternal(enabled);
+        setLiveDataValue(mLowLightBoostState,
+                enabled ? LowLightBoostState.INACTIVE : LowLightBoostState.OFF);
+        if (mEnableLlbCompleter != null) {
+            mEnableLlbCompleter.setException(new CameraControl.OperationCanceledException(
+                    "There is a new enableLowLightBoost being set"));
+        }
+        mEnableLlbCompleter = completer;
+    }
+
+    boolean isLowLightBoostSupported() {
+        return mIsLowLightBoostSupported;
+    }
+
+    static boolean checkLowLightBoostAvailability(
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics) {
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            return false;
+        }
+
+        int[] availableAeModes = cameraCharacteristics.get(
+                CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
+        if (availableAeModes != null) {
+            for (int availableAeMode : availableAeModes) {
+                if (availableAeMode
+                        == CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private <T> void setLiveDataValue(@NonNull MutableLiveData<T> liveData, T value) {
+        if (Threads.isMainThread()) {
+            liveData.setValue(value);
+        } else {
+            liveData.postValue(value);
+        }
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index c18d7b0..d4c634e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -61,9 +61,9 @@
 
     private boolean mIsActive;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    CallbackToFutureAdapter.Completer<Void> mEnableTorchCompleter;
+            CallbackToFutureAdapter.Completer<Void> mEnableTorchCompleter;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    boolean mTargetTorchEnabled;
+            boolean mTargetTorchEnabled;
 
     /**
      * Constructs a TorchControl.
@@ -193,6 +193,14 @@
             return;
         }
 
+        if (mCamera2CameraControlImpl.isLowLightBoostOn()) {
+            if (completer != null) {
+                completer.setException(new IllegalStateException(
+                        "Torch can not be enabled when low-light boost is on!"));
+            }
+            return;
+        }
+
         mTargetTorchEnabled = enabled;
         mCamera2CameraControlImpl.enableTorchInternal(enabled);
         setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
@@ -203,6 +211,23 @@
         mEnableTorchCompleter = completer;
     }
 
+    /**
+     * Force update the torch state to OFF.
+     *
+     * <p>This can be invoked when low-light boost is turned on. The torch state will also be
+     * updated as {@link TorchState#OFF}.
+     */
+    @ExecutedBy("mExecutor")
+    void forceUpdateTorchStateToOff() {
+        // Directly return if torch is originally off
+        if (!mTargetTorchEnabled) {
+            return;
+        }
+
+        mTargetTorchEnabled = false;
+        setLiveDataValue(mTorchState, TorchState.OFF);
+    }
+
     private <T> void setLiveDataValue(@NonNull MutableLiveData<T> liveData, T value) {
         if (Threads.isMainThread()) {
             liveData.setValue(value);
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 3228d2b..529c505 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -17,6 +17,7 @@
 package androidx.camera.camera2.internal;
 
 import static android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
 import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF;
 import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON;
 import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION;
@@ -62,6 +63,7 @@
 import androidx.camera.core.DynamicRange;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.LowLightBoostState;
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
 import androidx.camera.core.TorchState;
 import androidx.camera.core.ZoomState;
@@ -163,6 +165,7 @@
     private CameraManagerCompat mCameraManagerCompat;
     private ZoomControl mMockZoomControl;
     private TorchControl mMockTorchControl;
+    private LowLightBoostControl mMockLowLightBoostControl;
     private ExposureControl mExposureControl;
     private FocusMeteringControl mFocusMeteringControl;
     private Camera2CameraControlImpl mMockCameraControl;
@@ -254,6 +257,27 @@
         assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA1_FLASH_INFO_BOOLEAN);
     }
 
+    @Config(minSdk = 35)
+    @Test
+    public void cameraInfo_canReturnLowLightBoostSupported_forBackCamera()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ false);
+
+        CameraInfoInternal cameraInfoInternal =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+        assertThat(cameraInfoInternal.isLowLightBoostSupported()).isTrue();
+    }
+
+    @Test
+    public void cameraInfo_canReturnLowLightBoostSupported_forFrontCamera()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ false);
+
+        CameraInfoInternal cameraInfoInternal =
+                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+        assertThat(cameraInfoInternal.isLowLightBoostSupported()).isFalse();
+    }
+
     @Test
     public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState()
             throws CameraAccessExceptionCompat {
@@ -296,6 +320,46 @@
         assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
     }
 
+    @Config(minSdk = 35)
+    @Test
+    public void cameraInfoWithCameraControl_canReturnLowLightBoostState()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ false);
+
+        when(mMockLowLightBoostControl.getLowLightBoostState()).thenReturn(
+                new MutableLiveData<>(LowLightBoostState.ACTIVE));
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+        assertThat(camera2CameraInfoImpl.getLowLightBoostState().getValue()).isEqualTo(
+                LowLightBoostState.ACTIVE);
+    }
+
+    @Config(minSdk = 35)
+    @Test
+    public void lowLightBoostStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ false);
+
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+        // Calls getLowLightBoostState() to trigger RedirectableLiveData
+        LiveData<Integer> lowLightBoostStateLiveData =
+                camera2CameraInfoImpl.getLowLightBoostState();
+
+        when(mMockLowLightBoostControl.getLowLightBoostState()).thenReturn(
+                new MutableLiveData<>(LowLightBoostState.ACTIVE));
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        // LowLightBoostState LiveData instances are the same before and after the
+        // linkWithCameraControl.
+        assertThat(camera2CameraInfoImpl.getLowLightBoostState()).isSameInstanceAs(
+                lowLightBoostStateLiveData);
+        assertThat(camera2CameraInfoImpl.getLowLightBoostState().getValue()).isEqualTo(
+                LowLightBoostState.ACTIVE);
+    }
+
     // zoom related tests just ensure it uses ZoomControl to get the value
     // Full tests are performed at ZoomControlDeviceTest / ZoomControlTest.
     @Test
@@ -937,12 +1001,14 @@
 
         mMockZoomControl = mock(ZoomControl.class);
         mMockTorchControl = mock(TorchControl.class);
+        mMockLowLightBoostControl = mock(LowLightBoostControl.class);
         mExposureControl = mock(ExposureControl.class);
         mMockCameraControl = mock(Camera2CameraControlImpl.class);
         mFocusMeteringControl = mock(FocusMeteringControl.class);
 
         when(mMockCameraControl.getZoomControl()).thenReturn(mMockZoomControl);
         when(mMockCameraControl.getTorchControl()).thenReturn(mMockTorchControl);
+        when(mMockCameraControl.getLowLightBoostControl()).thenReturn(mMockLowLightBoostControl);
         when(mMockCameraControl.getExposureControl()).thenReturn(mExposureControl);
         when(mMockCameraControl.getFocusMeteringControl()).thenReturn(mFocusMeteringControl);
     }
@@ -1022,6 +1088,16 @@
                     });
         }
 
+        // Adds low-light boost support for the back camera since API 35
+        if (Build.VERSION.SDK_INT >= 35) {
+            shadowCharacteristics0.set(
+                    CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+                    new int[]{
+                            CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
+                    }
+            );
+        }
+
         // Mock the request capability
         if (hasAvailableCapabilities) {
             shadowCharacteristics0.set(REQUEST_AVAILABLE_CAPABILITIES,
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
index a9e2c9d..0a8d0c7 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -23,13 +23,13 @@
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.TotalCaptureResult
 import android.media.Image
 import android.media.ImageWriter
 import android.os.Build
-import android.os.Looper
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.impl.Camera2ImplConfig
@@ -79,15 +79,14 @@
 import kotlinx.coroutines.withTimeout
 import org.junit.After
 import org.junit.Assert.assertThrows
-import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows
+import org.robolectric.ParameterizedRobolectricTestRunner
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadow.api.Shadow
@@ -96,12 +95,12 @@
 
 private const val CAMERA_ID_0 = "0"
 
-@RunWith(RobolectricTestRunner::class)
+@RunWith(ParameterizedRobolectricTestRunner::class)
 @DoNotInstrument
 @Config(
     minSdk = Build.VERSION_CODES.LOLLIPOP,
 )
-class Camera2CapturePipelineTest {
+class Camera2CapturePipelineTest(private val isLowLightBoostEnabled: Boolean) {
 
     private val context = ApplicationProvider.getApplicationContext() as Context
     private val executorService = Executors.newSingleThreadScheduledExecutor()
@@ -151,8 +150,17 @@
 
     private lateinit var testScreenFlash: MockScreenFlash
 
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "isLowLightBoostEnabled: {0}")
+        fun data() = listOf(true, false)
+    }
+
     @Before
     fun setUp() {
+        if (isLowLightBoostEnabled) {
+            assumeTrue(Build.VERSION.SDK_INT >= 35)
+        }
         initCameras()
         testScreenFlash = MockScreenFlash()
     }
@@ -212,8 +220,8 @@
         )
 
         // Assert.
-        assertTrue(fakeTask.preCaptureCountDown.await(3, TimeUnit.SECONDS))
-        assertTrue(fakeTask.postCaptureCountDown.await(3, TimeUnit.SECONDS))
+        assertThat(fakeTask.preCaptureCountDown.await(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(fakeTask.postCaptureCountDown.await(3, TimeUnit.SECONDS)).isTrue()
     }
 
     @Test
@@ -288,12 +296,19 @@
                 simulateRepeatingResult(initialDelay = 100)
             }
 
-        // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered
+        // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered when low-light boost is
+        // off, otherwise, is not triggered.
         immediateCompleteCapture.verifyRequestResult {
             it.requestContains(
                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
-            )
+            ) != isLowLightBoostEnabled
+        }
+
+        // When low-light boost is on, AE pre-capture is not triggered. Therefore, converged stage
+        // is not required.
+        if (isLowLightBoostEnabled) {
+            return
         }
 
         // Switch the repeating result to 3A converged state.
@@ -302,7 +317,8 @@
             resultParameters = resultConverged
         )
 
-        // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally.
+        // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally when low-light
+        // boost is off, otherwise, TRIGGER_CANCEL is not triggered.
         if (Build.VERSION.SDK_INT >= 23) {
             immediateCompleteCapture.verifyRequestResult {
                 it.requestContains(
@@ -345,12 +361,19 @@
                 )
             }
 
-        // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered
+        // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered when low-light boost is
+        // off, otherwise, is not triggered.
         immediateCompleteCapture.verifyRequestResult {
             it.requestContains(
                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
-            )
+            ) != isLowLightBoostEnabled
+        }
+
+        // When low-light boost is on, AE pre-capture is not triggered. Therefore, converged stage
+        // is not required.
+        if (isLowLightBoostEnabled) {
+            return
         }
 
         // Switch the repeating result to 3A converged state.
@@ -359,7 +382,8 @@
             resultParameters = resultConverged
         )
 
-        // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally.
+        // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally when low-light
+        // boost is off, otherwise, TRIGGER_CANCEL is not triggered.
         if (Build.VERSION.SDK_INT >= 23) {
             immediateCompleteCapture.verifyRequestResult {
                 it.requestContains(
@@ -430,7 +454,19 @@
                 }
 
         // Assert 1 torch should be turned on
-        cameraControl.waitForSessionConfig { it.isTorchParameterEnabled() }
+        cameraControl.waitForSessionConfig {
+            if (isLowLightBoostEnabled) {
+                it.isLowLightBoostEnabled()
+            } else {
+                it.isTorchParameterEnabled()
+            }
+        }
+
+        // When low-light boost is on, AE pre-capture is not triggered. Therefore, converged stage
+        // is not required.
+        if (isLowLightBoostEnabled) {
+            return
+        }
 
         // Switch the repeating result to 3A converged state.
         cameraControl.simulateRepeatingResult(
@@ -468,7 +504,19 @@
             }
 
         // Assert 1 torch should be turned on
-        cameraControl.waitForSessionConfig { it.isTorchParameterEnabled() }
+        cameraControl.waitForSessionConfig {
+            if (isLowLightBoostEnabled) {
+                it.isLowLightBoostEnabled()
+            } else {
+                it.isTorchParameterEnabled()
+            }
+        }
+
+        // When low-light boost is on, AE pre-capture is not triggered. Therefore, converged stage
+        // is not required.
+        if (isLowLightBoostEnabled) {
+            return
+        }
 
         // Switch the repeating result to 3A converged state.
         cameraControl.simulateRepeatingResult(
@@ -503,7 +551,19 @@
             }
 
         // Assert 1 torch should be turned on
-        cameraControl.waitForSessionConfig { it.isTorchParameterEnabled() }
+        cameraControl.waitForSessionConfig {
+            if (isLowLightBoostEnabled) {
+                it.isLowLightBoostEnabled()
+            } else {
+                it.isTorchParameterEnabled()
+            }
+        }
+
+        // When low-light boost is on, AE pre-capture is not triggered. Therefore, converged stage
+        // is not required.
+        if (isLowLightBoostEnabled) {
+            return
+        }
 
         // Switch the repeating result to 3A converged state.
         cameraControl.simulateRepeatingResult(
@@ -794,7 +854,7 @@
         // Assert.
         val exception =
             assertThrows(ExecutionException::class.java) { future.get(1, TimeUnit.SECONDS) }
-        assertTrue(exception.cause is ImageCaptureException)
+        assertThat(exception.cause).isInstanceOf(ImageCaptureException::class.java)
         assertThat((exception.cause as ImageCaptureException).imageCaptureError)
             .isEqualTo(ImageCapture.ERROR_CAPTURE_FAILED)
     }
@@ -830,7 +890,7 @@
         // Assert.
         val exception =
             assertThrows(ExecutionException::class.java) { future.get(1, TimeUnit.SECONDS) }
-        assertTrue(exception.cause is ImageCaptureException)
+        assertThat(exception.cause).isInstanceOf(ImageCaptureException::class.java)
         assertThat((exception.cause as ImageCaptureException).imageCaptureError)
             .isEqualTo(ImageCapture.ERROR_CAMERA_CLOSED)
     }
@@ -1170,6 +1230,13 @@
                 CameraMetadata.FLASH_MODE_TORCH
     }
 
+    private fun SessionConfig.isLowLightBoostEnabled(): Boolean {
+        val config = toCamera2Config()
+
+        return config.getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, null) ==
+            CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
+    }
+
     private fun List<CaptureConfig>.isTorchParameterDisabled() =
         requestContains(
             CaptureRequest.CONTROL_AE_MODE,
@@ -1283,6 +1350,8 @@
                 setActive(true)
                 incrementUseCount()
                 this.screenFlash = testScreenFlash
+                // Applies low-light boost setting
+                enableLowLightBoostAndAssert(this)
             }
     }
 
@@ -1297,14 +1366,19 @@
                 set(CameraCharacteristics.FLASH_INFO_AVAILABLE, true)
                 set(
                     CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
-                    intArrayOf(
-                        CaptureRequest.CONTROL_AE_MODE_OFF,
-                        CaptureRequest.CONTROL_AE_MODE_ON,
-                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH,
-                        CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
-                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE,
-                        CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH
-                    )
+                    mutableListOf<Int>()
+                        .apply {
+                            add(CaptureRequest.CONTROL_AE_MODE_OFF)
+                            add(CaptureRequest.CONTROL_AE_MODE_ON)
+                            add(CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)
+                            add(CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+                            add(CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE)
+                            add(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH)
+                            if (Build.VERSION.SDK_INT >= 35) {
+                                add(CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY)
+                            }
+                        }
+                        .toIntArray()
                 )
                 set(CameraCharacteristics.LENS_FACING, CameraMetadata.LENS_FACING_BACK)
             }
@@ -1334,7 +1408,7 @@
                     }
                     waitingList.add(resultPair)
                 }
-                assertTrue(resultPair.first.await(timeout, TimeUnit.MILLISECONDS))
+                assertThat(resultPair.first.await(timeout, TimeUnit.MILLISECONDS)).isTrue()
                 waitingList.remove(resultPair)
             }
 
@@ -1459,13 +1533,16 @@
 
         cameraControl.mZslControl = zslControl
 
+        // Applies low-light boost setting
+        enableLowLightBoostAndAssert(cameraControl)
+
         return cameraControl
     }
 
-    private fun Looper.advanceUntilIdle() {
-        val shadowLooper = Shadows.shadowOf(this)
-        while (!shadowLooper.isIdle) {
-            shadowLooper.idle()
+    private fun enableLowLightBoostAndAssert(cameraControlImpl: Camera2CameraControlImpl) {
+        if (isLowLightBoostEnabled) {
+            executorService.run { cameraControlImpl.enableLowLightBoostInternal(true) }
         }
+        assertThat(cameraControlImpl.isLowLightBoostOn).isEqualTo(isLowLightBoostEnabled)
     }
 }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/LowLightBoostControlTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/LowLightBoostControlTest.kt
new file mode 100644
index 0000000..bed297c
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/LowLightBoostControlTest.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.camera2.internal
+
+import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE
+import android.hardware.camera2.TotalCaptureResult
+import android.os.Looper.getMainLooper
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.core.CameraControl.OperationCanceledException
+import androidx.camera.core.LowLightBoostState
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+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
+import org.robolectric.shadows.ShadowLog
+
+private const val CAMERA_ID_0 = "0"
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = 35,
+)
+class LowLightBoostControlTest {
+
+    private val context = ApplicationProvider.getApplicationContext() as Context
+
+    @Test
+    fun enableLowLightBoostThrowException_stateActive_whenIsSupported() {
+        ShadowLog.stream = System.out
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            val future = enableLowLightBoost(true)
+
+            // Issue CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY AE mode capture result
+            // to check whether the future can be completed or not.
+            issueControlAeModeCaptureResult(mCaptureResultListener)
+            shadowOf(getMainLooper()).idle()
+
+            future.get(1, TimeUnit.SECONDS)
+        }
+    }
+
+    @Test
+    fun enableLowLightBoost_stateInactive_whenIsSupported() {
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(false)
+            val future = enableLowLightBoost(true)
+            shadowOf(getMainLooper()).idle()
+            assertFutureCompleteWithException(future, OperationCanceledException::class.java)
+        }
+    }
+
+    @Test
+    fun enableLowLightBoostThrowException_stateActive_whenIsNotSupported() {
+        initCamera(supportedLlb = false)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            val future = enableLowLightBoost(true)
+            assertFutureCompleteWithException(future, IllegalStateException::class.java)
+        }
+    }
+
+    @Test
+    fun getLowLightBoostState_stateActive_whenIsNotSupported() {
+        initCamera(supportedLlb = false)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            enableLowLightBoost(true)
+            shadowOf(getMainLooper()).idle()
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.OFF)
+        }
+    }
+
+    @Test
+    fun getLowLightBoostState_stateActive_whenIsSupported() {
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            // State is OFF before low-light boost is enabled
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.OFF)
+
+            enableLowLightBoost(true)
+            shadowOf(getMainLooper()).idle()
+            // State is INACTIVE after low-light boost is enabled but no ACTIVE state is received
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.INACTIVE)
+
+            issueControlAeModeCaptureResult(
+                mCaptureResultListener,
+                llbState = LowLightBoostState.ACTIVE
+            )
+            // When low-light boost state is updated, postValue method is invoked. In robolectric
+            // test, it is hard to know when will the task is added to the main thread. Adding
+            // delay 100ms here to make sure that the value update task has been posted to the main
+            // thread.
+            runBlocking { delay(100) }
+            shadowOf(getMainLooper()).idle()
+            // State is ACTIVE after ACTIVE state is received
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.ACTIVE)
+
+            issueControlAeModeCaptureResult(
+                mCaptureResultListener,
+                llbState = LowLightBoostState.INACTIVE
+            )
+            runBlocking { delay(100) }
+            shadowOf(getMainLooper()).idle()
+            // State is INACTIVE after INACTIVE state is received again
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.INACTIVE)
+        }
+    }
+
+    @Test
+    fun getLowLightBoostState_stateInactive_whenIsSupported() {
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(false)
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.OFF)
+        }
+    }
+
+    @Test
+    fun enableTorchTwice_cancelPreviousFuture() {
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            val future1 = enableLowLightBoost(true)
+            val future2 = enableLowLightBoost(true)
+            shadowOf(getMainLooper()).idle()
+
+            // Verifies that the first future has been canceled.
+            assertFutureCompleteWithException(future1, OperationCanceledException::class.java)
+
+            // Verifies that the second future can be completed after issue the AE mode capture
+            // result
+            issueControlAeModeCaptureResult(mCaptureResultListener)
+            shadowOf(getMainLooper()).idle()
+            future2.get(1, TimeUnit.SECONDS)
+        }
+    }
+
+    @Test
+    fun setInActive_cancelPreviousFuture() {
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            val future = enableLowLightBoost(true)
+            shadowOf(getMainLooper()).idle()
+
+            setActive(false)
+            assertFutureCompleteWithException(future, OperationCanceledException::class.java)
+        }
+    }
+
+    @Test
+    fun setInActive_changeToStateOff() {
+        initCamera(supportedLlb = true)
+        createLowLightBoostControl().apply {
+            setActive(true)
+            enableLowLightBoost(true)
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.INACTIVE)
+
+            setActive(false)
+            shadowOf(getMainLooper()).idle()
+            assertThat(lowLightBoostState.value).isEqualTo(LowLightBoostState.OFF)
+        }
+    }
+
+    private fun assertFutureCompleteWithException(future: ListenableFuture<Void>, clazz: Class<*>) {
+        assertThat(future.isDone).isTrue()
+        try {
+            future.get()
+        } catch (exception: ExecutionException) {
+            assertThat(exception.cause).isInstanceOf(clazz)
+        }
+    }
+
+    private fun createLowLightBoostControl(): LowLightBoostControl {
+        val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+        val cameraCharacteristics = cameraManager.getCameraCharacteristics(CAMERA_ID_0)
+        val characteristicsCompat =
+            CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
+                cameraCharacteristics,
+                CAMERA_ID_0
+            )
+
+        val cameraControlImpl =
+            Camera2CameraControlImpl(
+                characteristicsCompat,
+                CameraXExecutors.mainThreadExecutor(),
+                CameraXExecutors.mainThreadExecutor(),
+                mock(CameraControlInternal.ControlUpdateCallback::class.java)
+            )
+
+        return LowLightBoostControl(
+            cameraControlImpl,
+            characteristicsCompat,
+            CameraXExecutors.mainThreadExecutor()
+        )
+    }
+
+    private fun issueControlAeModeCaptureResult(
+        captureResultListener: Camera2CameraControlImpl.CaptureResultListener,
+        resultAeMode: Int = CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY,
+        llbState: Int? = null
+    ) {
+        Executors.newSingleThreadScheduledExecutor()
+            .schedule(
+                {
+                    val captureRequest = mock(CaptureRequest::class.java)
+                    Mockito.`when`(captureRequest.get(CaptureRequest.CONTROL_AE_MODE))
+                        .thenReturn(resultAeMode)
+
+                    val totalCaptureResult = mock(TotalCaptureResult::class.java)
+                    Mockito.`when`(totalCaptureResult.request).thenReturn(captureRequest)
+
+                    llbState?.let {
+                        Mockito.`when`(totalCaptureResult.get(CONTROL_LOW_LIGHT_BOOST_STATE))
+                            .thenReturn(llbState)
+                    }
+                    captureResultListener.onCaptureResult(totalCaptureResult)
+                },
+                20,
+                TimeUnit.MILLISECONDS
+            )
+    }
+
+    private fun initCamera(supportedLlb: Boolean) {
+        val cameraCharacteristics =
+            ShadowCameraCharacteristics.newCameraCharacteristics().also {
+                Shadow.extract<ShadowCameraCharacteristics>(it).apply {
+                    set(
+                        CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+                        mutableListOf<Int>()
+                            .apply {
+                                add(CaptureRequest.CONTROL_AE_MODE_OFF)
+                                add(CaptureRequest.CONTROL_AE_MODE_ON)
+                                add(CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)
+                                add(CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+                                add(CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE)
+                                add(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH)
+
+                                if (supportedLlb) {
+                                    add(CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY)
+                                }
+                            }
+                            .toIntArray()
+                    )
+                    set(CameraCharacteristics.LENS_FACING, CameraMetadata.LENS_FACING_BACK)
+                }
+            }
+
+        Shadow.extract<ShadowCameraManager>(context.getSystemService(Context.CAMERA_SERVICE))
+            .apply { addCamera(CAMERA_ID_0, cameraCharacteristics) }
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
index 4937e7b..d725f7f 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.camera2.internal;
 
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
 import static android.os.Looper.getMainLooper;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -29,6 +30,7 @@
 import static org.mockito.internal.verification.VerificationModeFactory.times;
 import static org.robolectric.Shadows.shadowOf;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCharacteristics;
@@ -75,6 +77,8 @@
 
     private TorchControl mNoFlashUnitTorchControl;
     private TorchControl mTorchControl;
+    private CameraCharacteristicsCompat mLlbEnabledCameraCharacteristicsCompat;
+    private Camera2CameraControlImpl mLlbEnabledCamera2CameraControl;
     private Camera2CameraControlImpl.CaptureResultListener mCaptureResultListener;
     private TestLifecycleOwner mLifecycleOwner;
 
@@ -104,22 +108,25 @@
         /* Prepare CameraControl 1 which flash is available */
         CameraCharacteristics cameraCharacteristics1 =
                 cameraManager.getCameraCharacteristics(CAMERA1_ID);
-        CameraCharacteristicsCompat characteristicsCompat1 =
+        mLlbEnabledCameraCharacteristicsCompat =
                 CameraCharacteristicsCompat.toCameraCharacteristicsCompat(cameraCharacteristics1,
                         CAMERA1_ID);
 
-        Camera2CameraControlImpl camera2CameraControlImpl1 =
-                spy(new Camera2CameraControlImpl(characteristicsCompat1,
-                        CameraXExecutors.mainThreadExecutor(),
-                        CameraXExecutors.mainThreadExecutor(),
-                        mock(CameraControlInternal.ControlUpdateCallback.class)));
-        mTorchControl = new TorchControl(camera2CameraControlImpl1, characteristicsCompat1,
+        mLlbEnabledCamera2CameraControl = new Camera2CameraControlImpl(
+                mLlbEnabledCameraCharacteristicsCompat,
+                CameraXExecutors.mainThreadExecutor(),
+                CameraXExecutors.mainThreadExecutor(),
+                mock(CameraControlInternal.ControlUpdateCallback.class));
+        Camera2CameraControlImpl camera2CameraControl1 = spy(mLlbEnabledCamera2CameraControl);
+        mTorchControl = new TorchControl(camera2CameraControl1,
+                mLlbEnabledCameraCharacteristicsCompat,
                 CameraXExecutors.mainThreadExecutor());
         mTorchControl.setActive(true);
 
         ArgumentCaptor<Camera2CameraControlImpl.CaptureResultListener> argumentCaptor =
                 ArgumentCaptor.forClass(Camera2CameraControlImpl.CaptureResultListener.class);
-        verify(camera2CameraControlImpl1).addCaptureResultListener(argumentCaptor.capture());
+        verify(camera2CameraControl1).addCaptureResultListener(
+                argumentCaptor.capture());
         mCaptureResultListener = argumentCaptor.getValue();
 
         /* Prepare Lifecycle for test LiveData */
@@ -305,6 +312,60 @@
         assertThat(torchStates.get(2)).isEqualTo(TorchState.OFF); // by enableTorch(false)
     }
 
+    @Config(minSdk = 35)
+    @Test
+    public void enableTorchIsCanceled_whenLowLightBoostIsOn() {
+        // Activates the Camera2CameraControlImpl and ensures low-light boost is turned on
+        mLlbEnabledCamera2CameraControl.incrementUseCount();
+        mLlbEnabledCamera2CameraControl.setActive(true);
+
+        TorchControl torchControl = mLlbEnabledCamera2CameraControl.getTorchControl();
+
+        mLlbEnabledCamera2CameraControl.enableLowLightBoostAsync(true);
+        shadowOf(getMainLooper()).idle();
+        assertThat(mLlbEnabledCamera2CameraControl.isLowLightBoostOn()).isTrue();
+
+        // Verifies that enabling torch operation will run failed with IllegalStateException cause
+        ListenableFuture<Void> future = torchControl.enableTorch(true);
+        shadowOf(getMainLooper()).idle();
+        Throwable cause = null;
+        try {
+            future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            // The real cause is wrapped in ExecutionException, retrieve it and check.
+            cause = e.getCause();
+        }
+
+        assertThat(cause).isInstanceOf(IllegalStateException.class);
+    }
+
+    @SuppressLint("BanThreadSleep")
+    @Config(minSdk = 35)
+    @Test
+    public void torchIsDisabled_whenLowLightBoostIsTurnedOn() {
+        // Activates the Camera2CameraControlImpl
+        mLlbEnabledCamera2CameraControl.incrementUseCount();
+        mLlbEnabledCamera2CameraControl.setActive(true);
+
+        TorchControl torchControl = mLlbEnabledCamera2CameraControl.getTorchControl();
+        assertThat(torchControl.getTorchState().getValue()).isEqualTo(TorchState.OFF);
+
+        // Turns on torch
+        torchControl.enableTorch(true);
+        shadowOf(getMainLooper()).idle();
+        assertThat(mLlbEnabledCamera2CameraControl.isTorchOn()).isTrue();
+        assertThat(torchControl.getTorchState().getValue()).isEqualTo(TorchState.ON);
+
+        // Turns on low-light boost
+        mLlbEnabledCamera2CameraControl.enableLowLightBoostAsync(true);
+        shadowOf(getMainLooper()).idle();
+
+        // Verifies that enabling torch operation will run failed with IllegalStateException cause
+        assertThat(mLlbEnabledCamera2CameraControl.isLowLightBoostOn()).isTrue();
+        assertThat(mLlbEnabledCamera2CameraControl.isTorchOn()).isFalse();
+        assertThat(torchControl.getTorchState().getValue()).isEqualTo(TorchState.OFF);
+    }
+
     private void initShadowCameraManager() {
         // **** Camera 0 characteristics ****//
         CameraCharacteristics characteristics0 =
@@ -326,6 +387,11 @@
         ShadowCameraCharacteristics shadowCharacteristics1 = Shadow.extract(characteristics1);
 
         shadowCharacteristics1.set(CameraCharacteristics.FLASH_INFO_AVAILABLE, true);
+        if (Build.VERSION.SDK_INT >= 35) {
+            // Adds low-light boost capability for related tests
+            shadowCharacteristics1.set(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+                    new int[]{CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY});
+        }
 
         ((ShadowCameraManager)
                 Shadow.extract(
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 191b670..b00dd05 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -85,6 +85,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         externalNativeBuild {
             def versionScript = file("src/main/cpp/jni.lds").getAbsolutePath()
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index beff098..9dbfa50 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.FloatRange;
 import androidx.annotation.RestrictTo;
+import androidx.camera.core.impl.utils.futures.Futures;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -63,6 +64,53 @@
     @NonNull ListenableFuture<Void> enableTorch(boolean torch);
 
     /**
+     * Enables low-light boost mode.
+     *
+     * <p>Devices running Android 15 or higher can provide support for low-light boost. This
+     * feature can automatically adjust the brightness of the preview, video or image analysis
+     * streams in low-light conditions. This is different from how the night mode camera
+     * extension creates still images, because night mode combines a burst of photos to create a
+     * single, enhanced image. While night mode works very well for creating a still image, it
+     * can't create a continuous stream of frames, but Low Light Boost can. Thus, Low Light Boost
+     * enables new camera capabilities, such as the following:
+     *
+     * <ul>
+     * <li>Providing an enhanced image preview, so users can better frame their low-light pictures.
+     * <li>Recording brighter videos in low-light conditions.
+     * <li>Scanning QR codes in low-light conditions.
+     * </ul>
+     *
+     * <p>Applications can query the low-light boost availability via
+     * {@link CameraInfo#isLowLightBoostSupported()}. If you enable Low Light Boost, it
+     * automatically turns on when there's a low light level, and turns off when there's more light.
+     *
+     * <p>If the camera device does not support low-light boost, the value obtained via
+     * {@link CameraInfo#getLowLightBoostState()} will always be {@link LowLightBoostState#OFF}.
+     *
+     * <p>Note that this mode may interact with other configurations:
+     *
+     * <ul>
+     * <li>When low-light boost is on, the flash or torch functionality may be unavailable.
+     * <li>When frame rate configuration results in an FPS exceeding 30, low-light boost will be
+     * disabled and the state will always be ({@link LowLightBoostState#OFF}).
+     * </ul>
+     *
+     * <p>Therefore, to use flash or torch functionality, low-light boost mode must be disabled.
+     * To ensure low-light boost mode functions correctly, avoid frame rate settings that result
+     * in an FPS exceeding 30.
+     *
+     * @param lowLightBoost true to turn on the low-light boost mode, false to turn it off.
+     * @return A {@link ListenableFuture} which is successful when the low-light boost mode was
+     * changed to the value specified. It fails when it is unable to change the log-light boost
+     * state. Cancellation of this future is a no-op.
+     * @see CameraInfo#isLowLightBoostSupported()
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    default @NonNull ListenableFuture<Void> enableLowLightBoostAsync(boolean lowLightBoost) {
+        return Futures.immediateFailedFuture(new OperationCanceledException("Not supported!"));
+    }
+
+    /**
      * Starts a focus and metering action configured by the {@link FocusMeteringAction}.
      *
      * <p>It will trigger an auto focus action and enable AF/AE/AWB metering regions. The action
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 805440f..e2f6556 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -17,6 +17,7 @@
 package androidx.camera.core;
 
 import android.graphics.ImageFormat;
+import android.hardware.camera2.CaptureRequest;
 import android.media.MediaActionSound;
 import android.util.Range;
 import android.view.Surface;
@@ -30,6 +31,7 @@
 import androidx.camera.core.internal.compat.MediaActionSoundCompat;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Observer;
 
 import org.jspecify.annotations.NonNull;
@@ -430,4 +432,38 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @interface ImplementationType {
     }
+
+    /**
+     * Returns if low-light boost is supported on the device. Low-light boost can be turned on via
+     * {@link CameraControl#enableLowLightBoostAsync(boolean)}.
+     *
+     * @return true if
+     * {@link CaptureRequest#CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY} is supported,
+     * otherwise false.
+     * @see CameraControl#enableLowLightBoostAsync(boolean)
+     * @see CaptureRequest#CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default boolean isLowLightBoostSupported() {
+        return false;
+    }
+
+    /**
+     * Returns a {@link LiveData} of current {@link LowLightBoostState}.
+     *
+     * <p>Low-light boost can be turned on via
+     * {@link CameraControl#enableLowLightBoostAsync(boolean)} which will trigger the change
+     * event to the returned {@link LiveData}. Apps can either get immediate value via
+     * {@link LiveData#getValue()} or observe it via
+     * {@link LiveData#observe(LifecycleOwner, Observer)} to update low-light boost UI accordingly.
+     *
+     * <p>If the camera doesn't support low-light boost, then the state will always be
+     * {@link LowLightBoostState#OFF}.
+     *
+     * @return a {@link LiveData} containing current low-light boost state.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default @NonNull LiveData<Integer> getLowLightBoostState() {
+        return new MutableLiveData<>(LowLightBoostState.OFF);
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/LowLightBoostState.java b/camera/camera-core/src/main/java/androidx/camera/core/LowLightBoostState.java
new file mode 100644
index 0000000..94d37b3
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/LowLightBoostState.java
@@ -0,0 +1,51 @@
+/*
+ * 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 androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Defines the valid states for Low Light Boost, as returned by
+ * {@link CameraInfo#getLowLightBoostState()}.
+ *
+ * <p>These states indicate whether the camera device supports low light boost and if it is
+ * currently active.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class LowLightBoostState {
+    /** Low-light boost is off. */
+    public static final int OFF = -1;
+    /** Low-light boost is on but inactive. */
+    public static final int INACTIVE = 0;
+    /** Low-light boost is on and active. */
+    public static final int ACTIVE = 1;
+
+    private LowLightBoostState() {
+    }
+
+    /**
+     */
+    @IntDef({OFF, INACTIVE, ACTIVE})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @interface State {
+    }
+}
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
index f3531bb..66ea978 100644
--- 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
@@ -52,6 +52,11 @@
     }
 
     @Override
+    public @NonNull ListenableFuture<Void> enableLowLightBoostAsync(boolean lowLightBoost) {
+        return mCameraControlInternal.enableLowLightBoostAsync(lowLightBoost);
+    }
+
+    @Override
     public @NonNull ListenableFuture<FocusMeteringResult> startFocusAndMetering(
             @NonNull FocusMeteringAction action) {
         return mCameraControlInternal.startFocusAndMetering(action);
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
index 267475d..fd752fd 100644
--- 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
@@ -73,6 +73,16 @@
     }
 
     @Override
+    public boolean isLowLightBoostSupported() {
+        return mCameraInfoInternal.isLowLightBoostSupported();
+    }
+
+    @Override
+    public @NonNull LiveData<Integer> getLowLightBoostState() {
+        return mCameraInfoInternal.getLowLightBoostState();
+    }
+
+    @Override
     public @NonNull LiveData<ZoomState> getZoomState() {
         return mCameraInfoInternal.getZoomState();
     }
diff --git a/camera/camera-effects/build.gradle b/camera/camera-effects/build.gradle
index 647f2fd..8bb2c7e 100644
--- a/camera/camera-effects/build.gradle
+++ b/camera/camera-effects/build.gradle
@@ -49,6 +49,7 @@
     androidTestImplementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
 }
 android {
+    compileSdk 35
     testOptions.unitTests.includeAndroidResources = true
     namespace = "androidx.camera.effects"
 }
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 0e4ace09..bb99409 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -76,6 +76,8 @@
 }
 
 android {
+    compileSdk 35
+
     buildTypes.configureEach {
         consumerProguardFiles "proguard-rules.pro"
     }
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index c02d39a..a051ed1 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -61,6 +61,8 @@
 }
 
 android {
+    compileSdk 35
+
     lintOptions {
         enable 'CameraXQuirksClassDetector'
     }
diff --git a/camera/camera-lifecycle/samples/build.gradle b/camera/camera-lifecycle/samples/build.gradle
index 5f5011f..09838ff 100644
--- a/camera/camera-lifecycle/samples/build.gradle
+++ b/camera/camera-lifecycle/samples/build.gradle
@@ -39,6 +39,7 @@
 }
 
 android {
+    compileSdk 35
     namespace = "androidx.camera.lifecycle.samples"
 }
 
diff --git a/camera/camera-mlkit-vision/build.gradle b/camera/camera-mlkit-vision/build.gradle
index 6ad87b4..25831a7 100644
--- a/camera/camera-mlkit-vision/build.gradle
+++ b/camera/camera-mlkit-vision/build.gradle
@@ -45,6 +45,8 @@
 }
 
 android {
+    compileSdk 35
+
     lintOptions {
         enable 'CameraXQuirksClassDetector'
     }
diff --git a/camera/camera-testing/build.gradle b/camera/camera-testing/build.gradle
index 0038f04..a702c14 100644
--- a/camera/camera-testing/build.gradle
+++ b/camera/camera-testing/build.gradle
@@ -79,6 +79,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         externalNativeBuild {
             cmake {
diff --git a/camera/camera-testlib-extensions/build.gradle b/camera/camera-testlib-extensions/build.gradle
index d0e83eeb..a220c59 100644
--- a/camera/camera-testlib-extensions/build.gradle
+++ b/camera/camera-testlib-extensions/build.gradle
@@ -35,6 +35,8 @@
 }
 
 android {
+    compileSdk 35
+
     lintOptions {
         enable 'CameraXQuirksClassDetector'
     }
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index d719316..9cbe32e 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -75,6 +75,8 @@
 }
 
 android {
+    compileSdk 35
+
     lintOptions {
         enable 'CameraXQuirksClassDetector'
     }
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index e1601f3..1d1f1e5 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -76,6 +76,8 @@
     androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
 }
 android {
+    compileSdk 35
+
     lintOptions {
         enable 'CameraXQuirksClassDetector'
     }
diff --git a/camera/integration-tests/camerapipetestapp/build.gradle b/camera/integration-tests/camerapipetestapp/build.gradle
index a615ba4..6c49dbe 100644
--- a/camera/integration-tests/camerapipetestapp/build.gradle
+++ b/camera/integration-tests/camerapipetestapp/build.gradle
@@ -22,6 +22,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         applicationId = "androidx.camera.integration.camera2.pipe"
     }
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index 9b27d74..4b9931e 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -21,6 +21,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         applicationId = "androidx.camera.integration.core"
 
diff --git a/camera/integration-tests/diagnosetestapp/build.gradle b/camera/integration-tests/diagnosetestapp/build.gradle
index 70a9f1d..2db2390 100644
--- a/camera/integration-tests/diagnosetestapp/build.gradle
+++ b/camera/integration-tests/diagnosetestapp/build.gradle
@@ -21,6 +21,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         applicationId = "androidx.camera.integration.diagnose"
     }
diff --git a/camera/integration-tests/extensionstestapp/build.gradle b/camera/integration-tests/extensionstestapp/build.gradle
index c2c09a4..03e2990 100644
--- a/camera/integration-tests/extensionstestapp/build.gradle
+++ b/camera/integration-tests/extensionstestapp/build.gradle
@@ -31,6 +31,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         applicationId = "androidx.camera.integration.extensions"
     }
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/PermissionUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/PermissionUtil.kt
index 1ed34c4..0b1bd0b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/PermissionUtil.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/PermissionUtil.kt
@@ -88,7 +88,10 @@
             return arrayOfNulls(0)
         }
 
-        if (info.requestedPermissions == null || info.requestedPermissions.isEmpty()) {
+        if (
+            info.requestedPermissions == null ||
+                (info.requestedPermissions as Array<out Any>).isEmpty()
+        ) {
             return arrayOfNulls(0)
         }
 
@@ -99,7 +102,8 @@
         // READ_EXTERNAL_STORAGE will also be included if we specify WRITE_EXTERNAL_STORAGE
         // requirement in AndroidManifest.xml. Therefore, also need to skip the permission check
         // of READ_EXTERNAL_STORAGE.
-        for (permission in info.requestedPermissions) {
+        val requestedPermissions = info.requestedPermissions as Array<out Any>
+        for (permission in requestedPermissions) {
             if (
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
                     (Manifest.permission.WRITE_EXTERNAL_STORAGE == permission ||
@@ -108,7 +112,7 @@
                 continue
             }
 
-            requiredPermissions.add(permission)
+            requiredPermissions.add(permission.toString())
         }
 
         val permissions = requiredPermissions.toTypedArray<String?>()
diff --git a/camera/integration-tests/testingtestapp/build.gradle.kts b/camera/integration-tests/testingtestapp/build.gradle.kts
index e2c430a..3fbaddf 100644
--- a/camera/integration-tests/testingtestapp/build.gradle.kts
+++ b/camera/integration-tests/testingtestapp/build.gradle.kts
@@ -24,6 +24,8 @@
 }
 
 android {
+    compileSdk = 35
+
     namespace = "androidx.camera.integration.testingtestapp"
 
     defaultConfig {
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt
index 933cedc..44a8f40 100644
--- a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt
@@ -50,6 +50,7 @@
         */
     )
 
+@Suppress("DEPRECATION") // setStatusBarColor
 @Composable
 fun MultimoduleTemplateTheme(
     darkTheme: Boolean = isSystemInDarkTheme(),
diff --git a/camera/integration-tests/timingtestapp/build.gradle b/camera/integration-tests/timingtestapp/build.gradle
index 43c8180..896b44b 100644
--- a/camera/integration-tests/timingtestapp/build.gradle
+++ b/camera/integration-tests/timingtestapp/build.gradle
@@ -21,6 +21,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         applicationId = "androidx.camera.integration.antelope"
         versionCode 35
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
index 0f01e34..af82a97 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
@@ -776,7 +776,7 @@
 
 /** Return the version name of the Activity */
 @Suppress("DEPRECATION")
-fun getVersionName(activity: MainActivity): String {
+fun getVersionName(activity: MainActivity): String? {
     val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
     return packageInfo.versionName
 }
diff --git a/camera/viewfinder/viewfinder-core/samples/build.gradle b/camera/viewfinder/viewfinder-core/samples/build.gradle
index 0baad03..4f7cd42 100644
--- a/camera/viewfinder/viewfinder-core/samples/build.gradle
+++ b/camera/viewfinder/viewfinder-core/samples/build.gradle
@@ -38,6 +38,7 @@
 }
 
 android {
+    compileSdk 35
     namespace = "androidx.camera.viewfinder.core.samples"
 }