Feat(All): Tis

some changes
This commit is contained in:
Amir Mousavi
2026-05-06 23:33:56 +03:30
parent c9e656f705
commit d73df5524e
128 changed files with 1635 additions and 156 deletions
@@ -0,0 +1,24 @@
package com.example.p3
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.p3
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.p3.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,333 @@
package com.example.p3
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import com.example.core.domain.Printer
import com.example.core.model.PrintEntity
import com.pos.sdk.printer.POIPrinterManager
import com.pos.sdk.printer.POIPrinterManager.IPrinterListener
import com.pos.sdk.printer.models.BitmapPrintLine
import com.pos.sdk.printer.models.PrintLine
import com.pos.sdk.printer.models.TextPrintLine
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
class P3Printer @Inject constructor(
@param:ApplicationContext private val context: Context,
) : Printer {
companion object {
private const val TAG = "Printer"
// Printer state constants (should ideally come from the SDK if available)
private const val PRINTER_STATE_ERROR = 4
private const val DEFAULT_PRINT_GRAY = 2000
private const val DEFAULT_LINE_SPACE = 5
}
/**
* Prints a sample receipt for demonstration.
*/
fun printSampleReceipt(context: Context) {
// Use applicationContext to avoid potential memory leaks
val printerManager = initialP3Printer()
printerManager?.apply {
setPrintGray(DEFAULT_PRINT_GRAY)
setLineSpace(DEFAULT_LINE_SPACE)
// Header
addPrintLine(TextPrintLine("This is an example of a receipt", PrintLine.CENTER))
// Reusable logo bitmap
val logoBitmap = drawableToBitmap(context, android.R.drawable.btn_plus)
val logoLine = logoBitmap?.let { BitmapPrintLine(it, PrintLine.CENTER) }
logoLine?.let { addPrintLine(it) }
// Address
val addressFont = getFontPath("font")
if (addressFont.isNotEmpty()) {
setPrintFont(addressFont)
} else {
setPrintFont("/system/fonts/ComingSoon.ttf")
}
addPrintLine(
TextPrintLine(
"Floor ** , Building **, No.*** LONG DONG Avenue, Pudong New District, Shanghai, China",
PrintLine.LEFT,
20
)
)
// Transaction Details
val detailFont = getFontPath("font")
if (detailFont.isNotEmpty()) {
setPrintFont(detailFont)
} else {
setPrintFont("/system/fonts/DroidSansMono.ttf")
}
// addPrintLine(createPrintRow("24 June 2025", " Assistant 6", "815002", size = 18))
// addPrintLine(createPrintRow("Item", "Quantity", "Price", size = 24, isBold = true))
// addPrintLine(createPrintRow("Tomato", "1", "$2.08", size = 24))
// addPrintLine(createPrintRow("Orange", "1", "$1.06", size = 24))
//
// Footer
addPrintLine(TextPrintLine("Total $3.14", PrintLine.RIGHT))
addPrintLine(TextPrintLine(""))
logoLine?.let { addPrintLine(it) }
addPrintLine(TextPrintLine(""))
addPrintLine(
TextPrintLine(
"Did you know you could have earned Rewards points on this purchase?",
PrintLine.CENTER
)
)
addPrintLine(
TextPrintLine(
"Simply sign up today for a Membership Card!",
PrintLine.CENTER
)
)
logoLine?.let { addPrintLine(it) }
addPrintLine(TextPrintLine(" ", 0, 100))
val listener = object : IPrinterListener {
override fun onStart() {
Log.d(TAG, "Print started")
}
override fun onFinish() {
Log.d(TAG, "Print finished")
close()
}
override fun onError(code: Int, msg: String?) {
Log.e(TAG, "Print error - code: $code, message: $msg")
close()
}
}
beginPrint(listener)
}
}
private fun initialP3Printer(): POIPrinterManager? {
val printerManager = POIPrinterManager(context.applicationContext)
printerManager.open()
val state = printerManager.printerState
Log.d(TAG, "Printer state = $state")
if (state == PRINTER_STATE_ERROR) {
Log.e(TAG, "Printer is in an error state ($state). Aborting print.")
printerManager.close()
return null
}
return printerManager
}
/**
* Creates a row with left, center, and right aligned text.
*/
private fun createPrintRow(
label: String,
value: String,
size: Int,
isBold: Boolean = false
): List<TextPrintLine> {
return if (value.hasPersianLetters()) {
listOf(
TextPrintLine(label, PrintLine.LEFT, size, true),
TextPrintLine(value, PrintLine.RIGHT, size, isBold)
)
} else {
listOf(
TextPrintLine(label, PrintLine.LEFT, size, true),
TextPrintLine(value, PrintLine.LEFT, size, isBold)
)
}
}
private fun getFontPath(resourceName: String): String {
return try {
// Find resource ID by name (e.g., "font") in the "font" resource folder
val resId = context.resources.getIdentifier(resourceName, "font", context.packageName)
if (resId == 0) {
Log.e(TAG, "Font resource not found: $resourceName")
return ""
}
val fileName = "$resourceName.ttf"
val fontFile = File(context.filesDir, fileName)
if (!fontFile.exists()) {
context.resources.openRawResource(resId).use { input ->
fontFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
fontFile.absolutePath
} catch (e: Exception) {
Log.e(TAG, "Error loading font from resources: $resourceName", e)
""
}
}
override fun print(
printEntities: List<PrintEntity>,
@DrawableRes icon: Int
) {
val printerManager = initialP3Printer()
printerManager?.apply {
setPrintGray(DEFAULT_PRINT_GRAY)
setLineSpace(DEFAULT_LINE_SPACE)
// Reusable logo bitmap
val logoBitmap = drawableToBitmap(context, icon, 200)
if (logoBitmap != null) {
val logoLine = BitmapPrintLine(logoBitmap, PrintLine.CENTER)
addPrintLine(logoLine)
addPrintLine(TextPrintLine(" ", 0, 10))
}
val customFontPath = getFontPath("font")
if (customFontPath.isNotEmpty()) {
setPrintFont(customFontPath)
} else {
setPrintFont("/system/fonts/DroidSansMono.ttf") // Fallback
}
val divider = drawableToBitmap(context, com.example.core.R.drawable.horizontal_line_svgrepo_com__1_)
printEntities.forEachIndexed { index, printEntity ->
// Header
printEntity.title?.let {title->
addPrintLine(
TextPrintLine(
title,
if (title.hasPersianLetters()) PrintLine.LEFT else PrintLine.RIGHT
)
)
}
printEntity.items?.let {items->
items.forEach { itemEntity ->
addPrintLine(
createPrintRow(
label = itemEntity.label,
value = itemEntity.value,
size = 16,
)
)
}
}
if (divider != null && printEntities.lastIndex > index) {
val dividerLine = BitmapPrintLine(divider, PrintLine.CENTER)
addPrintLine(dividerLine)
}
}
addPrintLine(TextPrintLine(" ", 0, 60))
val listener = object : IPrinterListener {
override fun onStart() {
Log.d(TAG, "Print started")
}
override fun onFinish() {
Log.d(TAG, "Print finished")
close()
}
override fun onError(code: Int, msg: String?) {
Log.e(TAG, "Print error - code: $code, message: $msg")
close()
}
}
beginPrint(listener)
}
}
// private fun fixForPrinter(input: String): String {
// val LRM = '\u200E' // کنترل جهت
//
// return input
// .toPersianDigits() // همونی که قبل دادم
// .replace(Regex("(\\d+)")) { matchResult ->
// "$LRM${matchResult.value}$LRM"
// }
// }
private fun String.hasPersianLetters(): Boolean {
return Regex("[\\u0621-\\u0628\\u062A-\\u063A\\u0641-\\u0642\\u0644-\\u0648\\u067E\\u0686\\u0698\\u06A9\\u06AF\\u06CC\\u0647]").containsMatchIn(
this
)
}
private fun drawableToBitmap(context: Context, @DrawableRes drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = createBitmap(
drawable.intrinsicWidth.coerceAtLeast(1),
drawable.intrinsicHeight.coerceAtLeast(1)
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun drawableToBitmap(
context: Context,
@DrawableRes drawableId: Int,
maxWidth: Int = 300
): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
val original = if (drawable is BitmapDrawable) {
drawable.bitmap
} else {
val bitmap = createBitmap(
drawable.intrinsicWidth.coerceAtLeast(1),
drawable.intrinsicHeight.coerceAtLeast(1)
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
bitmap
}
// 👇 scale با حفظ نسبت
val ratio = maxWidth.toFloat() / original.width
val width = maxWidth
val height = (original.height * ratio).toInt()
return Bitmap.createScaledBitmap(original, width, height, true)
}
}
@@ -0,0 +1,43 @@
package com.example.p3
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 P3Service @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.printLines(lines) { status ->
onMessage("P3 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,173 @@
package com.example.p3
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,21 @@
package com.example.p3.di
import android.content.Context
import com.example.p3.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 P3Module {
@Provides
@Singleton
fun provideIapPosService(@ApplicationContext context: Context): IapPosService {
return IapPosService(context)
}
}
@@ -0,0 +1,6 @@
package com.example.p3.models
data class IapAccountSplit(
val index: Int,
val percent: Int
)
Binary file not shown.
@@ -0,0 +1,17 @@
package com.example.p3
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)
}
}