diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7f647efbd5..9a66725eb7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") + } + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/MLKitBarcodeAnalyzer.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/MLKitBarcodeAnalyzer.kt new file mode 100644 index 0000000000..d85d5f44c2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/MLKitBarcodeAnalyzer.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt index be7a6e4330..f49613149a 100644 --- a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt @@ -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? = 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, 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() { diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ViewFinderOverlay.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ViewFinderOverlay.kt new file mode 100644 index 0000000000..f4939b828f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ViewFinderOverlay.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt index ccfe000c5c..4ba97f4d51 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt @@ -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? = 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, 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() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PreviewBinder.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PreviewBinder.kt new file mode 100644 index 0000000000..b958759c58 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PreviewBinder.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/layout_qr_scanning.xml b/app/src/main/res/layout/layout_qr_scanning.xml new file mode 100644 index 0000000000..964d9acae6 --- /dev/null +++ b/app/src/main/res/layout/layout_qr_scanning.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8f75b4d5f4..63c85c5bfe 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -42,4 +42,7 @@ 16dp + 4dp + 8dp + diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a4e2b51a39..6de5a3f3ab 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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 diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 5a915b109e..3aa47352d5 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -6,7 +6,6 @@ import java.math.BigDecimal interface TransactionManager { - @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun sendApproveTransaction( txData: ApproveTxData,