Adopt CameraX Feature Group API#522
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements CameraX feature groups to handle compatibility between settings such as HDR, frame rate, and stabilization. It introduces FeatureGroupHandler to filter system constraints and refactors CameraSession to use a SessionConfig containing required feature groups. The PR also updates the project to CameraX 1.6.0-SNAPSHOT and compile SDK 36. Feedback was provided to ensure manual Job cleanup via try-finally and to restrict the visibility of new components to internal as per the style guide.
| val job = Job() | ||
|
|
||
| val sessionConfig = with( | ||
| defaultCameraSessionContext.copy( | ||
| transientSettings = MutableStateFlow(transientSettings).asStateFlow() | ||
| ) | ||
| ) { | ||
| val videoCaptureUseCase = | ||
| createVideoUseCase( | ||
| cameraInfo, | ||
| cameraAppSettings.aspectRatio, | ||
| cameraAppSettings.captureMode, | ||
| backgroundDispatcher, | ||
| cameraAppSettings.targetFrameRate.takeIfNoFeatureGroup(sessionSettings), | ||
| cameraAppSettings.stabilizationMode.takeIfNoFeatureGroup(sessionSettings), | ||
| cameraAppSettings.dynamicRange.takeIfNoFeatureGroup(sessionSettings), | ||
| cameraAppSettings.videoQuality.takeIfNoFeatureGroup(sessionSettings) | ||
| ) | ||
|
|
||
| createSessionConfig( | ||
| cameraConstraints = cameraConstraints, | ||
| initialTransientSettings = transientSettings, | ||
| videoCaptureUseCase = videoCaptureUseCase, | ||
| sessionSettings = sessionSettings, | ||
| sessionScope = CoroutineScope(defaultDispatcher + job) | ||
| ) | ||
| } | ||
|
|
||
| return cameraInfo.isSessionConfigSupported(sessionConfig).apply { | ||
| job.cancel() | ||
| } |
There was a problem hiding this comment.
The manual Job created here should be cancelled within a finally block to ensure it is always cleaned up, even if isSessionConfigSupported throws an exception or the calling coroutine is cancelled. This prevents potential resource leaks from background tasks (such as camera effects) launched within the temporary sessionScope.
Additionally, using try-finally ensures that the job is cancelled immediately upon completion or failure of the capability check.
val job = Job()
return try {
val sessionConfig = with(
defaultCameraSessionContext.copy(
transientSettings = MutableStateFlow(transientSettings).asStateFlow()
)
) {
val videoCaptureUseCase =
createVideoUseCase(
cameraInfo,
cameraAppSettings.aspectRatio,
cameraAppSettings.captureMode,
backgroundDispatcher,
cameraAppSettings.targetFrameRate.takeIfNoFeatureGroup(sessionSettings),
cameraAppSettings.stabilizationMode.takeIfNoFeatureGroup(sessionSettings),
cameraAppSettings.dynamicRange.takeIfNoFeatureGroup(sessionSettings),
cameraAppSettings.videoQuality.takeIfNoFeatureGroup(sessionSettings)
)
createSessionConfig(
cameraConstraints = cameraConstraints,
initialTransientSettings = transientSettings,
videoCaptureUseCase = videoCaptureUseCase,
sessionSettings = sessionSettings,
sessionScope = CoroutineScope(defaultDispatcher + job)
)
}
cameraInfo.isSessionConfigSupported(sessionConfig)
} finally {
job.cancel()
}| * This allows internal JCA models like [DynamicRange] and [VideoQuality] to be mapped to CameraX | ||
| * [GroupableFeature]s for compatibility checking. | ||
| */ | ||
| sealed interface FeatureGroupability<out T> { |
There was a problem hiding this comment.
This interface and its associated extension functions (lines 97, 109, 124, 138, 156) are only used within the core:camera module. Following the repository style guide (rule 22), they should use the most restrictive visibility modifier possible, which is internal in this case.
| sealed interface FeatureGroupability<out T> { | |
| internal sealed interface FeatureGroupability<out T> { |
References
- Ensure all new functions, properties, and classes use the most restrictive visibility modifier possible (e.g., private, internal) while still allowing necessary access. (link)
| * | ||
| * This allows the dynamic range to be used in CameraX feature group compatibility checks. | ||
| */ | ||
| fun DynamicRange.toFeatureGroupability(): FeatureGroupability<DynamicRange> { |
There was a problem hiding this comment.
As noted previously, these extension functions should be marked as internal to adhere to the repository style guide regarding proper visibility modifiers.
| fun DynamicRange.toFeatureGroupability(): FeatureGroupability<DynamicRange> { | |
| internal fun DynamicRange.toFeatureGroupability(): FeatureGroupability<DynamicRange> { |
References
- Ensure all new functions, properties, and classes use the most restrictive visibility modifier possible (e.g., private, internal) while still allowing necessary access. (link)
Description
This PR adopts the new CameraX Feature Group API to validate feature combinations and dynamically update system constraints. This ensures that the app only allows supported combinations of settings (e.g., HDR, FPS, Stabilization), preventing runtime failures on devices that do not support specific combinations (like HDR + 60 FPS on Pixel 9).
Key Changes
1.6.0-SNAPSHOTto access the stable Feature Groups API.FeatureGroupDatato map JCA internal models (DynamicRange,VideoQuality,StabilizationMode, etc.) to CameraX'sGroupableFeature.updateSystemConstraintsByFeatureGroupsinCameraXCameraSystem. This method asynchronously validates the current session settings against the device capabilities and filters out unsupported options for other settings (e.g., if 60 FPS is selected, it might disable HDR if they can't be used together).UseCaseGrouptoSessionConfigfor single camera session creation to align with the new API usage and allow forisSessionConfigSupportedchecks.Bug Fixes & Enhancements
Known Limitations & Future Work
System constraints not updating for multiple updates from setting screen
The system constraints are currently updated only when the camera session is initialized or re-created (e.g., upon returning to the preview screen). This means that if a user sets feature options (like UHD and 60 FPS) directly from the settings menu without navigating back to the preview screen, the system constraints is not re-evaluated.
Consequently, it allows users to select a combination of features that are not mutually supported by the device, which can lead to issues when they eventually navigate back to the preview screen.
TODO: Addressing this limitation requires further architectural refactoring to trigger constraint updates dynamically upon any individual setting change. Or, the constraints repository should be updated with the info of all possible constraints at initialization, rather than only the settings state navigable from the current settings state.
Features incompatible with CameraX Feature Group API
CameraX depends on the Camera2 feature combination query (FCQ) API which guarantees support only a limited set of features. So, the scenarios using features incompatible with the FCQ API do not get much benefits from this PR.
Asynchronous system constraints update
System constraints are updated asynchronously, creating a window where a user might enable an unsupported combination of features before the update completes.
Future Work: This could be addressed by waiting for the constraints update to finish and restricting setting changes until it is complete.
Tests
FeatureGroupDataTest(unit tests) to verify mapping logic between JCA models and CameraXGroupableFeature.setMultipleFeatures_systemConstraintsUpdatedAndFeaturesSetIfSupportedtoCameraXCameraSystemTest(integration test) to verify that the camera system dynamically updates constraints and sets features correctly based on CameraX feature group compatibility.Additional Context
Related Issues