forked from shihaam/thijooree
add support for PayMV QR scan
This commit is contained in:
Generated
+1
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/BinaryEye" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/shihaam/git/sargit/basedbank/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt:23:5: java.lang.IllegalArgumentException: source must not be null
|
||||||
|
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
|
||||||
|
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
|
||||||
|
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
|
||||||
|
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
|
||||||
|
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
|
||||||
|
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
|
||||||
|
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
|
||||||
|
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
|
||||||
|
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
||||||
|
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
||||||
|
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
||||||
|
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally$lambda$9$compile(IncrementalCompilerRunner.kt:249)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:267)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
|
||||||
|
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||||
|
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||||
|
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||||
|
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
|
||||||
|
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||||
|
at java.base/java.lang.Thread.run(Unknown Source)
|
||||||
|
Caused by: java.lang.IllegalArgumentException: source must not be null
|
||||||
|
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
|
||||||
|
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
|
||||||
|
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
|
||||||
|
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||||
|
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||||
|
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||||
|
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
|
||||||
|
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||||
|
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
|
||||||
|
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||||
|
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
|
||||||
|
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
|
||||||
|
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
|
||||||
|
... 34 more
|
||||||
|
|
||||||
|
|
||||||
+10
-2
@@ -5,12 +5,12 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "sh.sar.basedbank"
|
namespace = "sh.sar.basedbank"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@@ -61,6 +61,14 @@ dependencies {
|
|||||||
// Coroutines
|
// Coroutines
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||||
|
|
||||||
|
// QR scanning — CameraX + zxing-cpp (MIT, same stack as BinaryEye)
|
||||||
|
implementation("androidx.camera:camera-core:1.4.2")
|
||||||
|
implementation("androidx.camera:camera-camera2:1.4.2")
|
||||||
|
implementation("androidx.camera:camera-lifecycle:1.4.2")
|
||||||
|
implementation("androidx.camera:camera-view:1.4.2")
|
||||||
|
implementation("com.github.markusfisch:zxing-cpp:v3.0.2.5")
|
||||||
|
|
||||||
|
|
||||||
// Biometric authentication
|
// Biometric authentication
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
@@ -48,6 +49,11 @@
|
|||||||
android:name=".ui.home.HomeActivity"
|
android:name=".ui.home.HomeActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.home.QrScannerActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ImageAnalysis
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
||||||
|
import androidx.camera.core.resolutionselector.ResolutionSelector
|
||||||
|
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.databinding.ActivityQrScannerBinding
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class QrScannerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityQrScannerBinding
|
||||||
|
private val cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
private val parentJob = Job()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||||
|
private var resultDelivered = false
|
||||||
|
|
||||||
|
private val readerOptions = ZxingCpp.ReaderOptions(
|
||||||
|
tryHarder = true,
|
||||||
|
tryRotate = true,
|
||||||
|
tryInvert = true,
|
||||||
|
tryDownscale = true,
|
||||||
|
maxNumberOfSymbols = 1,
|
||||||
|
textMode = ZxingCpp.TextMode.PLAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
private val permissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted -> if (granted) startCamera() else finish() }
|
||||||
|
|
||||||
|
private val pickImageLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri ?: return@registerForActivityResult
|
||||||
|
scope.launch {
|
||||||
|
val bitmap = try {
|
||||||
|
contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it) }
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
|
||||||
|
if (bitmap == null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(this@QrScannerActivity, "Could not load image", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = bitmap.decodeQr()?.firstOrNull()?.text
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (text != null) {
|
||||||
|
deliverResult(text)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@QrScannerActivity, "No QR code found in image", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityQrScannerBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
binding.btnCancel.setOnClickListener { finish() }
|
||||||
|
binding.btnPickImage.setOnClickListener { pickImageLauncher.launch("image/*") }
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
|
) startCamera()
|
||||||
|
else permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCamera() {
|
||||||
|
val future = ProcessCameraProvider.getInstance(this)
|
||||||
|
future.addListener({
|
||||||
|
val provider = try {
|
||||||
|
future.get()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
finish(); return@addListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolutionSelector = ResolutionSelector.Builder()
|
||||||
|
.setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
|
||||||
|
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val preview = Preview.Builder()
|
||||||
|
.setResolutionSelector(resolutionSelector)
|
||||||
|
.build()
|
||||||
|
.also { it.setSurfaceProvider(binding.previewView.surfaceProvider) }
|
||||||
|
|
||||||
|
val analysis = ImageAnalysis.Builder()
|
||||||
|
.setResolutionSelector(resolutionSelector)
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build()
|
||||||
|
.also { ia ->
|
||||||
|
ia.setAnalyzer(cameraExecutor) { image ->
|
||||||
|
if (!resultDelivered) {
|
||||||
|
val yPlane = image.planes[0]
|
||||||
|
ZxingCpp.readYBuffer(
|
||||||
|
yPlane.buffer,
|
||||||
|
yPlane.rowStride,
|
||||||
|
Rect(0, 0, image.width, image.height),
|
||||||
|
image.imageInfo.rotationDegrees,
|
||||||
|
readerOptions
|
||||||
|
)?.firstOrNull()?.let { result ->
|
||||||
|
runOnUiThread { deliverResult(result.text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
provider.unbindAll()
|
||||||
|
provider.bindToLifecycle(
|
||||||
|
this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}, ContextCompat.getMainExecutor(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror BinaryEye: try LOCAL_AVERAGE first, fall back to GLOBAL_HISTOGRAM
|
||||||
|
private fun Bitmap.decodeQr() = ZxingCpp.readBitmap(
|
||||||
|
this, 0, 0, width, height, 0,
|
||||||
|
readerOptions.apply { binarizer = ZxingCpp.Binarizer.LOCAL_AVERAGE }
|
||||||
|
) ?: ZxingCpp.readBitmap(
|
||||||
|
this, 0, 0, width, height, 0,
|
||||||
|
readerOptions.apply { binarizer = ZxingCpp.Binarizer.GLOBAL_HISTOGRAM }
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun deliverResult(text: String) {
|
||||||
|
if (resultDelivered) return
|
||||||
|
resultDelivered = true
|
||||||
|
setResult(Activity.RESULT_OK, Intent().putExtra(EXTRA_QR_CONTENT, text))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
parentJob.cancel()
|
||||||
|
cameraExecutor.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_QR_CONTENT = "qr_content"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ import androidx.core.widget.addTextChangedListener
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -29,6 +32,7 @@ import sh.sar.basedbank.api.mib.MibLookupException
|
|||||||
import sh.sar.basedbank.api.mib.MibTransferClient
|
import sh.sar.basedbank.api.mib.MibTransferClient
|
||||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
import sh.sar.basedbank.util.RecentPick
|
import sh.sar.basedbank.util.RecentPick
|
||||||
import sh.sar.basedbank.util.RecentsCache
|
import sh.sar.basedbank.util.RecentsCache
|
||||||
|
|
||||||
@@ -41,6 +45,19 @@ class TransferFragment : Fragment() {
|
|||||||
private var selectedAccount: MibAccount? = null
|
private var selectedAccount: MibAccount? = null
|
||||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||||
|
|
||||||
|
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
|
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||||
|
val qr = PaymvQrParser.parse(raw)
|
||||||
|
if (qr == null || qr.accountNumber == null) {
|
||||||
|
Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
if (qr.amount != null) binding.etAmount.setText(qr.amount)
|
||||||
|
if (qr.purpose != null) binding.etRemarks.setText(qr.purpose)
|
||||||
|
prefillToFromContact(qr.accountNumber, "")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentTransferBinding.inflate(inflater, container, false)
|
_binding = FragmentTransferBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -61,6 +78,10 @@ class TransferFragment : Fragment() {
|
|||||||
sheet.show(childFragmentManager, "contact_picker")
|
sheet.show(childFragmentManager, "contact_picker")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnScanQr.setOnClickListener {
|
||||||
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
binding.btnTransfer.setOnClickListener {
|
binding.btnTransfer.setOnClickListener {
|
||||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -90,6 +111,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
binding.tilTo.error = null
|
binding.tilTo.error = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +121,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +170,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(displayName, colorHex))
|
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(displayName, colorHex))
|
||||||
binding.tilTo.visibility = View.GONE
|
binding.tilTo.visibility = View.GONE
|
||||||
binding.btnPickContact.visibility = View.GONE
|
binding.btnPickContact.visibility = View.GONE
|
||||||
|
binding.btnScanQr.visibility = View.GONE
|
||||||
binding.cardToInfo.visibility = View.VISIBLE
|
binding.cardToInfo.visibility = View.VISIBLE
|
||||||
saveToRecents(info)
|
saveToRecents(info)
|
||||||
|
|
||||||
@@ -166,6 +190,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
binding.tilTo.error = null
|
binding.tilTo.error = null
|
||||||
binding.etTo.setText(accountNumber)
|
binding.etTo.setText(accountNumber)
|
||||||
lookupAccount()
|
lookupAccount()
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
data class PaymvQrData(
|
||||||
|
val accountNumber: String?,
|
||||||
|
val amount: String?,
|
||||||
|
val purpose: String?,
|
||||||
|
val merchantName: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
object PaymvQrParser {
|
||||||
|
|
||||||
|
fun parse(raw: String): PaymvQrData? {
|
||||||
|
return try {
|
||||||
|
val root = parseTlv(raw)
|
||||||
|
|
||||||
|
// ID 26: Favara/PayMV merchant account info
|
||||||
|
val merchantInfo = root["26"]?.let { parseTlv(it) }
|
||||||
|
|
||||||
|
// ID 54: transaction amount
|
||||||
|
// ID 59: merchant/recipient name
|
||||||
|
// ID 62: additional data (sub-ID 08 = purpose)
|
||||||
|
val additionalData = root["62"]?.let { parseTlv(it) }
|
||||||
|
|
||||||
|
PaymvQrData(
|
||||||
|
accountNumber = merchantInfo?.get("03"),
|
||||||
|
amount = root["54"],
|
||||||
|
purpose = additionalData?.get("08"),
|
||||||
|
merchantName = root["59"]
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTlv(data: String): Map<String, String> {
|
||||||
|
val result = mutableMapOf<String, String>()
|
||||||
|
var pos = 0
|
||||||
|
while (pos + 4 <= data.length) {
|
||||||
|
val id = data.substring(pos, pos + 2)
|
||||||
|
val len = data.substring(pos + 2, pos + 4).toIntOrNull() ?: break
|
||||||
|
pos += 4
|
||||||
|
if (pos + len > data.length) break
|
||||||
|
result[id] = data.substring(pos, pos + len)
|
||||||
|
pos += len
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorOnSurface"
|
||||||
|
android:pathData="M9.5,6.5v3h-3v-3H9.5zM11,5L5,5v5.5h6L11,5zM9.5,14.5v3h-3v-3H9.5zM11,13L5,13v5.5h6L11,13zM17.5,6.5v3h-3v-3H17.5zM19,5h-6v5.5h6L19,5zM13,13h1.5v1.5L13,14.5zM14.5,14.5L16,14.5L16,16h-1.5zM16,13h1.5v1.5L16,14.5zM13,16h1.5v1.5L13,17.5zM14.5,17.5L16,17.5L16,19h-1.5zM16,16h1.5v1.5L16,17.5zM17.5,14.5L19,14.5L19,16h-1.5zM17.5,17.5L19,17.5L19,19h-1.5z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/black">
|
||||||
|
|
||||||
|
<androidx.camera.view.PreviewView
|
||||||
|
android:id="@+id/previewView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
|
android:layout_marginBottom="48dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnPickImage"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/qr_pick_image" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/back" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -81,6 +81,15 @@
|
|||||||
app:icon="@drawable/ic_contacts"
|
app:icon="@drawable/ic_contacts"
|
||||||
android:contentDescription="@string/transfer_pick_contact" />
|
android:contentDescription="@string/transfer_pick_contact" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnScanQr"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:icon="@drawable/ic_qr_scan"
|
||||||
|
android:contentDescription="@string/transfer_scan_qr" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Confirmed account info card (shown after successful lookup) -->
|
<!-- Confirmed account info card (shown after successful lookup) -->
|
||||||
|
|||||||
@@ -104,6 +104,9 @@
|
|||||||
<string name="transfer_lookup_account">Look up account</string>
|
<string name="transfer_lookup_account">Look up account</string>
|
||||||
<string name="transfer_clear_recipient">Clear recipient</string>
|
<string name="transfer_clear_recipient">Clear recipient</string>
|
||||||
<string name="transfer_pick_contact">Pick contact</string>
|
<string name="transfer_pick_contact">Pick contact</string>
|
||||||
|
<string name="transfer_scan_qr">Scan QR</string>
|
||||||
|
<string name="qr_pick_image">Pick image</string>
|
||||||
|
<string name="transfer_qr_invalid">Invalid or unsupported QR code</string>
|
||||||
<string name="transfer_enter_account_first">Enter an account number first</string>
|
<string name="transfer_enter_account_first">Enter an account number first</string>
|
||||||
<string name="transfer_account_not_found">Account not found</string>
|
<string name="transfer_account_not_found">Account not found</string>
|
||||||
<string name="transfer_session_unavailable">Session unavailable — please re-login</string>
|
<string name="transfer_session_unavailable">Session unavailable — please re-login</string>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.1"
|
agp = "8.9.1"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.1.21"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.1.5"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user