package cash.z.ecc.android.ui.scan import android.net.UrlQuerySanitizer import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowMetricsCalculator import cash.z.ecc.android.R import cash.z.ecc.android.databinding.FragmentScanBinding import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.sdk.ext.convertZecToZatoshi import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.send.SendViewModel import kotlinx.coroutines.launch import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import kotlin.math.abs class ScanFragment : BaseFragment() { private var _fragmentCameraBinding: FragmentScanBinding? = null private val fragmentCameraBinding get() = _fragmentCameraBinding!! private var displayId: Int = -1 private var lensFacing: Int = CameraSelector.LENS_FACING_BACK private var preview: Preview? = null private var imageCapture: ImageCapture? = null private var imageAnalyzer: ImageAnalysis? = null private var camera: Camera? = null private var cameraProvider: ProcessCameraProvider? = null private val viewModel: ScanViewModel by viewModels() private val sendViewModel: SendViewModel by activityViewModels() private lateinit var windowManager: WindowInfoTracker /** Blocking camera operations are performed using this executor */ private lateinit var cameraExecutor: ExecutorService override fun inflate(inflater: LayoutInflater): FragmentScanBinding = FragmentScanBinding.inflate(inflater) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() //Initialize WindowManager to retrieve display metrics windowManager = WindowInfoTracker.getOrCreate(view.context) // Wait for the views to be properly laid out fragmentCameraBinding.viewFinder.post { // Keep track of the display in which this view is attached displayId = fragmentCameraBinding.viewFinder.display.displayId // Set up the camera and its use cases setUpCamera() } // Initialize back button _fragmentCameraBinding?.backButtonHitArea?.onClickNavBack() } override fun onDestroyView() { _fragmentCameraBinding = null super.onDestroyView() // Shut down our background executor cameraExecutor.shutdown() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _fragmentCameraBinding = FragmentScanBinding.inflate(inflater, container, false) return fragmentCameraBinding.root } /** Initialize CameraX, and prepare to bind the camera use cases */ private fun setUpCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) cameraProviderFuture.addListener(Runnable { // CameraProvider cameraProvider = cameraProviderFuture.get() // Select lensFacing depending on the available cameras lensFacing = when { hasBackCamera() -> CameraSelector.LENS_FACING_BACK hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT else -> throw IllegalStateException("Back and front camera are unavailable") } // Build and bind the camera use cases bindCameraUseCases() }, ContextCompat.getMainExecutor(requireContext())) } /** Declare and bind preview, capture and analysis use cases */ private fun bindCameraUseCases() { // Get screen metrics used to setup camera for full screen resolution val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()) Log.d(TAG, "Screen metrics: ${windowMetrics.bounds.width()} x ${windowMetrics.bounds.height()}") val screenAspectRatio = aspectRatio(windowMetrics.bounds.width(), windowMetrics.bounds.height()) Log.d(TAG, "Preview aspect ratio: $screenAspectRatio") val rotation = fragmentCameraBinding.viewFinder.display.rotation Log.d(TAG, "Rotation: $rotation") // CameraProvider val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.") // CameraSelector val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() // Preview preview = Preview.Builder() // We request aspect ratio but no resolution .setTargetAspectRatio(screenAspectRatio) // Set initial target rotation .setTargetRotation(rotation) .build() // ImageCapture imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // We request aspect ratio but no resolution to match preview config, but letting // CameraX optimize for whatever specific resolution best fits 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(rotation) .build() // ImageAnalysis imageAnalyzer = ImageAnalysis.Builder() // We request aspect ratio but no resolution .setTargetAspectRatio(screenAspectRatio) // Set initial target rotation, we will have to call this again if rotation changes // during the lifecycle of this use case .setTargetRotation(rotation) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() // The analyzer can then be assigned to the instance .also { it.setAnalyzer(cameraExecutor, QrAnalyzer { q, i -> onQrScanned(q, i) } ) } // Must unbind the use-cases before rebinding them cameraProvider.unbindAll() if (camera != null) { // Must remove observers from the previous camera instance removeCameraStateObservers(camera!!.cameraInfo) } try { // A variable number of use-cases can be passed here - // camera provides access to CameraControl & CameraInfo camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture, imageAnalyzer) // Attach the viewfinder's surface provider to preview use case preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider) observeCameraState(camera?.cameraInfo!!) } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } } private fun removeCameraStateObservers(cameraInfo: CameraInfo) { cameraInfo.cameraState.removeObservers(viewLifecycleOwner) } private fun observeCameraState(cameraInfo: CameraInfo) { cameraInfo.cameraState.observe(viewLifecycleOwner) { cameraState -> run { when (cameraState.type) { CameraState.Type.PENDING_OPEN -> { // Ask the user to close other camera apps Toast.makeText(context, "CameraState: Pending Open", Toast.LENGTH_SHORT).show() } CameraState.Type.OPENING -> { // Show the Camera UI Toast.makeText(context, "CameraState: Opening", Toast.LENGTH_SHORT).show() } CameraState.Type.OPEN -> { // Setup Camera resources and begin processing Toast.makeText(context, "CameraState: Open", Toast.LENGTH_SHORT).show() } CameraState.Type.CLOSING -> { // Close camera UI Toast.makeText(context, "CameraState: Closing", Toast.LENGTH_SHORT).show() } CameraState.Type.CLOSED -> { // Free camera resources Toast.makeText(context, "CameraState: Closed", Toast.LENGTH_SHORT).show() } } } cameraState.error?.let { error -> when (error.code) { // Open errors CameraState.ERROR_STREAM_CONFIG -> { // Make sure to setup the use cases properly Toast.makeText(context, "Stream config error", Toast.LENGTH_SHORT).show() } // Opening errors CameraState.ERROR_CAMERA_IN_USE -> { // Close the camera or ask user to close another camera app that's using the // camera Toast.makeText(context, "Camera in use", Toast.LENGTH_SHORT).show() } CameraState.ERROR_MAX_CAMERAS_IN_USE -> { // Close another open camera in the app, or ask the user to close another // camera app that's using the camera Toast.makeText(context, "Max cameras in use", Toast.LENGTH_SHORT).show() } CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> { Toast.makeText(context, "Other recoverable error", Toast.LENGTH_SHORT).show() } // Closing errors CameraState.ERROR_CAMERA_DISABLED -> { // Ask the user to enable the device's cameras Toast.makeText(context, "Camera disabled", Toast.LENGTH_SHORT).show() } CameraState.ERROR_CAMERA_FATAL_ERROR -> { // Ask the user to reboot the device to restore camera function Toast.makeText(context, "Fatal error", Toast.LENGTH_SHORT).show() } // Closed errors CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> { // Ask the user to disable the "Do Not Disturb" mode, then reopen the camera Toast.makeText(context, "Do not disturb mode enabled", Toast.LENGTH_SHORT).show() } } } } } /** * [androidx.camera.core.ImageAnalysis.Builder] requires enum value of * [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9. * * Detecting the most suitable ratio for dimensions provided in @params by counting absolute * of preview ratio to one of the provided values. * * @param width - preview width * @param height - preview height * @return suitable aspect ratio */ private fun aspectRatio(width: Int, height: Int): Int { val previewRatio = width.toDouble() / height Log.d(TAG, "previewRatio: $previewRatio") if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { return AspectRatio.RATIO_4_3 } return AspectRatio.RATIO_16_9 } /** Returns true if the device has an available back camera. False otherwise */ private fun hasBackCamera(): Boolean { return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false } /** Returns true if the device has an available front camera. False otherwise */ private fun hasFrontCamera(): Boolean { return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false } companion object { private const val TAG = "SilentDragon" private const val RATIO_4_3_VALUE = 4.0 / 3.0 private const val RATIO_16_9_VALUE = 16.0 / 9.0 } private fun onQrScanned(qrContent: String, image: ImageProxy) { resumedScope.launch { val address = viewModel.parse(qrContent) val sanitizer = UrlQuerySanitizer(qrContent) val amt = sanitizer.getValue("amt") val memo = sanitizer.getValue("memo") /* Log.d(TAG, "qrContent: $qrContent") Log.d(TAG, "address: $address") Log.d(TAG, "amt: $amt") Log.d(TAG, "memo: $memo") */ if (address == null) { val network = viewModel.networkName _fragmentCameraBinding?.textScanError?.text = getString(R.string.scan_invalid_address, network, qrContent) image.close() } else { /* continue scanning*/ _fragmentCameraBinding?.textScanError?.text = "" sendViewModel.toAddress = address if(amt!=null){ sendViewModel.zatoshiAmount = amt.safelyConvertToBigDecimal().convertZecToZatoshi() } if(memo!=null){ sendViewModel.memo = memo } mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send) } } } }