Skip to content

Commit 6ba412b

Browse files
authored
Modularize low-light boost feature with Hilt DI (#419)
Extracts the low-light boost feature into separate Gradle modules. This allows the feature to be conditionally included in different builds or product flavors, improving build flexibility. The feature is split into two new modules: - :core📷low-light: Contains the public API, interfaces, and Hilt multibinding definitions. - :core📷low-light-playservices: Provides the concrete implementation using the Google Play Services SDK. The :core:camera module is now decoupled from the implementation and only depends on the :core📷low-light API module. The main :app module includes the feature by depending on the :core📷low-light-playservices module. Additionally, the `isCoreLibraryDesugaringEnabled` flag has been removed. Recent versions of the Google Play Services libraries, released after July 2025, no longer require this flag to be explicitly set.
1 parent 531892f commit 6ba412b

File tree

30 files changed

+606
-136
lines changed

30 files changed

+606
-136
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ dependencies {
168168

169169
// capture components
170170
implementation(project(":ui:components:capture"))
171+
172+
// Low Light implementations
173+
implementation(project(":core:camera:low-light-playservices"))
171174
}
172175

173176
// Allow references to generated code

core/camera/build.gradle.kts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ android {
8989
}
9090

9191
compileOptions {
92-
isCoreLibraryDesugaringEnabled = true
9392
sourceCompatibility = JavaVersion.VERSION_17
9493
targetCompatibility = JavaVersion.VERSION_17
9594
}
@@ -141,17 +140,8 @@ dependencies {
141140
implementation(project(":data:settings"))
142141
implementation(project(":core:model"))
143142
implementation(project(":core:common"))
143+
implementation(project(":core:camera:low-light"))
144144

145-
// Google Play Services
146-
implementation(libs.play.services.base)
147-
implementation(libs.play.services.tasks)
148-
implementation(libs.kotlinx.coroutines.play.services)
149-
150-
// Google Low Light Boost
151-
implementation(libs.play.services.camera.low.light.boost)
152-
153-
// Desugaring
154-
coreLibraryDesugaring(libs.desugar.jdk.libs)
155145
}
156146

157147
// Allow references to generated code
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (C) 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
plugins {
18+
alias(libs.plugins.android.library)
19+
alias(libs.plugins.kotlin.android)
20+
alias(libs.plugins.kotlin.kapt)
21+
alias(libs.plugins.dagger.hilt.android)
22+
}
23+
24+
android {
25+
namespace = "com.google.jetpackcamera.core.camera.lowlight.playservices"
26+
compileSdk = libs.versions.compileSdk.get().toInt()
27+
28+
defaultConfig {
29+
minSdk = libs.versions.minSdk.get().toInt()
30+
}
31+
compileOptions {
32+
sourceCompatibility = JavaVersion.VERSION_17
33+
targetCompatibility = JavaVersion.VERSION_17
34+
}
35+
kotlin {
36+
jvmToolchain(17)
37+
}
38+
}
39+
40+
dependencies {
41+
implementation(project(":core:camera:low-light"))
42+
implementation(libs.play.services.base)
43+
implementation(libs.play.services.camera.low.light.boost)
44+
implementation(libs.androidx.annotation)
45+
implementation(libs.camera.camera2)
46+
implementation(libs.camera.core)
47+
implementation(libs.dagger.hilt.android)
48+
kapt(libs.dagger.hilt.compiler)
49+
implementation(libs.kotlinx.coroutines.play.services)
50+
}
51+
52+
// Allow references to generated code
53+
kapt {
54+
correctErrorTypes = true
55+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2025 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<manifest />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (C) 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.jetpackcamera.core.camera.lowlight.playservices
17+
18+
import android.content.Context
19+
import android.os.Build
20+
import android.util.Log
21+
import androidx.annotation.OptIn
22+
import androidx.camera.camera2.interop.Camera2CameraInfo
23+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
24+
import androidx.camera.core.CameraInfo
25+
import com.google.android.gms.cameralowlight.LowLightBoost
26+
import com.google.android.gms.common.ConnectionResult
27+
import com.google.android.gms.common.GoogleApiAvailability
28+
import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostAvailabilityChecker
29+
import javax.inject.Inject
30+
import kotlinx.coroutines.tasks.await
31+
32+
private const val TAG = "PsLlbAvailChecker"
33+
34+
class PlayServicesLowLightBoostAvailabilityChecker @Inject constructor() :
35+
LowLightBoostAvailabilityChecker {
36+
@OptIn(ExperimentalCamera2Interop::class)
37+
override suspend fun isImplementationAvailable(
38+
cameraInfo: CameraInfo,
39+
context: Context
40+
): Boolean {
41+
val camera2Info = Camera2CameraInfo.from(cameraInfo)
42+
// Check for Google LLB support.
43+
var gLlbSupport = false
44+
var gLlbAvailable = false
45+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
46+
val cameraId = camera2Info.cameraId
47+
try {
48+
// TODO: Remove when Google LLB beta07 is available with this fixed.
49+
if (!isGooglePlayServicesWithVideoTimestampFixAvailable(context)) {
50+
throw Exception("Google Play Services with video timestamp fix not available.")
51+
}
52+
val lowLightBoostClient = LowLightBoost.getClient(context)
53+
gLlbSupport = lowLightBoostClient.isCameraSupported(cameraId).await()
54+
gLlbAvailable = lowLightBoostClient.isModuleInstalled().await()
55+
if (gLlbSupport && !gLlbAvailable) {
56+
// Install the module for future use, but the install will take too long to use
57+
// now since the camera needs to be opened right away.
58+
lowLightBoostClient.installModule(null).addOnSuccessListener {
59+
Log.d(
60+
TAG,
61+
"Low Light Boost module installation successful. " +
62+
"App restart required for the feature to be available."
63+
)
64+
}.addOnFailureListener { e ->
65+
Log.e(TAG, "Low Light Boost module installation failed.", e)
66+
}
67+
}
68+
} catch (e: Exception) {
69+
Log.e(TAG, "Failed to set up Google Low Light Boost for camera $cameraId", e)
70+
gLlbSupport = false
71+
gLlbAvailable = false
72+
}
73+
}
74+
return gLlbSupport && gLlbAvailable
75+
}
76+
}
77+
78+
// TODO: Remove when Google LLB beta07 is available with this fixed.
79+
private fun isGooglePlayServicesWithVideoTimestampFixAvailable(context: Context): Boolean {
80+
val minVersion = 253300000 // (Y25W33)
81+
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context, minVersion) ==
82+
ConnectionResult.SUCCESS
83+
}

core/camera/src/main/java/com/google/jetpackcamera/core/camera/effects/LowLightBoostEffect.kt renamed to core/camera/low-light-playservices/src/main/java/com/google/jetpackcamera/core/camera/lowlight/playservices/PlayServicesLowLightBoostEffect.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.google.jetpackcamera.core.camera.effects
16+
package com.google.jetpackcamera.core.camera.lowlight.playservices
1717

1818
import android.annotation.SuppressLint
1919
import android.hardware.camera2.TotalCaptureResult
@@ -22,31 +22,27 @@ import androidx.annotation.RequiresApi
2222
import androidx.camera.core.CameraEffect
2323
import com.google.android.gms.cameralowlight.LowLightBoostClient
2424
import com.google.android.gms.cameralowlight.SceneDetectorCallback
25-
import com.google.jetpackcamera.core.camera.effects.processors.LowLightBoostSurfaceProcessor
2625
import kotlinx.coroutines.CoroutineScope
2726
import kotlinx.coroutines.flow.StateFlow
2827

29-
private const val TARGETS =
28+
private const val PLAY_SERVICES_EFFECT_TARGETS =
3029
CameraEffect.PREVIEW or CameraEffect.IMAGE_CAPTURE or CameraEffect.VIDEO_CAPTURE
3130

32-
/**
33-
* [CameraEffect] that applies Google Low Light Boost.
34-
*/
3531
@SuppressLint("RestrictedApi")
3632
@RequiresApi(Build.VERSION_CODES.R)
37-
class LowLightBoostEffect(
33+
internal class PlayServicesLowLightBoostEffect(
3834
cameraId: String,
3935
lowLightBoostClient: LowLightBoostClient,
4036
captureResults: StateFlow<TotalCaptureResult?>,
4137
coroutineScope: CoroutineScope,
4238
sceneDetectorCallback: SceneDetectorCallback? = null,
4339
onLowLightBoostErrorCallback: (Exception) -> Unit = {}
4440
) : CameraEffect(
45-
TARGETS,
41+
PLAY_SERVICES_EFFECT_TARGETS,
4642
OUTPUT_OPTION_ONE_FOR_ALL_TARGETS,
4743
TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION,
4844
Runnable::run,
49-
LowLightBoostSurfaceProcessor(
45+
PlayServicesLowLightBoostSurfaceProcessor(
5046
cameraId,
5147
lowLightBoostClient,
5248
captureResults,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (C) 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.jetpackcamera.core.camera.lowlight.playservices
17+
18+
import android.annotation.SuppressLint
19+
import android.content.Context
20+
import android.hardware.camera2.TotalCaptureResult
21+
import android.os.Build
22+
import androidx.annotation.RequiresApi
23+
import androidx.camera.core.CameraEffect
24+
import com.google.android.gms.cameralowlight.LowLightBoost
25+
import com.google.android.gms.cameralowlight.LowLightBoostSession
26+
import com.google.android.gms.cameralowlight.SceneDetectorCallback
27+
import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostEffectProvider
28+
import dagger.hilt.android.qualifiers.ApplicationContext
29+
import javax.inject.Inject
30+
import kotlinx.coroutines.CoroutineScope
31+
import kotlinx.coroutines.flow.StateFlow
32+
33+
class PlayServicesLowLightBoostEffectProvider @Inject constructor(
34+
@ApplicationContext private val context: Context
35+
) : LowLightBoostEffectProvider {
36+
@SuppressLint("RestrictedApi")
37+
@RequiresApi(Build.VERSION_CODES.R)
38+
override fun create(
39+
cameraId: String,
40+
captureResults: StateFlow<TotalCaptureResult?>,
41+
coroutineScope: CoroutineScope,
42+
onSceneBrightnessChanged: (Float) -> Unit,
43+
onLowLightBoostError: (Exception) -> Unit
44+
): CameraEffect {
45+
val sceneDetectorCallback = object : SceneDetectorCallback {
46+
override fun onSceneBrightnessChanged(
47+
session: LowLightBoostSession,
48+
boostStrength: Float
49+
) {
50+
onSceneBrightnessChanged(boostStrength)
51+
}
52+
}
53+
return PlayServicesLowLightBoostEffect(
54+
cameraId = cameraId,
55+
lowLightBoostClient = LowLightBoost.getClient(context),
56+
captureResults = captureResults,
57+
coroutineScope = coroutineScope,
58+
sceneDetectorCallback = sceneDetectorCallback,
59+
onLowLightBoostErrorCallback = onLowLightBoostError
60+
)
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (C) 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.jetpackcamera.core.camera.lowlight.playservices
17+
18+
import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostAvailabilityChecker
19+
import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostEffectProvider
20+
import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostFeatureKey
21+
import dagger.Module
22+
import dagger.Provides
23+
import dagger.hilt.InstallIn
24+
import dagger.hilt.components.SingletonComponent
25+
import dagger.multibindings.IntoSet
26+
import java.util.AbstractMap
27+
import javax.inject.Provider
28+
29+
object PlayServicesLowLightBoostFeatureKey : LowLightBoostFeatureKey
30+
31+
@Module
32+
@InstallIn(SingletonComponent::class)
33+
internal object PlayServicesLowLightBoostModule {
34+
@Provides
35+
@IntoSet
36+
fun provideAvailabilityCheckerEntry(
37+
impl: Provider<PlayServicesLowLightBoostAvailabilityChecker>
38+
): Map.Entry<
39+
LowLightBoostFeatureKey,
40+
@JvmSuppressWildcards Provider<LowLightBoostAvailabilityChecker>
41+
> =
42+
AbstractMap.SimpleImmutableEntry(
43+
PlayServicesLowLightBoostFeatureKey,
44+
Provider { impl.get() }
45+
)
46+
47+
@Provides
48+
@IntoSet
49+
fun provideEffectProviderEntry(
50+
impl: Provider<PlayServicesLowLightBoostEffectProvider>
51+
): Map.Entry<
52+
LowLightBoostFeatureKey,
53+
@JvmSuppressWildcards Provider<LowLightBoostEffectProvider>
54+
> =
55+
AbstractMap.SimpleImmutableEntry(
56+
PlayServicesLowLightBoostFeatureKey,
57+
Provider { impl.get() }
58+
)
59+
}

0 commit comments

Comments
 (0)