add support for PayMV QR scan
This commit is contained in:
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/BinaryEye" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
77
.kotlin/errors/errors-1778623429200.log
Normal file
77
.kotlin/errors/errors-1778623429200.log
Normal file
@@ -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
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "sh.sar.basedbank"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "sh.sar.basedbank"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
@@ -61,6 +61,14 @@ dependencies {
|
||||
// Coroutines
|
||||
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
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<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.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -48,6 +49,11 @@
|
||||
android:name=".ui.home.HomeActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.QrScannerActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
169
app/src/main/java/sh/sar/basedbank/ui/home/QrScannerActivity.kt
Normal file
169
app/src/main/java/sh/sar/basedbank/ui/home/QrScannerActivity.kt
Normal file
@@ -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.activityViewModels
|
||||
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.launch
|
||||
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.databinding.FragmentTransferBinding
|
||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
import sh.sar.basedbank.util.RecentPick
|
||||
import sh.sar.basedbank.util.RecentsCache
|
||||
|
||||
@@ -41,6 +45,19 @@ class TransferFragment : Fragment() {
|
||||
private var selectedAccount: MibAccount? = null
|
||||
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 {
|
||||
_binding = FragmentTransferBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@@ -61,6 +78,10 @@ class TransferFragment : Fragment() {
|
||||
sheet.show(childFragmentManager, "contact_picker")
|
||||
}
|
||||
|
||||
binding.btnScanQr.setOnClickListener {
|
||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||
}
|
||||
|
||||
binding.btnTransfer.setOnClickListener {
|
||||
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.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
binding.tilTo.error = null
|
||||
}
|
||||
|
||||
@@ -99,6 +121,7 @@ class TransferFragment : Fragment() {
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.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.tilTo.visibility = View.GONE
|
||||
binding.btnPickContact.visibility = View.GONE
|
||||
binding.btnScanQr.visibility = View.GONE
|
||||
binding.cardToInfo.visibility = View.VISIBLE
|
||||
saveToRecents(info)
|
||||
|
||||
@@ -166,6 +190,7 @@ class TransferFragment : Fragment() {
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
binding.tilTo.error = null
|
||||
binding.etTo.setText(accountNumber)
|
||||
lookupAccount()
|
||||
|
||||
48
app/src/main/java/sh/sar/basedbank/util/PaymvQrParser.kt
Normal file
48
app/src/main/java/sh/sar/basedbank/util/PaymvQrParser.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/ic_qr_scan.xml
Normal file
9
app/src/main/res/drawable/ic_qr_scan.xml
Normal file
@@ -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>
|
||||
36
app/src/main/res/layout/activity_qr_scanner.xml
Normal file
36
app/src/main/res/layout/activity_qr_scanner.xml
Normal file
@@ -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"
|
||||
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>
|
||||
|
||||
<!-- Confirmed account info card (shown after successful lookup) -->
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
<string name="transfer_lookup_account">Look up account</string>
|
||||
<string name="transfer_clear_recipient">Clear recipient</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_account_not_found">Account not found</string>
|
||||
<string name="transfer_session_unavailable">Session unavailable — please re-login</string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
agp = "8.9.1"
|
||||
kotlin = "2.0.21"
|
||||
kotlin = "2.1.21"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user