Feat(All): Tis

create first version
This commit is contained in:
Amir Mousavi
2026-04-29 16:31:04 +03:30
commit c9e656f705
145 changed files with 3136 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+48
View File
@@ -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"))
}
View File
Binary file not shown.
+21
View File
@@ -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)
}
}
+5
View File
@@ -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)
}
}