Hush lite wallet for Android
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
14 KiB

package cash.z.ecc.android.ui.scan
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.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
import kotlin.math.max
import kotlin.math.min
class ScanFragment : BaseFragment<FragmentScanBinding>() {
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 parsed = viewModel.parse(qrContent)
if (parsed == 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 = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
}
}
}
}