react-native-mesmart 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,6 +59,7 @@ android {
59
59
  defaultConfig {
60
60
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
61
61
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
62
+ consumerProguardFiles "consumer-rules.pro"
62
63
  }
63
64
 
64
65
  buildTypes {
@@ -88,9 +89,10 @@ android {
88
89
  }
89
90
 
90
91
  repositories {
92
+ // local.properties e opcional (no EAS nao existe; o token vem da env var).
91
93
  def localProp = new Properties()
92
- def fileName = 'local.properties'
93
- localProp.load(new FileInputStream(rootProject.file(fileName)))
94
+ def localPropFile = rootProject.file('local.properties')
95
+ if (localPropFile.exists()) localPropFile.withInputStream { localProp.load(it) }
94
96
  def stoneToken = System.env.PACKAGECLOUD_READ_TOKEN != null ? System.env.PACKAGECLOUD_READ_TOKEN : localProp["packageCloudReadToken"] ?: ""
95
97
 
96
98
  mavenCentral()
@@ -0,0 +1,2 @@
1
+ # Interface trocada com o dex do SDK Gertec Neo (assets/gertec-neo): nao pode ser renomeada nem ter metodos removidos.
2
+ -keep class com.gertec.backend.** { *; }
@@ -0,0 +1 @@
1
+ 2.0.10-b12dc1dd8256
@@ -1,8 +1,15 @@
1
1
  package com.gertec
2
2
 
3
+ import android.util.Log
3
4
  import br.com.gertec.gedi.GEDI
4
5
  import com.facebook.react.bridge.Promise
5
6
  import com.facebook.react.bridge.ReactApplicationContext
7
+ import java.util.concurrent.ExecutionException
8
+ import java.util.concurrent.Executors
9
+ import java.util.concurrent.TimeUnit
10
+ import java.util.concurrent.TimeoutException
11
+ import com.gertec.backend.GertecBackend
12
+ import com.gertec.backend.GertecCallback
6
13
  import com.gertec.utils.MifareGPOS780
7
14
  import com.gertec.utils.PrinterGPOS780
8
15
  import com.mesmart.Utils
@@ -12,42 +19,124 @@ class GertecImpl(
12
19
  ) {
13
20
  private val isSupported = Utils.isGPOS780()
14
21
 
22
+ // GPOS780S usa o SDK Gertec Neo (libgedi 2.x), carregado a parte; os demais GPOS780 seguem na libgedi 1.16.
23
+ private val useNeo = Utils.isGPOS780S()
24
+
15
25
  @Volatile private var mifare: MifareGPOS780? = null
16
26
  @Volatile private var printer: PrinterGPOS780? = null
27
+ private var neo: GertecBackend? = null
28
+ private var neoError: Throwable? = null
29
+
30
+ // Falha no init da GEDI (ex.: libgedi.so incompativel com o Android do aparelho) nao pode derrubar o modulo:
31
+ // fica guardada e volta como erro legivel em cada chamada.
32
+ private var initError: Throwable? = null
17
33
 
18
34
  init {
19
- if (isSupported) {
20
- GEDI.init(reactContext.applicationContext)
35
+ if (isSupported && !useNeo) {
36
+ try {
37
+ GEDI.init(reactContext.applicationContext)
38
+ } catch (t: Throwable) {
39
+ initError = t
40
+ Log.e(TAG, "GEDI.init failed: ${t.javaClass.name}: ${t.message} (${Utils.deviceContext()})", t)
41
+ }
42
+ }
43
+ }
44
+
45
+ private fun checkSupported() {
46
+ if (!isSupported) throw Exception("Unsupported device (Supported models GPOS780, GPOS780S). ${Utils.deviceContext()}")
47
+ initError?.let {
48
+ throw Exception("GEDI init failed: ${it.javaClass.name}: ${it.message} (${Utils.deviceContext()})", it)
49
+ }
50
+ }
51
+
52
+ // GEDI.init() sobe a instancia numa thread e engole falhas; a lib de RFID le GEDI.getInstance() sem argumento
53
+ // e leva NPE se ainda for null. getInstance(context) cria de forma sincrona e deixa a causa real subir,
54
+ // com timeout p/ nao travar a fila de modulos nativos caso o init fique pendurado (visto na GPOS780S).
55
+ private fun ensureGedi() {
56
+ if (GEDI.getInstance() != null) return
57
+ val executor = Executors.newSingleThreadExecutor()
58
+ try {
59
+ executor.submit<Any?> { GEDI.getInstance(reactContext.applicationContext) }.get(GEDI_INIT_TIMEOUT_S, TimeUnit.SECONDS)
60
+ } catch (e: TimeoutException) {
61
+ throw Exception("GEDI init timeout (${GEDI_INIT_TIMEOUT_S}s) (${Utils.deviceContext()})", e)
62
+ } catch (e: ExecutionException) {
63
+ val cause = e.cause ?: e
64
+ throw Exception("GEDI init failed: ${cause.javaClass.name}: ${cause.message} (${Utils.deviceContext()})", cause)
65
+ } finally {
66
+ executor.shutdownNow()
67
+ }
68
+ if (GEDI.getInstance() == null) {
69
+ throw Exception("GEDI instance is null after init (${Utils.deviceContext()})")
21
70
  }
22
71
  }
23
72
 
24
73
  @Synchronized
25
74
  private fun ensureMifare(): MifareGPOS780 {
26
- if (!isSupported) throw Exception("Unsupported device (Supported models GPOS780, GPOS780S).")
75
+ checkSupported()
76
+ ensureGedi()
27
77
  if (mifare == null) mifare = MifareGPOS780()
28
78
  return mifare!!
29
79
  }
30
80
 
31
81
  @Synchronized
32
82
  private fun ensurePrinter(): PrinterGPOS780 {
33
- if (!isSupported) throw Exception("Unsupported device (Supported models GPOS780, GPOS780S).")
83
+ checkSupported()
34
84
  if (printer == null) printer = PrinterGPOS780(reactContext)
35
85
  return printer!!
36
86
  }
37
87
 
88
+ // Carga preguicosa: so a GPOS780S paga o custo de extrair o dex/.so da Neo, e a falha vira erro legivel por chamada.
89
+ @Synchronized
90
+ private fun ensureNeo(): GertecBackend {
91
+ neo?.let { return it }
92
+ neoError?.let { throw neoLoadException(it) }
93
+ return try {
94
+ NeoSdkLoader.load(reactContext.applicationContext).also { neo = it }
95
+ } catch (t: Throwable) {
96
+ neoError = t
97
+ Log.e(TAG, "Gertec Neo SDK load failed: ${t.javaClass.name}: ${t.message} (${Utils.deviceContext()})", t)
98
+ throw neoLoadException(t)
99
+ }
100
+ }
101
+
102
+ private fun neoLoadException(t: Throwable) =
103
+ Exception("Gertec Neo SDK load failed: ${t.javaClass.name}: ${t.message} (${Utils.deviceContext()})", t)
104
+
105
+ private fun neoCallback(promise: Promise, transform: (Any?) -> Any? = { it }) = object : GertecCallback {
106
+ override fun resolve(value: Any?) {
107
+ try {
108
+ promise.resolve(transform(value))
109
+ } catch (t: Throwable) {
110
+ promise.reject("GERTEC_RFID_ERROR", t.message, t)
111
+ }
112
+ }
113
+
114
+ override fun reject(code: String, message: String?, cause: Throwable?) {
115
+ if (cause != null) promise.reject(code, message, cause) else promise.reject(code, message)
116
+ }
117
+ }
118
+
38
119
  fun scanRfid(promise: Promise) {
39
120
  try {
121
+ if (useNeo) {
122
+ ensureNeo().scanRfid(neoCallback(promise) { Utils.resolveRFID(Utils.byteArrayToHexString(it as ByteArray)) })
123
+ return
124
+ }
40
125
  ensureMifare().scanRfid(promise)
41
126
  } catch (t: Throwable) {
42
- promise.reject(t)
127
+ promise.reject("GERTEC_RFID_ERROR", t.message, t)
43
128
  }
44
129
  }
45
130
 
46
131
  fun cancelScan(promise: Promise) {
47
132
  try {
133
+ if (useNeo) {
134
+ ensureNeo().cancelScan(neoCallback(promise))
135
+ return
136
+ }
48
137
  ensureMifare().cancelScan(promise)
49
138
  } catch (t: Throwable) {
50
- promise.reject(t)
139
+ promise.reject("GERTEC_RFID_ERROR", t.message, t)
51
140
  }
52
141
  }
53
142
 
@@ -55,13 +144,34 @@ class GertecImpl(
55
144
  lines: Int,
56
145
  promise: Promise
57
146
  ) {
58
- ensurePrinter().scrollPaper(lines, promise)
147
+ try {
148
+ if (useNeo) {
149
+ ensureNeo().scrollPrinterPaper(lines, neoCallback(promise))
150
+ return
151
+ }
152
+ ensurePrinter().scrollPaper(lines, promise)
153
+ } catch (t: Throwable) {
154
+ promise.reject("GERTEC_PRINTER_ERROR", t.message, t)
155
+ }
59
156
  }
60
157
 
61
158
  fun printHtml(
62
159
  html: String,
63
160
  promise: Promise
64
161
  ) {
65
- ensurePrinter().printHtml(html, promise)
162
+ try {
163
+ if (useNeo) {
164
+ ensureNeo().printHtml(html, neoCallback(promise))
165
+ return
166
+ }
167
+ ensurePrinter().printHtml(html, promise)
168
+ } catch (t: Throwable) {
169
+ promise.reject("GERTEC_PRINTER_ERROR", t.message, t)
170
+ }
171
+ }
172
+
173
+ private companion object {
174
+ const val TAG = "MeSmartGertec"
175
+ const val GEDI_INIT_TIMEOUT_S = 5L
66
176
  }
67
177
  }
@@ -18,7 +18,7 @@ class GertecModule(
18
18
  fun scanRfid(promise: Promise) {
19
19
  try {
20
20
  gertec.scanRfid(promise)
21
- } catch (e: Exception) {
21
+ } catch (e: Throwable) {
22
22
  e.printStackTrace()
23
23
  promise.reject(e)
24
24
  }
@@ -28,7 +28,7 @@ class GertecModule(
28
28
  fun cancelScan(promise: Promise) {
29
29
  try {
30
30
  gertec.cancelScan(promise)
31
- } catch (e: Exception) {
31
+ } catch (e: Throwable) {
32
32
  e.printStackTrace()
33
33
  promise.reject(e)
34
34
  }
@@ -41,7 +41,7 @@ class GertecModule(
41
41
  ) {
42
42
  try {
43
43
  gertec.scrollPrinterPaper(lines, promise)
44
- } catch (e: Exception) {
44
+ } catch (e: Throwable) {
45
45
  e.printStackTrace()
46
46
  promise.reject(e)
47
47
  }
@@ -54,7 +54,7 @@ class GertecModule(
54
54
  ) {
55
55
  try {
56
56
  gertec.printHtml(html, promise)
57
- } catch (e: Exception) {
57
+ } catch (e: Throwable) {
58
58
  e.printStackTrace()
59
59
  promise.reject(e)
60
60
  }
@@ -0,0 +1,94 @@
1
+ package com.gertec
2
+
3
+ import android.content.Context
4
+ import android.os.Process
5
+ import com.gertec.backend.GertecBackend
6
+ import dalvik.system.BaseDexClassLoader
7
+ import dalvik.system.DexClassLoader
8
+ import java.io.File
9
+
10
+ /**
11
+ * Carrega o SDK Gertec Neo (libgedi 2.x) num classloader proprio, so na GPOS780S.
12
+ *
13
+ * O app usa a libgedi 1.16 nos demais POS e as duas tem as mesmas classes e o mesmo libgedi.so (JNI por nome,
14
+ * entao nao da p/ renomear pacote), por isso a Neo vai num dex a parte (assets/gertec-neo, ver gertec-neo/build.sh)
15
+ * e o libgedi.so dela fica fora de jniLibs, extraido p/ uma pasta que so esse classloader enxerga.
16
+ */
17
+ internal object NeoSdkLoader {
18
+ private const val ASSET_DIR = "gertec-neo"
19
+ private const val ADAPTER_CLASS = "com.gertec.neo.NeoGertecBackend"
20
+
21
+ @Synchronized
22
+ fun load(context: Context): GertecBackend {
23
+ val abi = if (Process.is64Bit()) "arm64-v8a" else "armeabi-v7a"
24
+ val assets = context.assets
25
+ val version = assets.open("$ASSET_DIR/VERSION").bufferedReader().use { it.readText().trim() }
26
+
27
+ val root = File(context.codeCacheDir, "$ASSET_DIR/$version-$abi")
28
+ val jar = File(root, "neo-bridge.jar")
29
+ val libDir = File(root, "lib")
30
+ val so = File(libDir, "libgedi.so")
31
+
32
+ if (!jar.exists() || !so.exists()) {
33
+ File(context.codeCacheDir, ASSET_DIR).deleteRecursively()
34
+ libDir.mkdirs()
35
+ copyAsset(context, "$ASSET_DIR/neo-bridge.jar", jar)
36
+ copyAsset(context, "$ASSET_DIR/$abi/libgedi.so", so)
37
+ }
38
+ // Android 14+ recusa dex gravavel em DexClassLoader.
39
+ jar.setReadOnly()
40
+
41
+ val parent = GertecBackend::class.java.classLoader
42
+ ?: throw IllegalStateException("Sem classloader do app para o SDK Gertec Neo")
43
+ val loader = ChildFirstDexClassLoader(jar.absolutePath, libDir.absolutePath, parent)
44
+ val adapter = loader.loadClass(ADAPTER_CLASS)
45
+ return adapter.getConstructor(Context::class.java).newInstance(context.applicationContext) as GertecBackend
46
+ }
47
+
48
+ private fun copyAsset(context: Context, assetPath: String, dest: File) {
49
+ context.assets.open(assetPath).use { input ->
50
+ dest.outputStream().use { output -> input.copyTo(output) }
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Classloader "filho primeiro": as classes do dex da Neo (br.com.gertec.*, gedi.*, ...) tem o mesmo nome das da 1.16
57
+ * que estao no app, entao procura no dex antes de perguntar ao pai. Plataforma, Kotlin, React Native e a interface
58
+ * com.gertec.backend continuam vindo do pai, para os tipos trocados entre os dois lados serem os mesmos.
59
+ */
60
+ internal class ChildFirstDexClassLoader(
61
+ dexPath: String,
62
+ libPath: String,
63
+ parent: ClassLoader
64
+ ) : DexClassLoader(dexPath, null, libPath, parent) {
65
+
66
+ override fun loadClass(name: String, resolve: Boolean): Class<*> {
67
+ synchronized(this) {
68
+ findLoadedClass(name)?.let { return it }
69
+ if (!isShared(name)) {
70
+ try {
71
+ val clazz = findClass(name)
72
+ if (resolve) resolveClass(clazz)
73
+ return clazz
74
+ } catch (_: ClassNotFoundException) {
75
+ // nao esta no dex da Neo: cai no pai
76
+ }
77
+ }
78
+ return super.loadClass(name, resolve)
79
+ }
80
+ }
81
+
82
+ // O libgedi.so da Neo tem que vir daqui (nao da 1.16 do app); o resto (ex.: opencv) pode vir do pai.
83
+ override fun findLibrary(name: String): String? =
84
+ super.findLibrary(name) ?: (parent as? BaseDexClassLoader)?.findLibrary(name)
85
+
86
+ private fun isShared(name: String): Boolean = SHARED_PREFIXES.any { name.startsWith(it) }
87
+
88
+ private companion object {
89
+ val SHARED_PREFIXES = arrayOf(
90
+ "java.", "javax.", "android.", "androidx.", "dalvik.", "libcore.",
91
+ "kotlin.", "kotlinx.", "com.facebook.", "com.gertec.backend."
92
+ )
93
+ }
94
+ }
@@ -0,0 +1,20 @@
1
+ package com.gertec.backend;
2
+
3
+ /**
4
+ * Contrato entre o modulo e o SDK Gertec "Neo" (libgedi 2.x), que roda num classloader isolado.
5
+ * A interface e o callback sao carregados pelo classloader do app (pai) e so usam tipos da plataforma,
6
+ * assim o adaptador carregado do dex da Neo consegue implementar sem depender de classes do React Native.
7
+ */
8
+ public interface GertecBackend {
9
+ /** Resolve com o UID do cartao (byte[]). */
10
+ void scanRfid(GertecCallback callback);
11
+
12
+ /** Resolve com Boolean.TRUE. */
13
+ void cancelScan(GertecCallback callback);
14
+
15
+ /** Resolve com Boolean.TRUE. */
16
+ void scrollPrinterPaper(int lines, GertecCallback callback);
17
+
18
+ /** Resolve com Boolean.TRUE. */
19
+ void printHtml(String html, GertecCallback callback);
20
+ }
@@ -0,0 +1,7 @@
1
+ package com.gertec.backend;
2
+
3
+ public interface GertecCallback {
4
+ void resolve(Object value);
5
+
6
+ void reject(String code, String message, Throwable cause);
7
+ }
@@ -1,5 +1,6 @@
1
1
  package com.gertec.utils
2
2
 
3
+ import android.util.Log
3
4
  import br.com.gertec.easylayer.contactless.MifareClassic
4
5
  import com.facebook.react.bridge.Promise
5
6
  import com.mesmart.Utils
@@ -33,7 +34,10 @@ class MifareGPOS780 {
33
34
  } catch (e: Exception) {
34
35
  val msg = e.message?.lowercase().orEmpty()
35
36
  val isTimeoutLike = msg.contains("timeout") || msg.contains("polling")
36
- if (!isTimeoutLike) throw e
37
+ if (!isTimeoutLike) {
38
+ Log.e(TAG, "RFID connect failed: ${e.javaClass.name}: ${e.message}")
39
+ throw e
40
+ }
37
41
  } finally {
38
42
  try { mifare.close() } catch (_: Exception) {}
39
43
  }
@@ -46,9 +50,11 @@ class MifareGPOS780 {
46
50
  )
47
51
  else -> promise.reject("Timeout to read RFID card!")
48
52
  }
49
- } catch (e: Exception) {
50
- e.printStackTrace()
51
- promise.reject(e)
53
+ } catch (t: Throwable) {
54
+ // Throwable: falha da lib nativa e Error e, sem isso, a thread morre e a promise nunca resolve.
55
+ val message = "RFID scan threw ${t.javaClass.name}: ${t.message} (${Utils.deviceContext()})"
56
+ Log.e(TAG, message, t)
57
+ promise.reject("GERTEC_RFID_ERROR", message, t)
52
58
  } finally {
53
59
  try { mifare.close() } catch (_: Exception) {}
54
60
  mifare = MifareClassic()
@@ -70,4 +76,8 @@ class MifareGPOS780 {
70
76
  promise.reject(e)
71
77
  }
72
78
  }
79
+
80
+ private companion object {
81
+ const val TAG = "MeSmartGertec"
82
+ }
73
83
  }
@@ -1,9 +1,11 @@
1
1
  package com.gertec.utils
2
2
 
3
+ import android.util.Log
3
4
  import br.com.gertec.easylayer.printer.Printer
4
5
  import br.com.gertec.easylayer.printer.PrinterError
5
6
  import com.facebook.react.bridge.Promise
6
7
  import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.mesmart.Utils
7
9
 
8
10
  class PrinterGPOS780(
9
11
  private val reactContext: ReactApplicationContext
@@ -12,27 +14,48 @@ class PrinterGPOS780(
12
14
  lines: Int,
13
15
  promise: Promise
14
16
  ) {
15
- Printer.getInstance(reactContext.applicationContext, object: Printer.Listener {
16
- override fun onPrinterError(printerError: PrinterError?) {
17
- promise.reject("Cause: ${printerError?.cause}\\nError code: ${printerError?.code}\\nRequest ID: ${printerError?.requestId}\"")
18
- }
19
- override fun onPrinterSuccessful(var1: Int) {
20
- promise.resolve(true)
21
- }
22
- }).scrollPaper(lines)
17
+ try {
18
+ Printer.getInstance(reactContext.applicationContext, listener(promise, "scrollPaper"))
19
+ .scrollPaper(lines)
20
+ } catch (t: Throwable) {
21
+ reject(promise, "scrollPaper", t)
22
+ }
23
23
  }
24
24
 
25
25
  fun printHtml(
26
26
  html: String,
27
27
  promise: Promise
28
28
  ) {
29
- Printer.getInstance(reactContext.applicationContext, object: Printer.Listener {
30
- override fun onPrinterError(printerError: PrinterError?) {
31
- promise.reject("Cause: ${printerError?.cause}\\nError code: ${printerError?.code}\\nRequest ID: ${printerError?.requestId}\"")
32
- }
33
- override fun onPrinterSuccessful(var1: Int) {
34
- promise.resolve(true)
35
- }
36
- }).printHtml(reactContext.applicationContext, html)
29
+ try {
30
+ Printer.getInstance(reactContext.applicationContext, listener(promise, "printHtml"))
31
+ .printHtml(reactContext.applicationContext, html)
32
+ } catch (t: Throwable) {
33
+ reject(promise, "printHtml", t)
34
+ }
35
+ }
36
+
37
+ private fun listener(promise: Promise, operation: String) = object : Printer.Listener {
38
+ override fun onPrinterError(printerError: PrinterError?) {
39
+ val message = "Printer $operation failed. Cause: ${printerError?.cause}, " +
40
+ "error code: ${printerError?.code}, request ID: ${printerError?.requestId} " +
41
+ "(${Utils.deviceContext()})"
42
+ Log.e(TAG, message)
43
+ promise.reject("GERTEC_PRINTER_ERROR", message)
44
+ }
45
+
46
+ override fun onPrinterSuccessful(var1: Int) {
47
+ promise.resolve(true)
48
+ }
49
+ }
50
+
51
+ // Throwable (e nao so Exception): falha da lib nativa (UnsatisfiedLinkError, NoClassDefFoundError) e Error.
52
+ private fun reject(promise: Promise, operation: String, t: Throwable) {
53
+ val message = "Printer $operation threw ${t.javaClass.name}: ${t.message} (${Utils.deviceContext()})"
54
+ Log.e(TAG, message, t)
55
+ promise.reject("GERTEC_PRINTER_ERROR", message, t)
56
+ }
57
+
58
+ private companion object {
59
+ const val TAG = "MeSmartGertec"
37
60
  }
38
61
  }
@@ -18,11 +18,21 @@ object Utils {
18
18
  return Build.MODEL.equals("A910", true)
19
19
  }
20
20
 
21
- /** Cobre GPOS780 e GPOS780S (mesma familia Gertec, mesmo SDK). */
21
+ /** Cobre GPOS780 e GPOS780S (mesma familia Gertec). */
22
22
  fun isGPOS780(): Boolean {
23
23
  return Build.MODEL.startsWith("GPOS780", true)
24
24
  }
25
25
 
26
+ /** GPOS780S: nao inicializa o GEDI da libgedi 1.16, usa o SDK Neo (ver com.gertec.NeoSdkLoader). */
27
+ fun isGPOS780S(): Boolean {
28
+ return Build.MODEL.startsWith("GPOS780S", true)
29
+ }
30
+
31
+ /** Contexto do aparelho p/ mensagens de erro do Gertec (modelo + Android), p/ debugar em campo. */
32
+ fun deviceContext(): String {
33
+ return "model=${Build.MODEL}, android=${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})"
34
+ }
35
+
26
36
  fun isT8(): Boolean {
27
37
  return Build.MODEL.equals("T8", true)
28
38
  }
@@ -9,11 +9,16 @@ import android.graphics.Typeface
9
9
  import android.text.Layout
10
10
  import android.text.StaticLayout
11
11
  import android.text.TextPaint
12
+ import android.util.Log
12
13
  import br.com.stone.posandroid.providers.PosPrintProvider
13
14
  import com.facebook.react.bridge.Promise
14
15
  import com.facebook.react.bridge.ReactApplicationContext
15
16
  import com.stone.utils.StoneUtils
16
17
  import stone.application.interfaces.StoneCallbackInterface
18
+ import java.lang.reflect.Field
19
+ import java.lang.reflect.InvocationHandler
20
+ import java.lang.reflect.InvocationTargetException
21
+ import java.lang.reflect.Proxy
17
22
 
18
23
  /**
19
24
  * Mesmas primitivas da Sunmi, mas acumuladas: o HAL da Tectoy avanca o papel pra corte no fim de
@@ -68,6 +73,7 @@ class PrinterT8(
68
73
  return
69
74
  }
70
75
 
76
+ guardLateEvents()
71
77
  val provider = PosPrintProvider(reactContext)
72
78
  job.forEach { provider.addBitmap(it) }
73
79
  provider.connectionCallback = object : StoneCallbackInterface {
@@ -87,6 +93,47 @@ class PrinterT8(
87
93
  provider.execute()
88
94
  }
89
95
 
96
+ /**
97
+ * O HAL da Tectoy (posandroid-manufacturer-tectoy 2.2.10, SDK Stone 4.20.x) solta o PosPrinter no
98
+ * fim do job (onFinish -> close -> release) com eventos de status ainda na fila do main looper; o
99
+ * proximo onInfo chama getPrintStateInfo() com mPosPrinter nulo e o NPE derruba o app. Abre aqui o
100
+ * gerenciador que o HAL reaproveita no job e embrulha o listener que ele entrega ao PosPrinter no
101
+ * beginPrint, pra descartar so os eventos que chegam depois do release. Tudo por reflexao: se o
102
+ * HAL mudar, imprime como antes.
103
+ */
104
+ private fun guardLateEvents() {
105
+ try {
106
+ val services = Class.forName("com.transire.transireservice.TransireServiceManager")
107
+ .getMethod("getInstance").invoke(null) ?: return
108
+ val manager = services.javaClass.getMethod("getPOIPrinterService").invoke(services) ?: return
109
+ val listenerField = field(manager.javaClass, "eventListener")
110
+ val listener = listenerField.get(manager) ?: return
111
+ if (Proxy.isProxyClass(listener.javaClass)) return
112
+
113
+ val guard = InvocationHandler { _, method, args ->
114
+ try {
115
+ method.invoke(listener, *(args ?: emptyArray<Any?>()))
116
+ } catch (e: InvocationTargetException) {
117
+ if (e.cause !is NullPointerException || !isReleased(args?.firstOrNull())) throw e.cause ?: e
118
+ Log.w(TAG, "Evento ${method.name} da impressora depois do release ignorado")
119
+ null
120
+ }
121
+ }
122
+ listenerField.set(
123
+ manager,
124
+ Proxy.newProxyInstance(listener.javaClass.classLoader, listener.javaClass.interfaces, guard)
125
+ )
126
+ } catch (t: Throwable) {
127
+ Log.w(TAG, "Impressora sem protecao contra eventos tardios do HAL", t)
128
+ }
129
+ }
130
+
131
+ private fun isReleased(printer: Any?): Boolean =
132
+ printer != null && runCatching { field(printer.javaClass, "mPosPrinter").get(printer) == null }.getOrDefault(false)
133
+
134
+ private fun field(type: Class<*>, name: String): Field =
135
+ type.getDeclaredField(name).apply { isAccessible = true }
136
+
90
137
  private fun textBlock(
91
138
  text: String,
92
139
  fontSize: Int,
@@ -133,6 +180,7 @@ class PrinterT8(
133
180
  .apply { eraseColor(Color.WHITE) }
134
181
 
135
182
  companion object {
183
+ private const val TAG = "PrinterT8"
136
184
  private const val PRINT_WIDTH = 384
137
185
  private const val DEFAULT_FONT_SIZE = 24
138
186
  private const val CALIBRATION_CHARS = 32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-mesmart",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Native methods for integrating with Sunmi P2-B/P2-A11 POS, Pax A910/A920, Pagbank's Plugpag payment SDK, Stone's payment SDK, and Paystore's payment SDK.",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -27,7 +27,7 @@
27
27
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
28
28
  "clean": "del-cli android/build example/android/build example/android/app/build lib",
29
29
  "prepare": "bob build",
30
- "release": "release-it"
30
+ "release": "bash scripts/release.sh"
31
31
  },
32
32
  "keywords": [
33
33
  "react-native",