@@ -67,6 +67,8 @@ fun CodeScanner(
6767 onPreviewStateChanged : (Boolean ) -> Unit ,
6868 onCodeScanned : (CodeScanResult ) -> Unit ,
6969 onError : (Throwable ) -> Unit = { },
70+ // Called when the device has no back camera and the scanner bound the front one instead.
71+ onFrontCameraFallback : () -> Unit = { },
7072 // Surfaces the underlying PreviewView so callers can snapshot the live feed (e.g. to render a
7173 // blurred camera backdrop behind an overlay). Non-null while mounted, null on dispose.
7274 onPreviewViewChanged : (PreviewView ? ) -> Unit = { },
@@ -89,13 +91,6 @@ fun CodeScanner(
8991 Preview .Builder ().build().apply { surfaceProvider = previewView.surfaceProvider }
9092 }
9193
92- val cameraSelector = remember {
93- val lensFacing = CameraSelector .LENS_FACING_BACK
94- CameraSelector .Builder ()
95- .requireLensFacing(lensFacing)
96- .build()
97- }
98-
9994 // Ask for the analysis stream through ResolutionSelector rather than the deprecated
10095 // setTargetResolution. The old API is only a hint, and on a 4:3 sensor it resolves a 1920x1080
10196 // request down to a 720x720 square -- which costs range twice over. A square crop keeps the
@@ -150,11 +145,11 @@ fun CodeScanner(
150145 cameraProvider.bindToLifecycleSafely(
151146 context,
152147 lifecycleOwner,
153- cameraSelector,
154148 preview,
155149 imageAnalysis
156150 ).onSuccess {
157151 camera = it
152+ if (it.cameraInfo.lensFacing == CameraSelector .LENS_FACING_FRONT ) onFrontCameraFallback()
158153 }.onFailure { onError(it) }
159154 } else {
160155 cameraProvider.unbindAll()
@@ -187,11 +182,13 @@ fun CodeScanner(
187182 cameraProvider.bindToLifecycleSafely(
188183 context,
189184 lifecycleOwner,
190- cameraSelector,
191185 preview,
192186 imageAnalysis
193187 ).onSuccess {
194188 camera = it
189+ if (it.cameraInfo.lensFacing == CameraSelector .LENS_FACING_FRONT ) {
190+ onFrontCameraFallback()
191+ }
195192 }.onFailure { onError(it) }
196193 }
197194 }
@@ -289,16 +286,22 @@ private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
289286private suspend fun ProcessCameraProvider.bindToLifecycleSafely (
290287 context : Context ,
291288 lifecycleOwner : LifecycleOwner ,
292- cameraSelector : CameraSelector ,
293289 preview : Preview ,
294290 imageAnalysis : ImageAnalysis ,
295291 retries : Int = 3,
296292): Result <Camera > {
297293 val hasCamera = context.packageManager.hasSystemFeature(PackageManager .FEATURE_CAMERA_ANY )
298294 if (! hasCamera) {
299- return Result .failure(IllegalStateException ( " No camera available on this device " ))
295+ return Result .failure(NoCamerasAvailableException ( ))
300296 }
301297
298+ // Some devices expose no back camera to CameraX -- an Infinix X688B lists a single camera and
299+ // reports it as front facing -- so requiring LENS_FACING_BACK fails every bind there. Prefer
300+ // the back camera and fall back to whatever the device has; a code held up to a front camera
301+ // still scans.
302+ val cameraSelector = pickScannerCamera(availableCameraInfos) { it.lensFacing }?.cameraSelector
303+ ? : return Result .failure(NoCamerasAvailableException ())
304+
302305 return runCatching {
303306 bindWithRetry(retries) {
304307 bindToLifecycle(
@@ -319,11 +322,23 @@ suspend fun bindWithRetry(
319322 try {
320323 return bind()
321324 } catch (e: Exception ) {
322- if (attempt == retries - 1 ) throw e
325+ // CameraX throws IllegalArgumentException when no camera matches the selector or the
326+ // use cases can't be combined. Neither changes between attempts, so retrying only
327+ // delays the failure.
328+ if (e is IllegalArgumentException || attempt == retries - 1 ) throw e
323329 delay(1000 )
324330 }
325331 }
326332 throw NoCamerasAvailableException ()
327333}
328334
335+ /* *
336+ * The back camera when there is one, otherwise the first camera the device exposes, or null when
337+ * it exposes none.
338+ */
339+ internal fun <T > pickScannerCamera (cameras : List <T >, lensFacing : (T ) -> Int ): T ? {
340+ return cameras.firstOrNull { lensFacing(it) == CameraSelector .LENS_FACING_BACK }
341+ ? : cameras.firstOrNull()
342+ }
343+
329344class NoCamerasAvailableException : Throwable ()
0 commit comments