Feat(P3): Modularize module and add Pardakht Novin support
- Split `p3` module into `p3_pardakht_novin` and `p3_sepehr` sub-modules - Create `pardakht_novin_app` module with dedicated WebView and Hilt configuration - Rename P3 services, printers, and models to be vendor-specific (e.g., `P3PardakhtNovinService`) - Add `p3_pardakht_novin` and `p3_sepehr` product flavors to `tis_app` and `stage_app` - Update project settings and build configurations to include the new modular structure - Implement Sepehr-specific POS and Printer services in the new module
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,54 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.p3_pardakht_novin"
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
// targetSdk is often not needed for library modules
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-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)
|
||||
|
||||
// Using implementation to bundle the JAR and avoid NoClassDefFoundError
|
||||
implementation(files("libs/platform_sdk_v2.3.706.jar"))
|
||||
implementation(files("libs/classes-2.1.17.jar"))
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
+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
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.example.p3_pardakht_novin
|
||||
|
||||
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>
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package com.example.p3_pardakht_novin
|
||||
|
||||
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_pardakht_novin.models.IapAccountSplit
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class P3PardakhtNovinIapPosService @Inject constructor(
|
||||
@param:ApplicationContext 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()) {
|
||||
return PaymentResultEntity.failure(
|
||||
id = id,
|
||||
message = "پرداخت لغو شد!"
|
||||
)
|
||||
}
|
||||
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.SUCCESS else PaymentResultStatus.FAILURE,
|
||||
paymentResultDataEntity = PaymentResultEntity.PaymentResultDataEntity(
|
||||
terminalId = data["TerminalId"].toString(),
|
||||
stan = data["STAN"].toString(),
|
||||
rrn = data["RRN"].toString(),
|
||||
responseCode = data["ResponseCode"].toString(),
|
||||
customerCardNO = data["CustomerCardNO"].toString(),
|
||||
transactionDateTime = data["TransactionDateTime"].toString(),
|
||||
),
|
||||
message = data["Description"].toString()
|
||||
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
PaymentResultEntity.failure(
|
||||
id = id,
|
||||
message = e.message ?: "در هنگام پرداخت خطایی رخ داده است!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
package com.example.p3_pardakht_novin
|
||||
|
||||
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 P3PardakhtNovinPrinter @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(TextPrintLine(" ", 0, 10))
|
||||
addPrintLine(dividerLine)
|
||||
addPrintLine(TextPrintLine(" ", 0, 10))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package com.example.p3_pardakht_novin
|
||||
|
||||
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 P3PardakhtNovinPrinterHelper @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
|
||||
}
|
||||
*/
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.example.p3_pardakht_novin
|
||||
|
||||
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 P3PardakhtNovinService @Inject constructor(
|
||||
private val p3PardakhtNovinIapPosService: P3PardakhtNovinIapPosService,
|
||||
private val p3PardakhtNovinPrinterHelper: P3PardakhtNovinPrinterHelper
|
||||
) : PspService {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun pay(amount: Long): Intent {
|
||||
return p3PardakhtNovinIapPosService.startPurchase(amount)
|
||||
|
||||
}
|
||||
|
||||
override fun print(lines: List<String>, onMessage: (String) -> Unit) {
|
||||
p3PardakhtNovinPrinterHelper.printLines(lines) { status ->
|
||||
onMessage("P3 Printer: $status")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun decodePosResponse(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity {
|
||||
return try {
|
||||
p3PardakhtNovinIapPosService.decodeResponse(
|
||||
id = id,
|
||||
activityResult = activityResult
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
PaymentResultEntity.failure(
|
||||
id = id,
|
||||
e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.example.p3_pardakht_novin.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object P3PardakhtNovinModule {
|
||||
// P3PardakhtNovinIapPosService is provided via @Inject constructor
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example.p3_pardakht_novin.models
|
||||
|
||||
data class IapAccountSplit(
|
||||
val index: Int,
|
||||
val percent: Int
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
package com.example.p3_pardakht_novin
|
||||
|
||||
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