Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-10 11:00:41 +02:00
parent 2806489bd8
commit c2a6235483
10 changed files with 293 additions and 75 deletions

View file

@ -105,6 +105,7 @@ dependencies {
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.browser)
implementation(deps.androidx.paging.runtime)
implementation(deps.androidx.swipeRefreshLayout)
implementation(deps.lifecycle.runtime.ktx)
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
@ -153,7 +154,6 @@ dependencies {
implementation(deps.timber)
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.zxing.qrBarcodeScanner)
implementation(deps.otaliastudiosCameraView)
implementation(deps.coil)
implementation(deps.appsflyer)
@ -192,4 +192,19 @@ dependencies {
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)
/** Camera */
implementation(deps.camera.camera2)
implementation(deps.camera.lifecycle)
implementation(deps.camera.view)
implementation(deps.listenableFuture)
implementation(deps.mlKit.barcodeScanning)
/** Excluded dependencies */
implementation("com.google.guava:guava:30.0-android") {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group="com.google.guava", module = "listenablefuture")
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.common.qrCodeScan
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnalysis.Analyzer {
private var isScanning: Boolean = false
@ExperimentalGetImage
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null && !isScanning) {
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val scanner = BarcodeScanning.getClient()
isScanning = true
scanner.process(image)
.addOnSuccessListener { codes ->
codes.firstOrNull().let { barcode ->
val rawValue = barcode?.rawValue
rawValue?.let {
onScanned.invoke(it)
}
}
isScanning = false
imageProxy.close()
}
.addOnFailureListener {
isScanning = false
imageProxy.close()
}
}
}
}

View file

@ -3,48 +3,63 @@ package com.tangem.tap.common.qrCodeScan
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.common.util.concurrent.ListenableFuture
import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE
import me.dm7.barcodescanner.zxing.ZXingScannerView
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
[REDACTED_AUTHOR]
*/
class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
class ScanQrCodeActivity : AppCompatActivity() {
private lateinit var mScannerView: ZXingScannerView
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var cameraExecutor: ExecutorService? = null
private val binder = PreviewBinder()
override fun onCreate(state: Bundle?) {
super.onCreate(state)
mScannerView = ZXingScannerView(this).apply {
setFormats(listOf(BarcodeFormat.QR_CODE))
}
setContentView(mScannerView)
if (!permissionIsGranted()) requestPermission()
}
override fun onResume() {
super.onResume()
mScannerView.setResultHandler(this)
mScannerView.startCamera()
}
setContentView(R.layout.layout_qr_scanning)
override fun onPause() {
super.onPause()
mScannerView.stopCamera()
}
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraExecutor = Executors.newSingleThreadExecutor()
override fun handleResult(result: Result) {
setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result.text) })
finish()
cameraProviderFuture?.addListener(
{
val cameraProvider = cameraProviderFuture?.get()
binder.bindPreview(
context = this,
binding = binding,
lifecycleOwner = this,
cameraProvider = requireNotNull(cameraProvider),
cameraExecutor = requireNotNull(cameraExecutor),
onScanned = { result ->
setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result) })
finish()
},
)
},
ContextCompat.getMainExecutor(this),
)
binding.overlay.post {
binding.overlay.setViewFinder()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
@ -56,12 +71,8 @@ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
}
private fun permissionIsGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
cameraPermission == PackageManager.PERMISSION_GRANTED
} else {
true
}
val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
return cameraPermission == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {

View file

@ -0,0 +1,42 @@
package com.tangem.tap.common.qrCodeScan
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import com.tangem.wallet.R
class ViewFinderOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val boxPaint: Paint = Paint().apply {
color = ContextCompat.getColor(context, R.color.white)
style = Paint.Style.STROKE
strokeWidth = context.resources.getDimensionPixelOffset(R.dimen.qr_border_stroke_width).toFloat()
}
private val boxWidthRatio = 0.8F
private val boxCornerRadius: Float =
context.resources.getDimensionPixelOffset(R.dimen.qr_border_corner_radius).toFloat()
private var boxRect: RectF? = null
@Suppress("MagicNumber")
fun setViewFinder() {
val overlayWidth = width.toFloat()
val overlayHeight = height.toFloat()
val boxSize = overlayWidth * boxWidthRatio
val cx = overlayWidth / 2
val cy = overlayHeight / 2
boxRect = RectF(cx - boxSize / 2, cy - boxSize / 2, cx + boxSize / 2, cy + boxSize / 2)
invalidate()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
boxRect?.let {
canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, boxPaint)
}
}
}

View file

@ -2,26 +2,33 @@ package com.tangem.tap.features.details.ui.walletconnect
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.activity.OnBackPressedCallback
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.common.util.concurrent.ListenableFuture
import com.otaliastudios.cameraview.CameraView
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
import com.tangem.tap.store
import me.dm7.barcodescanner.zxing.ZXingScannerView
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
internal class QrScanFragment : Fragment(R.layout.layout_qr_scanning) {
private var scannerView: ZXingScannerView? = null
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
private val binder = PreviewBinder()
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var cameraExecutor: ExecutorService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -36,25 +43,38 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!permissionIsGranted()) requestPermission()
scannerView = ZXingScannerView(activity).apply {
setFormats(listOf(BarcodeFormat.QR_CODE))
cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraExecutor = Executors.newSingleThreadExecutor()
cameraProviderFuture?.addListener(
{
val cameraProvider = cameraProviderFuture?.get()
binder.bindPreview(
context = requireContext(),
binding = binding,
lifecycleOwner = this,
cameraProvider = requireNotNull(cameraProvider),
cameraExecutor = requireNotNull(cameraExecutor),
onScanned = { result ->
store.dispatch(NavigationAction.PopBackTo())
setFitSystemWindows(fit = false)
if (result.isNotBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result))
}
},
)
},
ContextCompat.getMainExecutor(requireContext()),
)
binding.overlay.post {
binding.overlay.setViewFinder()
}
return scannerView
}
override fun onResume() {
super.onResume()
scannerView?.setResultHandler(this)
scannerView?.startCamera()
}
override fun onPause() {
super.onPause()
scannerView?.stopCamera()
}
override fun onDestroy() {
@ -62,14 +82,6 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
setFitSystemWindows(fit = false)
}
override fun handleResult(result: Result) {
store.dispatch(NavigationAction.PopBackTo())
setFitSystemWindows(fit = false)
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result.text))
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return
@ -80,13 +92,8 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
}
private fun permissionIsGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cameraPermission =
ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
cameraPermission == PackageManager.PERMISSION_GRANTED
} else {
true
}
val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
return cameraPermission == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {

View file

@ -0,0 +1,69 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import android.util.Size
import android.view.OrientationEventListener
import android.view.Surface
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.lifecycle.LifecycleOwner
import com.tangem.tap.common.qrCodeScan.MLKitBarcodeAnalyzer
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
internal class PreviewBinder {
@Suppress("LongParameterList")
fun bindPreview(
context: Context,
binding: LayoutQrScanningBinding,
lifecycleOwner: LifecycleOwner,
cameraProvider: ProcessCameraProvider,
cameraExecutor: ExecutorService,
onScanned: (String) -> Unit,
) {
cameraProvider.unbindAll()
val preview: Preview = Preview.Builder()
.build()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(binding.cameraPreview.width, binding.cameraPreview.height))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val orientationEventListener = object : OrientationEventListener(context) {
@Suppress("MagicNumber")
override fun onOrientationChanged(orientation: Int) {
val rotation: Int = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
imageAnalysis.targetRotation = rotation
}
}
orientationEventListener.enable()
val analyzer: ImageAnalysis.Analyzer = MLKitBarcodeAnalyzer {
imageAnalysis.clearAnalyzer()
onScanned.invoke(it)
}
cameraExecutor.let {
imageAnalysis.setAnalyzer(it, analyzer)
}
preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview)
}
}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
tools:background="@android:color/white">
<androidx.camera.view.PreviewView
android:id="@+id/cameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.tangem.tap.common.qrCodeScan.ViewFinderOverlay
android:id="@+id/overlay"
android:layerType="software"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View file

@ -42,4 +42,7 @@
<dimen name="layout_address_info_margin">16dp</dimen>
<dimen name="qr_border_stroke_width">4dp</dimen>
<dimen name="qr_border_corner_radius">8dp</dimen>
</resources>

View file

@ -68,7 +68,8 @@ spongycastleCryptoCore = "1.58.0.0"
timber = "4.7.1"
viewBindingDelegate = "1.5.9"
xmlShimmer = "1.1.3"
zxingQrBarcodeScanner = "1.9.8"
zendeskChat = "3.3.5"
zendeskMessaging = "5.2.4"
zxingQrCode = "3.5.1"
mviCore = "1.3.1"
kotlinSerialization = "1.4.1"
@ -79,6 +80,10 @@ walletConnectWeb3 = "1.11.0"
prettyLogger = "2.2.0"
okHttp-prettyLogging = "3.1.0"
chucker = "4.0.0"
mlKit-barcodeScanning = "17.2.0"
androidXCamera = "1.3.0"
listenableFuture = "1.0"
swipeRefreshLayout = "1.1.0"
spr-client = "3.6.2"
# endregion Other libraries
@ -132,11 +137,12 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx
androidx-core-splashScreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplashScreen" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragment" }
androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" }
androidx-swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayout" }
androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" }
lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" }
androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
# region AndroidX
@ -220,7 +226,8 @@ retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.re
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" }
xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" }
zxing-qrBarcodeScanner = { module = "me.dm7.barcodescanner:zxing", version.ref = "zxingQrBarcodeScanner" }
zendesk-chat = { module = "com.zendesk:chat", version.ref = "zendeskChat" }
zendesk-messaging = { module = "com.zendesk:messaging", version.ref = "zendeskMessaging" }
zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" }
mviCore-watcher = { module = "com.github.badoo.mvicore:mvicore-diff", version.ref = "mviCore" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
@ -233,4 +240,10 @@ prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" }
sprClient = { module = "com.spr:messengerclient", version.ref = "spr-client" }
chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" }
chuckerStub = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" }
mlKit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlKit-barcodeScanning" }
listenableFuture = { module = "com.google.guava:listenablefuture", version.ref = "listenableFuture" }
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidXCamera" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidXCamera" }
camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" }
# endregion Other

View file

@ -6,7 +6,6 @@ import java.math.BigDecimal
interface TransactionManager {
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun sendApproveTransaction(
txData: ApproveTxData,