QR scanning fixes
This commit is contained in:
@@ -143,11 +143,16 @@ dependencies {
|
|||||||
implementation Deps.AndroidX.LEGACY
|
implementation Deps.AndroidX.LEGACY
|
||||||
implementation Deps.AndroidX.PAGING
|
implementation Deps.AndroidX.PAGING
|
||||||
implementation Deps.AndroidX.RECYCLER
|
implementation Deps.AndroidX.RECYCLER
|
||||||
implementation Deps.AndroidX.CameraX.CAMERA2
|
|
||||||
implementation Deps.AndroidX.CameraX.CORE
|
def camerax_version = "1.2.0-rc01"
|
||||||
implementation Deps.AndroidX.CameraX.LIFECYCLE
|
implementation "androidx.camera:camera-core:${camerax_version}"
|
||||||
implementation Deps.AndroidX.CameraX.View.EXT
|
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||||
implementation Deps.AndroidX.CameraX.View.VIEW
|
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||||
|
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||||
|
|
||||||
|
//WindowManager
|
||||||
|
implementation "androidx.window:window:1.1.0-alpha01"
|
||||||
|
|
||||||
implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX
|
implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX
|
||||||
implementation Deps.AndroidX.Navigation.FRAGMENT_KTX
|
implementation Deps.AndroidX.Navigation.FRAGMENT_KTX
|
||||||
implementation Deps.AndroidX.Navigation.UI_KTX
|
implementation Deps.AndroidX.Navigation.UI_KTX
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
@@ -11,8 +11,12 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/ZcashTheme">
|
android:theme="@style/ZcashTheme"
|
||||||
<activity android:name=".ui.MainActivity" android:screenOrientation="portrait">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".ui.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@@ -1,216 +1,341 @@
|
|||||||
package cash.z.ecc.android.ui.scan
|
package cash.z.ecc.android.ui.scan
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.camera.core.*
|
import androidx.camera.core.*
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
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.R
|
||||||
import cash.z.ecc.android.databinding.FragmentScanBinding
|
import cash.z.ecc.android.databinding.FragmentScanBinding
|
||||||
import cash.z.ecc.android.ext.onClickNavBack
|
import cash.z.ecc.android.ext.onClickNavBack
|
||||||
import cash.z.ecc.android.feedback.Report
|
|
||||||
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
|
|
||||||
import cash.z.ecc.android.ui.base.BaseFragment
|
import cash.z.ecc.android.ui.base.BaseFragment
|
||||||
import cash.z.ecc.android.ui.send.SendViewModel
|
import cash.z.ecc.android.ui.send.SendViewModel
|
||||||
import cash.z.ecc.android.util.twig
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
class ScanFragment : BaseFragment<FragmentScanBinding>() {
|
class ScanFragment : BaseFragment<FragmentScanBinding>() {
|
||||||
|
|
||||||
override val screen = Report.Screen.SCAN
|
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 viewModel: ScanViewModel by viewModels()
|
||||||
|
|
||||||
private val sendViewModel: SendViewModel by activityViewModels()
|
private val sendViewModel: SendViewModel by activityViewModels()
|
||||||
|
private lateinit var windowManager: WindowInfoTracker
|
||||||
|
|
||||||
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
/** Blocking camera operations are performed using this executor */
|
||||||
|
private lateinit var cameraExecutor: ExecutorService
|
||||||
private var cameraExecutor: ExecutorService? = null
|
|
||||||
|
|
||||||
override fun inflate(inflater: LayoutInflater): FragmentScanBinding =
|
override fun inflate(inflater: LayoutInflater): FragmentScanBinding =
|
||||||
FragmentScanBinding.inflate(inflater)
|
FragmentScanBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
if (cameraExecutor != null) cameraExecutor?.shutdown()
|
|
||||||
|
// Initialize our background executor
|
||||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
binding.backButtonHitArea.onClickNavBack() { tapped(SCAN_BACK) }
|
//Initialize WindowManager to retrieve display metrics
|
||||||
}
|
windowManager = WindowInfoTracker.getOrCreate(view.context)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
// Wait for the views to be properly laid out
|
||||||
super.onCreate(savedInstanceState)
|
fragmentCameraBinding.viewFinder.post {
|
||||||
if (!allPermissionsGranted()) getRuntimePermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
// Keep track of the display in which this view is attached
|
||||||
super.onAttach(context)
|
displayId = fragmentCameraBinding.viewFinder.display.displayId
|
||||||
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
|
||||||
cameraProviderFuture.addListener(
|
// Set up the camera and its use cases
|
||||||
Runnable {
|
setUpCamera()
|
||||||
bindPreview(cameraProviderFuture.get())
|
}
|
||||||
},
|
|
||||||
ContextCompat.getMainExecutor(context)
|
// Initialize back button
|
||||||
)
|
_fragmentCameraBinding?.backButtonHitArea?.onClickNavBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
_fragmentCameraBinding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
cameraExecutor?.shutdown()
|
|
||||||
cameraExecutor = null
|
// Shut down our background executor
|
||||||
|
cameraExecutor.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindPreview(cameraProvider: ProcessCameraProvider) {
|
override fun onCreateView(
|
||||||
// Most of the code here is adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt
|
inflater: LayoutInflater,
|
||||||
// it's worth keeping tabs on that implementation because they keep making breaking changes to these APIs!
|
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
|
// Get screen metrics used to setup camera for full screen resolution
|
||||||
val metrics = DisplayMetrics().also { binding.preview.display.getRealMetrics(it) }
|
/*
|
||||||
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
|
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()).bounds
|
||||||
val rotation = binding.preview.display.rotation
|
Log.d("SilentDragon", "Screen metrics: ${metrics.width()} x ${metrics.height()}")
|
||||||
|
val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
|
||||||
|
*/
|
||||||
|
|
||||||
val preview =
|
// Hardcode to square for now otherwise scanning doesn't work
|
||||||
Preview.Builder().setTargetName("Preview").setTargetAspectRatio(screenAspectRatio)
|
val screenAspectRatio = aspectRatio(1, 1)
|
||||||
.setTargetRotation(rotation).build()
|
Log.d("SilentDragon", "Preview aspect ratio: $screenAspectRatio")
|
||||||
|
|
||||||
val cameraSelector = CameraSelector.Builder()
|
val rotation = fragmentCameraBinding.viewFinder.display.rotation
|
||||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
|
||||||
|
// 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()
|
.build()
|
||||||
|
|
||||||
val imageAnalysis = ImageAnalysis.Builder().setTargetAspectRatio(screenAspectRatio)
|
// 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)
|
.setTargetRotation(rotation)
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
imageAnalysis.setAnalyzer(
|
|
||||||
cameraExecutor!!,
|
// The analyzer can then be assigned to the instance
|
||||||
QrAnalyzer { q, i ->
|
.also {
|
||||||
onQrScanned(q, i)
|
it.setAnalyzer(cameraExecutor,
|
||||||
|
QrAnalyzer { q, i ->
|
||||||
|
onQrScanned(q, i)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// Must unbind the use-cases before rebinding them
|
// Must unbind the use-cases before rebinding them
|
||||||
cameraProvider.unbindAll()
|
cameraProvider.unbindAll()
|
||||||
|
|
||||||
|
if (camera != null) {
|
||||||
|
// Must remove observers from the previous camera instance
|
||||||
|
removeCameraStateObservers(camera!!.cameraInfo)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
|
// A variable number of use-cases can be passed here -
|
||||||
preview.setSurfaceProvider(binding.preview.surfaceProvider)
|
// camera provides access to CameraControl & CameraInfo
|
||||||
} catch (t: Throwable) {
|
camera = cameraProvider.bindToLifecycle(
|
||||||
// TODO: consider bubbling this up to the user
|
this, cameraSelector, preview, imageCapture, imageAnalyzer)
|
||||||
mainActivity?.feedback?.report(t)
|
|
||||||
twig("Error while opening the camera: $t")
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350
|
* [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 {
|
private fun aspectRatio(width: Int, height: Int): Int {
|
||||||
val previewRatio = kotlin.math.max(width, height).toDouble() / kotlin.math.min(
|
val previewRatio = max(width, height).toDouble() / min(width, height)
|
||||||
width,
|
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
|
||||||
height
|
|
||||||
)
|
|
||||||
if (kotlin.math.abs(previewRatio - (4.0 / 3.0))
|
|
||||||
<= kotlin.math.abs(previewRatio - (16.0 / 9.0))
|
|
||||||
) {
|
|
||||||
return AspectRatio.RATIO_4_3
|
return AspectRatio.RATIO_4_3
|
||||||
}
|
}
|
||||||
return AspectRatio.RATIO_16_9
|
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) {
|
private fun onQrScanned(qrContent: String, image: ImageProxy) {
|
||||||
|
//Log.d("SilentDragon", "QR scanned: $qrContent")
|
||||||
resumedScope.launch {
|
resumedScope.launch {
|
||||||
val parsed = viewModel.parse(qrContent)
|
val parsed = viewModel.parse(qrContent)
|
||||||
if (parsed == null) {
|
if (parsed == null) {
|
||||||
val network = viewModel.networkName
|
val network = viewModel.networkName
|
||||||
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent)
|
_fragmentCameraBinding?.textScanError?.text =
|
||||||
|
getString(R.string.scan_invalid_address, network, qrContent)
|
||||||
image.close()
|
image.close()
|
||||||
} else { /* continue scanning*/
|
} else { /* continue scanning*/
|
||||||
binding.textScanError.text = ""
|
_fragmentCameraBinding?.textScanError?.text = ""
|
||||||
sendViewModel.toAddress = parsed
|
sendViewModel.toAddress = parsed
|
||||||
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
|
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun updateOverlay(detectedObjects: DetectedObjects) {
|
|
||||||
// if (detectedObjects.objects.isEmpty()) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// overlay.setSize(detectedObjects.imageWidth, detectedObjects.imageHeight)
|
|
||||||
// val list = mutableListOf<BoxData>()
|
|
||||||
// for (obj in detectedObjects.objects) {
|
|
||||||
// val box = obj.boundingBox
|
|
||||||
// val name = "${categoryNames[obj.classificationCategory]}"
|
|
||||||
// val confidence =
|
|
||||||
// if (obj.classificationCategory != FirebaseVisionObject.CATEGORY_UNKNOWN) {
|
|
||||||
// val confidence: Int = obj.classificationConfidence!!.times(100).toInt()
|
|
||||||
// "$confidence%"
|
|
||||||
// } else {
|
|
||||||
// ""
|
|
||||||
// }
|
|
||||||
// list.add(BoxData("$name $confidence", box))
|
|
||||||
// }
|
|
||||||
// overlay.set(list)
|
|
||||||
// }
|
|
||||||
|
|
||||||
//
|
|
||||||
// Permissions
|
|
||||||
//
|
|
||||||
|
|
||||||
private val requiredPermissions: Array<String?>
|
|
||||||
get() {
|
|
||||||
return try {
|
|
||||||
val info = mainActivity?.packageManager
|
|
||||||
?.getPackageInfo(mainActivity?.packageName ?: "", PackageManager.GET_PERMISSIONS)
|
|
||||||
val ps = info?.requestedPermissions
|
|
||||||
if (ps != null && ps.isNotEmpty()) {
|
|
||||||
ps
|
|
||||||
} else {
|
|
||||||
arrayOfNulls(0)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
arrayOfNulls(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun allPermissionsGranted(): Boolean {
|
|
||||||
for (permission in requiredPermissions) {
|
|
||||||
if (!isPermissionGranted(mainActivity!!, permission!!)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRuntimePermissions() {
|
|
||||||
val allNeededPermissions = arrayListOf<String>()
|
|
||||||
for (permission in requiredPermissions) {
|
|
||||||
if (!isPermissionGranted(mainActivity!!, permission!!)) {
|
|
||||||
allNeededPermissions.add(permission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allNeededPermissions.isNotEmpty()) {
|
|
||||||
requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CAMERA_PERMISSION_REQUEST = 1002
|
|
||||||
|
|
||||||
private fun isPermissionGranted(context: Context, permission: String): Boolean {
|
|
||||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ class ScanViewModel : ViewModel() {
|
|||||||
val networkName get() = synchronizer.network.networkName
|
val networkName get() = synchronizer.network.networkName
|
||||||
|
|
||||||
suspend fun parse(qrCode: String): String? {
|
suspend fun parse(qrCode: String): String? {
|
||||||
// temporary parse code to allow both plain addresses and those that start with zcash:
|
// temporary parse code to allow both plain addresses and those that start with hush:
|
||||||
// TODO: replace with more robust ZIP-321 handling of QR codes
|
// TODO: replace with more robust ZIP-321 handling of QR codes
|
||||||
val address = if (qrCode.startsWith("zcash:")) {
|
val address = if (qrCode.startsWith("hush:")) {
|
||||||
qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length)
|
qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length)
|
||||||
} else {
|
} else {
|
||||||
qrCode
|
qrCode
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
tools:background="@color/zcashRed" />
|
tools:background="@color/zcashRed" />
|
||||||
|
|
||||||
<androidx.camera.view.PreviewView
|
<androidx.camera.view.PreviewView
|
||||||
android:id="@+id/preview"
|
android:id="@+id/viewFinder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ object Deps {
|
|||||||
const val kotlinVersion = "1.7.20"
|
const val kotlinVersion = "1.7.20"
|
||||||
const val navigationVersion = "2.5.2"
|
const val navigationVersion = "2.5.2"
|
||||||
|
|
||||||
const val compileSdkVersion = 31
|
const val compileSdkVersion = 33
|
||||||
const val minSdkVersion = 21
|
const val minSdkVersion = 21
|
||||||
const val targetSdkVersion = 30
|
const val targetSdkVersion = 33
|
||||||
const val versionName = "1.0.0"
|
const val versionName = "1.0.0"
|
||||||
const val versionCode = 1_00_00 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
|
const val versionCode = 1_00_00 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
|
||||||
const val packageName = "hush.android"
|
const val packageName = "hush.android"
|
||||||
@@ -25,15 +25,7 @@ object Deps {
|
|||||||
const val MULTIDEX = "androidx.multidex:multidex:2.0.1"
|
const val MULTIDEX = "androidx.multidex:multidex:2.0.1"
|
||||||
const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2"
|
const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||||
const val RECYCLER = "androidx.recyclerview:recyclerview:1.2.1"
|
const val RECYCLER = "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
object CameraX : Version("1.1.0-alpha05") {
|
|
||||||
val CAMERA2 = "androidx.camera:camera-camera2:$version"
|
|
||||||
val CORE = "androidx.camera:camera-core:$version"
|
|
||||||
val LIFECYCLE = "androidx.camera:camera-lifecycle:$version"
|
|
||||||
object View : Version("1.0.0-alpha27") {
|
|
||||||
val EXT = "androidx.camera:camera-extensions:$version"
|
|
||||||
val VIEW = "androidx.camera:camera-view:$version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
object Lifecycle : Version("2.4.0-alpha02") {
|
object Lifecycle : Version("2.4.0-alpha02") {
|
||||||
val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
|
val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user