Feat(All): Tis
create first version
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.ps3"
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
||||
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
|
||||
|
||||
// 2. This specifically includes the platform SDK jar
|
||||
compileOnly(files("libs/platform_sdk_v2.3.706.jar"))
|
||||
|
||||
}
|
||||
Binary file not shown.
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.ps3
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.ps3.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.PRINTER" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.example.ps3
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.result.ActivityResult
|
||||
import com.example.core.model.PaymentResultEntity
|
||||
import com.example.core.model.PaymentResultStatus
|
||||
import com.example.ps3.models.IapAccountSplit
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class IapPosService @Inject constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
private val gson = Gson()
|
||||
|
||||
fun isPosAppInstalled(packageName: String = "ir.co.pna.pos"): Boolean {
|
||||
return try {
|
||||
context.packageManager.getPackageInfo(packageName, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun startPurchase(
|
||||
amount: Long,
|
||||
billNo: String? = null,
|
||||
additionalData: String? = null,
|
||||
swipeCardTimeoutMs: Int = 30000,
|
||||
receiptType: String = "1",
|
||||
packageName: String = "ir.co.pna.pos"
|
||||
): Intent {
|
||||
val payload = mutableMapOf<String, Any>(
|
||||
"AndroidPosMessageHeader" to "@@PNA@@",
|
||||
"ECRType" to "1",
|
||||
"Amount" to amount.toString(),
|
||||
"TransactionType" to "00",
|
||||
"SwipeCardTimeout" to swipeCardTimeoutMs.toString(),
|
||||
"ReceiptType" to receiptType,
|
||||
"OriginalAmount" to amount.toString()
|
||||
)
|
||||
|
||||
if (!billNo.isNullOrEmpty()) {
|
||||
payload["BillNo"] = billNo
|
||||
}
|
||||
if (!additionalData.isNullOrEmpty()) {
|
||||
payload["AdditionalData"] = additionalData
|
||||
}
|
||||
|
||||
return createIntent(payload, packageName)
|
||||
}
|
||||
|
||||
fun startBillPayment(
|
||||
billId: String,
|
||||
paymentId: String,
|
||||
billNo: String? = null,
|
||||
additionalData: String? = null,
|
||||
swipeCardTimeoutMs: Int = 30000,
|
||||
receiptType: String = "1",
|
||||
packageName: String = "ir.co.pna.pos"
|
||||
): Intent {
|
||||
val payload = mutableMapOf<String, Any>(
|
||||
"AndroidPosMessageHeader" to "@@PNA@@",
|
||||
"ECRType" to "1",
|
||||
"TransactionType" to "01",
|
||||
"BillId" to billId,
|
||||
"PaymentId" to paymentId,
|
||||
"SwipeCardTimeout" to swipeCardTimeoutMs.toString(),
|
||||
"ReceiptType" to receiptType
|
||||
)
|
||||
|
||||
if (!billNo.isNullOrEmpty()) {
|
||||
payload["BillNo"] = billNo
|
||||
}
|
||||
if (!additionalData.isNullOrEmpty()) {
|
||||
payload["AdditionalData"] = additionalData
|
||||
}
|
||||
|
||||
return createIntent(payload, packageName)
|
||||
}
|
||||
|
||||
fun startMultiAccountPurchase(
|
||||
amount: Long,
|
||||
splits: List<IapAccountSplit>,
|
||||
billNo: String? = null,
|
||||
swipeCardTimeoutMs: Int = 30000,
|
||||
receiptType: String = "1",
|
||||
packageName: String = "ir.co.pna.pos"
|
||||
): Intent {
|
||||
require(splits.isNotEmpty()) { "splits must not be empty." }
|
||||
val totalPercent = splits.sumOf { it.percent }
|
||||
require(totalPercent == 100) { "splits percent total must be 100." }
|
||||
|
||||
val additionalDataList = mutableListOf<Map<String, Any>>()
|
||||
var totalProcessedAmount = 0L
|
||||
for (i in splits.indices) {
|
||||
val split = splits[i]
|
||||
val isLast = i == splits.size - 1
|
||||
val accountAmount = if (isLast) {
|
||||
amount - totalProcessedAmount
|
||||
} else {
|
||||
(amount * split.percent / 100)
|
||||
}
|
||||
totalProcessedAmount += accountAmount
|
||||
additionalDataList.add(
|
||||
mapOf(
|
||||
"Index" to split.index,
|
||||
"AccountAmount" to accountAmount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val payload = mutableMapOf<String, Any>(
|
||||
"AndroidPosMessageHeader" to "@@PNA@@",
|
||||
"ECRType" to "1",
|
||||
"Amount" to amount.toString(),
|
||||
"TransactionType" to "04",
|
||||
"AdditionalData" to additionalDataList,
|
||||
"SwipeCardTimeout" to swipeCardTimeoutMs.toString(),
|
||||
"ReceiptType" to receiptType,
|
||||
"OriginalAmount" to amount.toString()
|
||||
)
|
||||
|
||||
if (!billNo.isNullOrEmpty()) {
|
||||
payload["BillNo"] = billNo
|
||||
}
|
||||
|
||||
return createIntent(payload, packageName)
|
||||
}
|
||||
|
||||
fun getSettlementAccounts(packageName: String = "ir.co.pna.pos"): Intent {
|
||||
val payload = mapOf(
|
||||
"AndroidPosMessageHeader" to "@@PNA@@",
|
||||
"TransactionType" to "10"
|
||||
)
|
||||
return createIntent(payload, packageName)
|
||||
}
|
||||
|
||||
private fun createIntent(payload: Map<String, Any>, packageName: String): Intent {
|
||||
val data = gson.toJson(payload)
|
||||
|
||||
return Intent("ir.co.pna.pos.view.cart.IAPCActivity").apply {
|
||||
// setPackage(packageName)
|
||||
putExtra("Data", data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun decodeResponse(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity {
|
||||
val result = activityResult.data?.getStringExtra("Result")
|
||||
|
||||
if (result.isNullOrEmpty()) {
|
||||
throw Exception("POS app returned no result.")
|
||||
}
|
||||
return try {
|
||||
val type = object : TypeToken<Map<String, Any>>() {}.type
|
||||
val data = gson.fromJson<Map<String, Any>>(result, type)
|
||||
PaymentResultEntity(
|
||||
id = id,
|
||||
status = if (data["Status"] == "OK") PaymentResultStatus.OK else PaymentResultStatus.ERROR,
|
||||
terminalId = data["TerminalId"].toString(),
|
||||
stan = data["STAN"].toString(),
|
||||
rrn = data["RRN"].toString(),
|
||||
responseCode = data["ResponseCode"].toString(),
|
||||
customerCardNO = data["CustomerCardNO"].toString(),
|
||||
transactionDateTime = data["TransactionDateTime"].toString(),
|
||||
description = data["Description"].toString()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("POS response is not a JSON object.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.example.ps3
|
||||
|
||||
import android.device.PrinterManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Helper class for interacting with the POS Printer.
|
||||
*/
|
||||
@Singleton
|
||||
class PrinterHelper @Inject constructor() {
|
||||
private var printer: PrinterManager? = null
|
||||
private val gson = GsonBuilder().setPrettyPrinting().create()
|
||||
|
||||
private val textPaint = Paint().apply {
|
||||
textSize = 24f
|
||||
color = Color.BLACK
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
init {
|
||||
detectPrinter()
|
||||
}
|
||||
|
||||
private fun detectPrinter() {
|
||||
try {
|
||||
// Attempt to instantiate the manager
|
||||
val manager = PrinterManager()
|
||||
|
||||
try {
|
||||
// Test the implementation.
|
||||
// We use getStatus() to see if it's the system implementation or the stub.
|
||||
val status = manager.getStatus()
|
||||
printer = manager
|
||||
Log.d("PrinterHelper", "PrinterManager initialized. Initial status: $status")
|
||||
} catch (e: Exception) {
|
||||
val errorMsg = e.cause?.message ?: e.message ?: ""
|
||||
Log.w("PrinterHelper", "getStatus() failed with: $errorMsg")
|
||||
|
||||
if (errorMsg.contains("stub", ignoreCase = true)) {
|
||||
Log.e("PrinterHelper", "CRITICAL: The SDK JAR is being bundled in your APK. You must Clean and Rebuild.")
|
||||
} else {
|
||||
// If it's not a 'stub' error, it might just be the hardware state
|
||||
printer = manager
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("PrinterHelper", "PrinterManager not available on this device: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getStatusMessage(code: Int): String {
|
||||
return when (code) {
|
||||
0 -> "Printer is healthy"
|
||||
-1 -> "No paper in printer"
|
||||
2 -> "Device is overheating"
|
||||
-3 -> "Battery is too low to print"
|
||||
else -> "Hardware error code: $code"
|
||||
}
|
||||
}
|
||||
|
||||
fun printJson(jsonData: String, onStatus: (String) -> Unit) {
|
||||
try {
|
||||
val jsonElement: JsonElement = JsonParser.parseString(jsonData)
|
||||
val prettyJson = gson.toJson(jsonElement)
|
||||
printLines(prettyJson.split("\n"), onStatus)
|
||||
} catch (e: Exception) {
|
||||
onStatus("Invalid JSON data provided.")
|
||||
}
|
||||
}
|
||||
|
||||
fun printLines(lines: List<String>, onStatus: (String) -> Unit) {
|
||||
if (printer == null) {
|
||||
detectPrinter()
|
||||
}
|
||||
|
||||
val currentPrinter = printer
|
||||
if (currentPrinter == null) {
|
||||
onStatus("Printer hardware not supported or SDK issue.")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("PrinterHelper", "Starting print job...")
|
||||
var bitmap: Bitmap? = null
|
||||
try {
|
||||
currentPrinter.open()
|
||||
currentPrinter.setupPage(384, -1)
|
||||
currentPrinter.clearPage()
|
||||
|
||||
val lineHeight = 32
|
||||
val totalHeight = (lines.size * lineHeight) + 40
|
||||
|
||||
bitmap = Bitmap.createBitmap(384, totalHeight, Bitmap.Config.ARGB_8888)
|
||||
bitmap.eraseColor(Color.WHITE)
|
||||
val canvas = Canvas(bitmap)
|
||||
|
||||
var yPos = 30f
|
||||
for (line in lines) {
|
||||
canvas.drawText(line, 10f, yPos, textPaint)
|
||||
yPos += lineHeight
|
||||
}
|
||||
|
||||
currentPrinter.drawBitmap(bitmap, 0, 0)
|
||||
val printResult = currentPrinter.printPage(0)
|
||||
|
||||
if (printResult == 0) {
|
||||
currentPrinter.paperFeed(20)
|
||||
onStatus("Print Job Completed Successfully")
|
||||
} else {
|
||||
onStatus(getStatusMessage(printResult))
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
onStatus("Printer Error: ${e.localizedMessage}")
|
||||
Log.e("PrinterHelper", "Unexpected error during printing", e)
|
||||
} finally {
|
||||
bitmap?.recycle()
|
||||
try {
|
||||
currentPrinter.close()
|
||||
} catch (e: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fun printText(): Int {
|
||||
val manager = printer
|
||||
val mCenterPaint = Paint().apply {
|
||||
textSize = 30f
|
||||
color = Color.BLACK
|
||||
}
|
||||
var mBitmap: Bitmap? = null
|
||||
manager.setupPage(384, -1)
|
||||
manager.clearPage()
|
||||
|
||||
val mconfig = Bitmap.Config.ARGB_8888
|
||||
mBitmap = Bitmap.createBitmap(384, 2000, mconfig)
|
||||
mBitmap.eraseColor(-0x1) // 0xffffffff
|
||||
|
||||
val mCanvas = Canvas(mBitmap)
|
||||
|
||||
mCanvas.drawText("سلام", 330f, 30f, mCenterPaint)
|
||||
mCanvas.drawText("حال شما خوب هست", 165f, 60f, mCenterPaint)
|
||||
mCanvas.drawText("سلام", 330f, 90f, mCenterPaint)
|
||||
mCanvas.drawText("حال شما خوب هست", 165f, 120f, mCenterPaint)
|
||||
|
||||
var mirrored: Bitmap? = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.width, 130)
|
||||
manager.drawBitmap(mirrored, 0, 0)
|
||||
|
||||
val ret = manager.printPage(0)
|
||||
manager.paperFeed(15)
|
||||
|
||||
mBitmap?.let {
|
||||
it.recycle()
|
||||
mBitmap = null
|
||||
}
|
||||
|
||||
mirrored?.let {
|
||||
it.recycle()
|
||||
mirrored = null
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.ps3
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResult
|
||||
import com.example.core.domain.PspService
|
||||
import com.example.core.model.PaymentResultEntity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Inject
|
||||
|
||||
class Ps3Service @Inject constructor(
|
||||
private val iapPosService: IapPosService,
|
||||
private val printerHelper: PrinterHelper
|
||||
) : PspService {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun pay(amount: Long): Intent {
|
||||
return iapPosService.startPurchase(amount)
|
||||
|
||||
}
|
||||
|
||||
override fun print(lines: List<String>, onMessage: (String) -> Unit) {
|
||||
// printerHelper.printText()
|
||||
printerHelper.printLines(lines) { status ->
|
||||
onMessage("PS3 Printer: $status")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun decodePosResponse(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity? {
|
||||
return try {
|
||||
iapPosService.decodeResponse(
|
||||
id = id,
|
||||
activityResult = activityResult
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.ps3.di
|
||||
|
||||
import android.content.Context
|
||||
import com.example.ps3.IapPosService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object Ps3Module {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIapPosService(@ApplicationContext context: Context): IapPosService {
|
||||
return IapPosService(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.ps3.models
|
||||
|
||||
data class IapAccountSplit(
|
||||
val index: Int,
|
||||
val percent: Int
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.ps3
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user