CameraX change from alpha-04 to beta-01 broke the code

Viewed 1104

I have added CameraX to my ongoing development app a while ago. I know it was in alpha but I was ready to make the change when beta or final release will be available.

So I started working on it today. I have updated from

implementation 'androidx.camera:camera-core:1.0.0-alpha04'
implementation 'androidx.camera:camera-camera2:1.0.0-alpha04'

to this:

implementation 'androidx.camera:camera-core:1.0.0-beta01'
implementation 'androidx.camera:camera-camera2:1.0.0-beta01'
implementation 'androidx.camera:camera-lifecycle:1.0.0-beta01'

My Previous Working Code (alpha-04):

class ScannerX : AppCompatActivity() {
private lateinit var context: Context

var isOtpAuthCode = true

private val immersiveFlagTimeout = 500L
private val flagsFullscreen = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

private var preview: Preview? = null
private var lensFacing = CameraX.LensFacing.BACK
private var imageAnalyzer: ImageAnalysis? = null

private lateinit var analyzerThread: HandlerThread

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_scanner_x)

    context = this

    btnCancel.setOnClickListener {
        finish()
    }

    analyzerThread = if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
        HandlerThread("BarcodeFirebaseAnalyzer").apply { start() }
    } else {
        HandlerThread("BarcodeZxingAnalyzer").apply { start() }
    }

    Dexter.withActivity(this)
        .withPermissions(Manifest.permission.CAMERA)
        .withListener(object : MultiplePermissionsListener {
            override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
                textureView.post {
                    val metrics = DisplayMetrics().also { textureView.display.getRealMetrics(it) }
                    val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels)

                    val previewConfig = PreviewConfig.Builder().apply {
                        setLensFacing(lensFacing)
                        // We request aspect ratio but no resolution to let CameraX optimize our use cases
                        setTargetAspectRatio(screenAspectRatio)
                        // Set initial target rotation, we will have to call this again if rotation changes
                        // during the lifecycle of this use case
                        setTargetRotation(textureView.display.rotation)
                    }.build()

                    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
                        setLensFacing(lensFacing)
                        // Use a worker thread for image analysis to prevent preview glitches
                        setCallbackHandler(Handler(analyzerThread.looper))
                        // In our analysis, we care more about the latest image than analyzing *every* image
                        setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
                        // Set initial target rotation, we will have to call this again if rotation changes
                        // during the lifecycle of this use case
                        setTargetRotation(textureView.display.rotation)
                    }.build()

                    preview = AutoFitPreviewBuilder.build(previewConfig, textureView)

                    imageAnalyzer = ImageAnalysis(analyzerConfig).apply {
                        analyzer = if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
                            BarcodeFirebaseAnalyzer { qrCode ->
                                if (isOtpAuthCode) {
                                    if (qrCode.startsWith("otpauth")) {
                                        toAddAuth(qrCode)
                                    }
                                } else {
                                    toAddAuth(qrCode)
                                }
                            }
                        } else {
                            BarcodeZxingAnalyzer { qrCode ->
                                if (isOtpAuthCode) {
                                    if (qrCode.startsWith("otpauth")) {
                                        toAddAuth(qrCode)
                                    }
                                } else {
                                    toAddAuth(qrCode)
                                }
                            }
                        }
                    }

                    // Apply declared configs to CameraX using the same lifecycle owner
                    CameraX.bindToLifecycle(this@ScannerX, preview, imageAnalyzer)
                }
            }

            override fun onPermissionRationaleShouldBeShown(permissions: MutableList<PermissionRequest>?, token: PermissionToken?) {
                //
            }
        }).check()
}

override fun onStart() {
    super.onStart()
    // Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
    // be trying to set app to immersive mode before it's ready and the flags do not stick
    textureView.postDelayed({
        textureView.systemUiVisibility = flagsFullscreen
    }, immersiveFlagTimeout)
}

override fun onDestroy() {
    analyzerThread.quit()
    super.onDestroy()
}

private fun toAddAuth(scannedCode: String) {
    if (CameraX.isBound(imageAnalyzer)) {
        CameraX.unbind(imageAnalyzer)
    }

    val intent = Intent()
    intent.putExtra("scanResult", scannedCode)
    setResult(RESULT_OK, intent)
    finish()
}

companion object {
    private const val RESULT_OK = 666
}
}

And the code I have changed is as follows (beta-01):

class ScannerX : AppCompatActivity() {
private lateinit var context: Context

var isOtpAuthCode = true

private val immersiveFlagTimeout = 500L
private val flagsFullscreen = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

private var preview: Preview? = null
private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA
private var imageAnalyzer: ImageAnalysis? = null

private lateinit var analysisExecutor: ExecutorService
private lateinit var processCameraProvider: ListenableFuture<ProcessCameraProvider>

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_scanner_x)

    context = this

    btnCancel.setOnClickListener {
        finish()
    }

    Dexter.withActivity(this)
        .withPermissions(Manifest.permission.CAMERA)
        .withListener(object : MultiplePermissionsListener {
            override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
                textureView.post {
                    analysisExecutor = Executors.newSingleThreadExecutor()
                    processCameraProvider = ProcessCameraProvider.getInstance(context)

                    preview = Preview.Builder()
                        .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                        .setTargetRotation(textureView.display.rotation)
                        .build()

                    imageAnalyzer = ImageAnalysis.Builder()
                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                        .setTargetRotation(textureView.display.rotation)
                        .build()

                    if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
                        imageAnalyzer?.apply {
                            setAnalyzer(analysisExecutor, BarcodeFirebaseAnalyzer { qrCode ->
                                if (isOtpAuthCode) {
                                    if (qrCode.startsWith("otpauth")) {
                                        toAddAuth(qrCode)
                                    }
                                } else {
                                    toAddAuth(qrCode)
                                }
                            })
                        }
                    } else {
                        imageAnalyzer?.apply {
                            setAnalyzer(analysisExecutor, BarcodeZxingAnalyzer { qrCode ->
                                if (isOtpAuthCode) {
                                    if (qrCode.startsWith("otpauth")) {
                                        toAddAuth(qrCode)
                                    }
                                } else {
                                    toAddAuth(qrCode)
                                }
                            })
                        }
                    }

                    processCameraProvider.get().bindToLifecycle(this@ScannerX, lensFacing, imageAnalyzer)
                }
            }

            override fun onPermissionRationaleShouldBeShown(permissions: MutableList<PermissionRequest>?, token: PermissionToken?) {
                //
            }
        }).check()
}

override fun onStart() {
    super.onStart()
    // Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
    // be trying to set app to immersive mode before it's ready and the flags do not stick
    textureView.postDelayed({
        textureView.systemUiVisibility = flagsFullscreen
    }, immersiveFlagTimeout)
}

override fun onDestroy() {
    if (!analysisExecutor.isShutdown) {
        analysisExecutor.shutdown()
    }

    super.onDestroy()
}

private fun toAddAuth(scannedCode: String) {
    /*if (CameraX.isBound(imageAnalyzer)) {
        CameraX.unbind(imageAnalyzer)
    }*/

    val intent = Intent()
    intent.putExtra("scanResult", scannedCode)
    setResult(RESULT_OK, intent)
    finish()
}

companion object {
    private const val RESULT_OK = 666
}
}

After I upgraded there were so many changes in library and now I cant make it work.

I also cant use Google Provided AutoFitPreview Class along with initial alpha release of this library. This was not necessary even with alpha04 since the only problem without this class was camera view little bit stretched out but scanning and analyzing worked properly.

/**
* Builder for [Preview] that takes in a [WeakReference] of the view finder and [PreviewConfig],
* then instantiates a [Preview] which automatically resizes and rotates reacting to config changes.
*/
class AutoFitPreviewBuilder private constructor(config: PreviewConfig, viewFinderRef: WeakReference<TextureView>) {

/** Public instance of preview use-case which can be used by consumers of this adapter */
val useCase: Preview

/** Internal variable used to keep track of the use case's output rotation */
private var bufferRotation: Int = 0

/** Internal variable used to keep track of the view's rotation */
private var viewFinderRotation: Int? = null

/** Internal variable used to keep track of the use-case's output dimension */
private var bufferDimens: Size = Size(0, 0)

/** Internal variable used to keep track of the view's dimension */
private var viewFinderDimens: Size = Size(0, 0)

/** Internal variable used to keep track of the view's display */
private var viewFinderDisplay: Int = -1

/** Internal reference of the [DisplayManager] */
private lateinit var displayManager: DisplayManager

/**
 * We need a display listener for orientation changes that do not trigger a configuration
 * change, for example if we choose to override config change in manifest or for 180-degree
 * orientation changes.
 */
private val displayListener = object : DisplayManager.DisplayListener {
    override fun onDisplayAdded(displayId: Int) = Unit
    override fun onDisplayRemoved(displayId: Int) = Unit
    override fun onDisplayChanged(displayId: Int) {
        val viewFinder = viewFinderRef.get() ?: return
        if (displayId == viewFinderDisplay) {
            val display = displayManager.getDisplay(displayId)
            val rotation = getDisplaySurfaceRotation(display)
            updateTransform(viewFinder, rotation, bufferDimens, viewFinderDimens)
        }
    }
}

init {
    // Make sure that the view finder reference is valid
    val viewFinder = viewFinderRef.get() ?:
    throw IllegalArgumentException("Invalid reference to view finder used")

    // Initialize the display and rotation from texture view information
    viewFinderDisplay = viewFinder.display.displayId
    viewFinderRotation = getDisplaySurfaceRotation(viewFinder.display) ?: 0

    // Initialize public use-case with the given config
    useCase = Preview(config)

    // Every time the view finder is updated, recompute layout
    useCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
        val viewFinderI = viewFinderRef.get() ?: return@OnPreviewOutputUpdateListener
        Log.d(TAG, "Preview output changed. " +
                "Size: ${it.textureSize}. Rotation: ${it.rotationDegrees}")

        // To update the SurfaceTexture, we have to remove it and re-add it
        val parent = viewFinderI.parent as ViewGroup
        parent.removeView(viewFinderI)
        parent.addView(viewFinderI, 0)

        // Update internal texture
        viewFinderI.surfaceTexture = it.surfaceTexture

        // Apply relevant transformations
        bufferRotation = it.rotationDegrees
        val rotation = getDisplaySurfaceRotation(viewFinderI.display)
        updateTransform(viewFinderI, rotation, it.textureSize, viewFinderDimens)
    }

    // Every time the provided texture view changes, recompute layout
    viewFinder.addOnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
        val viewFinderII = view as TextureView
        val newViewFinderDimens = Size(right - left, bottom - top)
        Log.d(TAG, "View finder layout changed. Size: $newViewFinderDimens")
        val rotation = getDisplaySurfaceRotation(viewFinderII.display)
        updateTransform(viewFinderII, rotation, bufferDimens, newViewFinderDimens)
    }

    // Every time the orientation of device changes, recompute layout
    // NOTE: This is unnecessary if we listen to display orientation changes in the camera
    //  fragment and call [Preview.setTargetRotation()] (like we do in this sample), which will
    //  trigger [Preview.OnPreviewOutputUpdateListener] with a new
    //  [PreviewOutput.rotationDegrees]. CameraX Preview use case will not rotate the frames for
    //  us, it will just tell us about the buffer rotation with respect to sensor orientation.
    //  In this sample, we ignore the buffer rotation and instead look at the view finder's
    //  rotation every time [updateTransform] is called, which gets triggered by
    //  [CameraFragment] display listener -- but the approach taken in this sample is not the
    //  only valid one.
    displayManager = viewFinder.context
        .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
    displayManager.registerDisplayListener(displayListener, null)

    // Remove the display listeners when the view is detached to avoid holding a reference to
    //  it outside of the Fragment that owns the view.
    // NOTE: Even though using a weak reference should take care of this, we still try to avoid
    //  unnecessary calls to the listener this way.
    viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
        override fun onViewAttachedToWindow(view: View?) =
            displayManager.registerDisplayListener(displayListener, null)
        override fun onViewDetachedFromWindow(view: View?) =
            displayManager.unregisterDisplayListener(displayListener)
    })
}

/** Helper function that fits a camera preview into the given [TextureView] */
private fun updateTransform(textureView: TextureView?, rotation: Int?, newBufferDimens: Size, newViewFinderDimens: Size) {
    // This should not happen anyway, but now the linter knows
    val textureViewI = textureView ?: return

    if (rotation == viewFinderRotation &&
        Objects.equals(newBufferDimens, bufferDimens) &&
        Objects.equals(newViewFinderDimens, viewFinderDimens)) {
        // Nothing has changed, no need to transform output again
        return
    }

    if (rotation == null) {
        // Invalid rotation - wait for valid inputs before setting matrix
        return
    } else {
        // Update internal field with new inputs
        viewFinderRotation = rotation
    }

    if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
        // Invalid buffer dimens - wait for valid inputs before setting matrix
        return
    } else {
        // Update internal field with new inputs
        bufferDimens = newBufferDimens
    }

    if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
        // Invalid view finder dimens - wait for valid inputs before setting matrix
        return
    } else {
        // Update internal field with new inputs
        viewFinderDimens = newViewFinderDimens
    }

    val matrix = Matrix()
    Log.d(TAG, "Applying output transformation.\n" +
            "View finder size: $viewFinderDimens.\n" +
            "Preview output size: $bufferDimens\n" +
            "View finder rotation: $viewFinderRotation\n" +
            "Preview output rotation: $bufferRotation")

    // Compute the center of the view finder
    val centerX = viewFinderDimens.width / 2f
    val centerY = viewFinderDimens.height / 2f

    // Correct preview output to account for display rotation
    matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)

    // Buffers are rotated relative to the device's 'natural' orientation: swap width and height
    val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()

    val scaledWidth: Int
    val scaledHeight: Int
    // Match longest sides together -- i.e. apply center-crop transformation
    if (viewFinderDimens.width > viewFinderDimens.height) {
        scaledHeight = viewFinderDimens.width
        scaledWidth = (viewFinderDimens.width * bufferRatio).roundToInt()
    } else {
        scaledHeight = viewFinderDimens.height
        scaledWidth = (viewFinderDimens.height * bufferRatio).roundToInt()
    }

    // Compute the relative scale value
    val xScale = scaledWidth / viewFinderDimens.width.toFloat()
    val yScale = scaledHeight / viewFinderDimens.height.toFloat()

    // Scale input buffers to fill the view finder
    matrix.preScale(xScale, yScale, centerX, centerY)

    // Finally, apply transformations to our TextureView
    textureViewI.setTransform(matrix)
}

companion object {
    private val TAG = AutoFitPreviewBuilder::class.java.simpleName

    /** Helper function that gets the rotation of a [Display] in degrees */
    fun getDisplaySurfaceRotation(display: Display?) = when(display?.rotation) {
        Surface.ROTATION_0 -> 0
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> null
    }

    /**
     * Main entry point for users of this class: instantiates the adapter and returns an instance
     * of [Preview] which automatically adjusts in size and rotation to compensate for
     * config changes.
     */
    fun build(config: PreviewConfig, viewFinder: TextureView) =
        AutoFitPreviewBuilder(config, WeakReference(viewFinder)).useCase
}
}

Please Help

0 Answers
Related