From ee0482059464b5c0fd0872ce7a8e9141035c8158 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jan 2024 15:03:48 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../src/main/res/drawable/ic_gallery_24.xml | 9 ++ .../feature/qrscanning/QrScanningRouter.kt | 1 + features/qr-scanning/impl/build.gradle.kts | 16 ++- .../com/tangem/feature/qrscanning/Const.kt | 5 - .../qrscanning/DefaultQrScanningRouter.kt | 8 -- .../feature/qrscanning/QrScanningFragment.kt | 118 ++++++++--------- .../qrscanning/QrScanningRouterModule.kt | 18 --- .../feature/qrscanning/QrScanningViewModel.kt | 25 ---- .../qrscanning/{ => di}/QrScanningModule.kt | 2 +- .../qrscanning/di/QrScanningRouterModule.kt | 21 +++ .../qrscanning/inner/MLKitBarcodeAnalyzer.kt | 2 + .../feature/qrscanning/inner/PreviewBinder.kt | 68 ---------- .../qrscanning/inner/ViewFinderOverlay.kt | 42 ------ .../navigation/DefaultQrScanningRouter.kt | 15 +++ .../navigation/QrScanningInnerRouter.kt | 8 ++ .../qrscanning/presentation/CameraView.kt | 35 +++++ .../presentation/QrScanningContent.kt | 125 ++++++++++++++++++ .../presentation/QrScanningState.kt | 12 ++ .../presentation/QrScanningStateFactory.kt | 26 ++++ .../viewmodel/QrScanningClickIntents.kt | 10 ++ .../viewmodel/QrScanningViewModel.kt | 55 ++++++++ .../main/res/layout/layout_qr_scanning.xml | 20 --- .../impl/src/main/res/values/colors.xml | 4 - .../impl/src/main/res/values/dimens.xml | 7 - 24 files changed, 384 insertions(+), 268 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_gallery_24.xml delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/Const.kt delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningRouter.kt delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningRouterModule.kt delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningViewModel.kt rename features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/{ => di}/QrScanningModule.kt (95%) create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/PreviewBinder.kt delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/ViewFinderOverlay.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt delete mode 100644 features/qr-scanning/impl/src/main/res/layout/layout_qr_scanning.xml delete mode 100644 features/qr-scanning/impl/src/main/res/values/colors.xml delete mode 100644 features/qr-scanning/impl/src/main/res/values/dimens.xml diff --git a/core/ui/src/main/res/drawable/ic_gallery_24.xml b/core/ui/src/main/res/drawable/ic_gallery_24.xml new file mode 100644 index 0000000000..b71095523e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_gallery_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt index ce7ea43935..7f5f0352b7 100644 --- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt +++ b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt @@ -8,5 +8,6 @@ interface QrScanningRouter { companion object { const val SOURCE_KEY = "source" + const val NETWORK_KEY = "network" } } \ No newline at end of file diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 22fd460d7f..1d6c8db728 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -8,17 +8,17 @@ plugins { android { namespace = "com.tangem.feature.qrscanning.impl" - - viewBinding { - enable = true - } } dependencies { + /** Core */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.navigation) + implementation(deps.androidx.fragment.ktx) - implementation(deps.viewBindingDelegate) /** Camera */ implementation(deps.camera.camera2) @@ -42,6 +42,12 @@ dependencies { /** Compose */ implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + implementation(deps.compose.accompanist.systemUiController) + /** Other dependencies */ implementation(deps.arrow.core) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/Const.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/Const.kt deleted file mode 100644 index e2e475f8e9..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/Const.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.feature.qrscanning - -const val SCAN_QR_REQUEST_CODE = 1001 -const val SCAN_RESULT = "scanResult" -const val PERMISSION_REQUEST_CODE = 16 \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningRouter.kt deleted file mode 100644 index 0e53af0a78..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningRouter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.feature.qrscanning - -import androidx.fragment.app.Fragment - -internal class DefaultQrScanningRouter : QrScanningRouter { - - override fun getEntryFragment(): Fragment = QrScanningFragment.create() -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 8db65f98c2..9120d00cd2 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -3,98 +3,86 @@ package com.tangem.feature.qrscanning import android.Manifest import android.content.pm.PackageManager import android.os.Bundle -import android.view.* -import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat -import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import by.kirich1409.viewbindingdelegate.viewBinding -import com.google.common.util.concurrent.ListenableFuture -import com.tangem.feature.qrscanning.impl.R -import com.tangem.feature.qrscanning.impl.databinding.LayoutQrScanningBinding -import com.tangem.feature.qrscanning.inner.PreviewBinder +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer +import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter +import com.tangem.feature.qrscanning.presentation.QrScanningContent +import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel import dagger.hilt.android.AndroidEntryPoint -// 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 java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.properties.Delegates @AndroidEntryPoint -internal class QrScanningFragment : Fragment(R.layout.layout_qr_scanning) { +internal class QrScanningFragment : ComposeFragment() { - private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder - private val binder = PreviewBinder() + @Inject + lateinit var router: QrScanningRouter + + private val innerRouter: QrScanningInnerRouter + get() = requireNotNull(router as? QrScanningInnerRouter) { + "innerRouter should be instance of QrScanningInnerRouter" + } private val viewModel by viewModels() - private var cameraProviderFuture: ListenableFuture? = null - private var cameraExecutor: ExecutorService? = null + private var cameraExecutor: ExecutorService by Delegates.notNull() + + private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (!it) parentFragmentManager.popBackStack() + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setFitSystemWindows(fit = true) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - if (!permissionIsGranted()) requestPermission() - - cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + viewModel.router = innerRouter 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 -> - parentFragmentManager.popBackStack() - setFitSystemWindows(fit = false) - if (result.isNotBlank()) { - viewModel.onQrScanned(result) - } - }, - ) - }, - ContextCompat.getMainExecutor(requireContext()), - ) - - binding.overlay.post { - binding.overlay.setViewFinder() - } + requestCameraPermission() } override fun onDestroy() { super.onDestroy() + setFitSystemWindows(fit = true) + cameraPermissionLauncher.unregister() + cameraExecutor.shutdown() + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + SystemBarsEffect { + setSystemBarsColor(Color.Transparent, darkIcons = false) + } + QrScanningContent( + executor = { cameraExecutor }, + analyzer = { MLKitBarcodeAnalyzer(viewModel::onQrScanned) }, + uiState = viewModel.uiState, + ) setFitSystemWindows(fit = false) } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - if (requestCode != PERMISSION_REQUEST_CODE) return - - if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { - parentFragmentManager.popBackStack() + private fun requestCameraPermission() { + if ( + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_DENIED + ) { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) } } - private fun permissionIsGranted(): Boolean { - val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) - return cameraPermission == PackageManager.PERMISSION_GRANTED - } - - private fun requestPermission() { - requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE) - } - private fun setFitSystemWindows(fit: Boolean) { activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, fit) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningRouterModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningRouterModule.kt deleted file mode 100644 index 688c8f2cf9..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningRouterModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.qrscanning - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped - -@Module -@InstallIn(ActivityComponent::class) -internal object QrScanningRouterModule { - - @Provides - @ActivityScoped - fun provideQrScanRouter(): QrScanningRouter { - return DefaultQrScanningRouter() - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningViewModel.kt deleted file mode 100644 index b950318128..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.feature.qrscanning - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY -import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -internal class QrScanningViewModel @Inject constructor( - private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - - private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory") - - fun onQrScanned(qrCode: String) { - viewModelScope.launch { - emitQrScannedEventUseCase.invoke(source, qrCode) - } - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt similarity index 95% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningModule.kt rename to features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt index 7cd0a6cde4..ad088e8c28 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningModule.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.qrscanning +package com.tangem.feature.qrscanning.di import com.tangem.feature.qrscanning.repo.DefaultQrScanningEventsRepository import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt new file mode 100644 index 0000000000..5bf6beee3f --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.qrscanning.di + +import com.tangem.core.navigation.ReduxNavController +import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.feature.qrscanning.navigation.DefaultQrScanningRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object QrScanningRouterModule { + + @Provides + @ActivityScoped + fun provideQrScanRouter(reduxNavController: ReduxNavController): QrScanningRouter { + return DefaultQrScanningRouter(reduxNavController) + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt index 40a37a068f..ddd9d09857 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt @@ -5,7 +5,9 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.common.InputImage +import javax.annotation.concurrent.Immutable +@Immutable class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnalysis.Analyzer { private var isScanning: Boolean = false diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/PreviewBinder.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/PreviewBinder.kt deleted file mode 100644 index 609a26688a..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/PreviewBinder.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.feature.qrscanning.inner - -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.feature.qrscanning.impl.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/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/ViewFinderOverlay.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/ViewFinderOverlay.kt deleted file mode 100644 index febd5bbf06..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/ViewFinderOverlay.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.feature.qrscanning.inner - -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.view.View -import androidx.core.content.ContextCompat -import com.tangem.feature.qrscanning.impl.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/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt new file mode 100644 index 0000000000..17de053b79 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.qrscanning.navigation + +import androidx.fragment.app.Fragment +import com.tangem.core.navigation.ReduxNavController +import com.tangem.feature.qrscanning.QrScanningFragment + +class DefaultQrScanningRouter( + private val reduxNavController: ReduxNavController, +) : QrScanningInnerRouter { + override fun getEntryFragment(): Fragment = QrScanningFragment.create() + + override fun popBackStack() { + reduxNavController.popBackStack() + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt new file mode 100644 index 0000000000..cdfb4cf247 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.qrscanning.navigation + +import com.tangem.feature.qrscanning.QrScanningRouter + +interface QrScanningInnerRouter : QrScanningRouter { + + fun popBackStack() +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt new file mode 100644 index 0000000000..d3526d4e5a --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.qrscanning.presentation + +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer +import java.util.concurrent.ExecutorService + +@Composable +internal fun CameraView(executor: () -> ExecutorService, analyzer: () -> MLKitBarcodeAnalyzer) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val previewView = remember { PreviewView(context) } + val cameraController = remember { LifecycleCameraController(context) } + + cameraController.apply { + bindToLifecycle(lifecycleOwner) + cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + setImageAnalysisAnalyzer(executor(), analyzer()) + imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST + } + + previewView.controller = cameraController + + AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt new file mode 100644 index 0000000000..0a7ef5a8ec --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -0,0 +1,125 @@ +package com.tangem.feature.qrscanning.presentation + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Constraints +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.qrscanning.impl.R +import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer +import java.util.concurrent.ExecutorService +import kotlin.math.roundToInt + +private const val QR_CODE_SIZE = 0.8f + +@Composable +internal fun QrScanningContent( + executor: () -> ExecutorService, + analyzer: () -> MLKitBarcodeAnalyzer, + uiState: QrScanningState, +) { + Box( + modifier = Modifier + .fillMaxSize(), + ) { + CameraView( + executor = executor, + analyzer = analyzer, + ) + Overlay( + message = uiState.message, + ) + AppBarWithBackButtonAndIcon( + onBackClick = uiState.onBackClick, + iconRes = R.drawable.ic_gallery_24, + onIconClick = uiState.onGalleryClicked, + backgroundColor = Color.Transparent, + modifier = Modifier.statusBarsPadding(), + ) + } +} + +@Composable +private fun Overlay(message: TextReference?, overlayColor: Color = TangemColorPalette.Black.copy(alpha = 0.7f)) { + val whiteColor = TangemTheme.colors.text.constantWhite + val borderWidth = with(LocalDensity.current) { TangemTheme.dimens.spacing4.toPx() } + val borderLength = with(LocalDensity.current) { TangemTheme.dimens.size20.toPx() } + + val textMeasurer = rememberTextMeasurer() + val textPadding = with(LocalDensity.current) { TangemTheme.dimens.spacing24.toPx() } + val style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Center) + val text = message?.resolveReference() + + Canvas(modifier = Modifier.fillMaxSize()) { + val squareSize = size.minDimension * QR_CODE_SIZE + val verticalOffset = (size.height - squareSize) / 2f + val horizontalOffset = (size.width - squareSize) / 2f + drawRect( + color = overlayColor, + topLeft = Offset.Zero, + size = Size(size.width, verticalOffset), + ) + drawRect( + color = overlayColor, + topLeft = Offset(0f, verticalOffset), + size = Size(horizontalOffset, squareSize), + ) + drawRect( + color = overlayColor, + topLeft = Offset(horizontalOffset + squareSize, verticalOffset), + size = Size(horizontalOffset, squareSize), + ) + drawRect( + color = overlayColor, + topLeft = Offset(0f, verticalOffset + squareSize), + size = Size(size.width, verticalOffset), + ) + drawRect( + color = whiteColor, + topLeft = Offset(horizontalOffset, verticalOffset), + size = Size(squareSize, squareSize), + style = Stroke( + width = borderWidth, + join = StrokeJoin.Round, + pathEffect = PathEffect.dashPathEffect( + intervals = floatArrayOf(2 * borderLength, squareSize - 2 * borderLength), + phase = borderLength, + ), + ), + ) + text?.let { + val textLayoutResult = text.let { + textMeasurer.measure( + text = it, + style = style, + constraints = Constraints.fixedWidth(squareSize.roundToInt()), + ) + } + drawText( + textLayoutResult = textLayoutResult, + color = whiteColor, + topLeft = Offset( + x = horizontalOffset, + y = verticalOffset + squareSize + textPadding, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt new file mode 100644 index 0000000000..d9eb27b155 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.qrscanning.presentation + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +data class QrScanningState( + val message: TextReference?, + val onQrScanned: (String) -> Unit, + val onBackClick: () -> Unit, + val onGalleryClicked: () -> Unit, +) \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt new file mode 100644 index 0000000000..0b7ef2c4da --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.qrscanning.presentation + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents +import com.tangem.feature.qrscanning.SourceType +import com.tangem.feature.qrscanning.impl.R + +internal class QrScanningStateFactory( + val clickIntents: QrScanningClickIntents, +) { + + fun getInitialState(source: SourceType, network: String?): QrScanningState { + val message = when (source) { + SourceType.SEND -> network?.let { resourceReference(R.string.send_qrcode_scan_info, wrappedList(it)) } + else -> null + } + + return QrScanningState( + message = message, + onBackClick = clickIntents::onBackClick, + onQrScanned = clickIntents::onQrScanned, + onGalleryClicked = clickIntents::onGalleryClicked, + ) + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt new file mode 100644 index 0000000000..38219460a6 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.qrscanning.viewmodel + +interface QrScanningClickIntents { + + fun onBackClick() + + fun onQrScanned(qrCode: String) + + fun onGalleryClicked() +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt new file mode 100644 index 0000000000..a879b5f9fb --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.qrscanning.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY +import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY +import com.tangem.feature.qrscanning.SourceType +import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter +import com.tangem.feature.qrscanning.presentation.QrScanningState +import com.tangem.feature.qrscanning.presentation.QrScanningStateFactory +import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.properties.Delegates + +@HiltViewModel +internal class QrScanningViewModel @Inject constructor( + private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase, + private val dispatcher: CoroutineDispatcherProvider, + savedStateHandle: SavedStateHandle, +) : ViewModel(), QrScanningClickIntents { + + private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory") + private val network: String? = savedStateHandle[NETWORK_KEY] + + private val factory = QrScanningStateFactory( + clickIntents = this, + ) + + var router: QrScanningInnerRouter by Delegates.notNull() + + var uiState: QrScanningState by mutableStateOf(factory.getInitialState(source, network)) + private set + + override fun onBackClick() = router.popBackStack() + + override fun onQrScanned(qrCode: String) { + if (qrCode.isNotBlank()) { + router.popBackStack() + viewModelScope.launch(dispatcher.main) { + emitQrScannedEventUseCase.invoke(source, qrCode) + } + } + } + + override fun onGalleryClicked() { + // [REDACTED_JIRA] + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/res/layout/layout_qr_scanning.xml b/features/qr-scanning/impl/src/main/res/layout/layout_qr_scanning.xml deleted file mode 100644 index ecf16ff82e..0000000000 --- a/features/qr-scanning/impl/src/main/res/layout/layout_qr_scanning.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/res/values/colors.xml b/features/qr-scanning/impl/src/main/res/values/colors.xml deleted file mode 100644 index 028dd5972c..0000000000 --- a/features/qr-scanning/impl/src/main/res/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFFFF - \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/res/values/dimens.xml b/features/qr-scanning/impl/src/main/res/values/dimens.xml deleted file mode 100644 index 19f54ca894..0000000000 --- a/features/qr-scanning/impl/src/main/res/values/dimens.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - 4dp - 8dp - - \ No newline at end of file